Skip to content
All posts
January 9, 20263 min read

The QA Mindset Every Developer Needs to Adopt

QA thinking isn't about finding bugs after code is written — it's a mindset that shapes how you design, build, and validate software from the start. Here's what that mindset looks like in practice.

TestingCareerBest Practices
Share:

The best developers I've worked with think like QA engineers. Not because they're looking for defects in their own code, but because they've internalized a way of thinking about systems that prevents defects from forming.

Here's the mindset — and how to develop it.


Think About Failure Before Success

When writing any function, the first question should be: how can this fail?

Not "what does it do when everything works" — that's easy. "What does it do when the network drops mid-request? When the input is null? When the database returns zero rows? When the user hits the back button at exactly the wrong moment?"

Most bugs live in paths the developer didn't think through. QA engineers find them because they're trained to think about them first.

kotlin
// Developer thinking (happy path first):
fun createTask(title: String): Task {
    val task = Task(id = UUID.randomUUID().toString(), title = title)
    repository.save(task)
    return task
}

// QA thinking (failure modes first):
fun createTask(title: String): Result<Task> {
    if (title.isBlank()) return Result.failure(ValidationException("Title cannot be empty"))
    if (title.length > 200) return Result.failure(ValidationException("Title too long"))
    
    return try {
        val task = Task(id = UUID.randomUUID().toString(), title = title.trim())
        repository.save(task)
        Result.success(task)
    } catch (e: Exception) {
        Result.failure(e)
    }
}

The second version handles the cases that will actually occur.


Question Requirements, Not Just Implementations

QA engineers question the specification, not just the implementation. When you get a feature request, ask:

  • What happens when the user provides invalid input?
  • What's the maximum allowed value?
  • What happens when the network times out?
  • What's the expected behavior when the user doesn't have permission?
  • What if the user does X while Y is loading?

These questions often reveal requirements gaps that are much cheaper to resolve before development than after.


Test Your Own Code Before Calling It Done

"Done" doesn't mean "it works in the one scenario I tested." Done means:

  • Happy path tested
  • Primary edge cases tested
  • Error handling tested
  • Tested by someone other than the author (or at a minimum, fresh eyes)

The code review step where another developer tests your feature isn't a formality — it's the minimum viable QA step. If your team doesn't have a QA engineer, developers must cover both roles.


Write Assertions, Not Just Code

Every piece of logic has invariants — conditions that must always be true. Make them explicit:

kotlin
fun calculateDiscount(price: Double, percentage: Int): Double {
    require(price >= 0) { "Price cannot be negative" }
    require(percentage in 0..100) { "Percentage must be between 0 and 100" }
    
    val discount = price * (percentage / 100.0)
    
    check(discount >= 0) { "Calculated discount is negative — logic error" }
    check(discount <= price) { "Discount exceeds price — logic error" }
    
    return discount
}

code
require()
validates inputs.
code
check()
validates outputs. Both throw
code
IllegalArgumentException
/
code
IllegalStateException
in debug builds and catch logic errors that would otherwise silently corrupt data.


Reproduce Before Fixing

When a bug is reported, the first step is reproduction — not fixing. This seems obvious but developers routinely apply "obvious" fixes without confirming the bug and verifying the fix.

Reproduction proves:

  1. You understand the actual problem (not a similar-looking problem)
  2. Your fix actually resolves it (not a different path)
  3. You know what a passing state looks like

The reproduction step is also where you write the regression test that prevents this bug from returning.


Takeaways

  • Think about failure modes before success paths — most bugs live in unconsidered paths
  • Question requirements, not just implementations — specification gaps are cheaper to fix early
  • "Done" means edge cases and error handling tested, not just happy path
  • Explicit assertions (
    code
    require
    ,
    code
    check
    ) catch logic errors immediately instead of silently
  • Always reproduce before fixing — it proves you understand the actual problem
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus