Skip to content
← Back to Codex
QA8 min read

QA Fundamentals: Writing Test Cases That Actually Find Bugs

Anyone can write test cases. The difference between a junior QA engineer and a senior one is how many bugs their tests find per test written. The secret isn't writing more tests — it's writing smarter ones using test design techniques that systematically target where bugs hide.

Why Random Testing Fails

Most people start testing by trying obvious inputs: enter a valid email, click submit, check the result. This catches maybe 20% of the bugs. The other 80% live in the gaps — what happens at the boundaries, what happens with unexpected combinations, what happens when things are empty or null.

Test design techniques are systematic methods for choosing inputs that maximize your chances of finding bugs while minimizing the number of tests you need to write. Three techniques cover most real-world testing.

Equivalence Partitioning

The core idea: inputs that should be handled the same way belong to the same partition. You only need one test per partition because if one value in the group works, they all should. If one fails, they all should.

Example: An age field that accepts values 18-65 for insurance eligibility.

The partitions are:

Instead of testing ages 18, 19, 20, 21... through 65, you test one value from each partition:

| Partition | Test Value | Expected Result | |-----------|-----------|-----------------| | Valid | 30 | Accepted | | Too young | 10 | Rejected: "Must be 18 or older" | | Too old | 70 | Rejected: "Must be 65 or younger" | | Negative | -1 | Rejected: "Invalid age" | | Non-numeric | "abc" | Rejected: "Must be a number" | | Empty | "" | Rejected: "Age is required" |

Six tests instead of sixty-five, and they cover every meaningful category of input.

Boundary Value Analysis

Bugs cluster at the edges. Off-by-one errors are the most common bug in software, and boundary analysis is designed specifically to catch them.

For our age field (18-65), the boundaries are:

| Value | Expected | What It Catches | |-------|----------|-----------------| | 17 | Rejected | Off-by-one at lower bound | | 18 | Accepted | Lower bound works | | 19 | Accepted | Just above lower bound | | 64 | Accepted | Just below upper bound | | 65 | Accepted | Upper bound works | | 66 | Rejected | Off-by-one at upper bound |

The classic bug this catches: a developer writes if (age > 18) instead of if (age >= 18), rejecting exactly 18-year-olds. Without boundary tests, you'd never notice — your test with age 30 would pass fine.

Combine this with equivalence partitioning. The partitions tell you which categories to test. The boundaries tell you exactly which values within those categories are most likely to expose bugs.

Decision Tables

When a feature has multiple inputs that interact with each other, decision tables map every combination to an expected outcome. They're especially powerful for business rules.

Example: A shipping calculator with two inputs — membership tier and order total:

| # | Tier | Order Total | Expected Shipping | |---|------|------------|-------------------| | 1 | Gold | $30 | Free | | 2 | Gold | $75 | Free | | 3 | Gold | $150 | Free | | 4 | Silver | $30 | $5.99 | | 5 | Silver | $75 | Free | | 6 | Silver | $150 | Free | | 7 | Regular | $30 | $5.99 | | 8 | Regular | $75 | $5.99 | | 9 | Regular | $150 | Free |

Nine tests cover every meaningful combination. Without the table, it's easy to miss cases — like a Silver member at exactly $50 (boundary!) or a Regular member at $100. The table forces completeness.

State Transition Testing

Some features behave differently based on what happened before. An order that's "Pending" can be "Cancelled," but a "Shipped" order can't. State transition diagrams map valid and invalid transitions.

[Pending] --pay--> [Paid] --ship--> [Shipped] --deliver--> [Delivered]
    |                 |                                          |
    +---cancel--->  [Cancelled]                              [Returned]
                      ^                                          |
                      +--------return (within 30 days)-----------+

Test cases from this diagram:

State transition testing catches bugs where the system allows impossible operations — like re-activating a cancelled subscription or shipping an already-delivered order.

Putting It Together

In practice, I combine these techniques:

  1. Start with equivalence partitioning to identify the categories of inputs.
  2. Apply boundary analysis to each partition edge.
  3. Build decision tables when multiple inputs interact.
  4. Map state transitions for workflow-based features.

For the age field alone, this gives me about 10 targeted test cases instead of either 3 (too few — misses boundary bugs) or 100 (too many — wastes time testing the same partition).

Real-World Application

These techniques aren't academic exercises. Last quarter, I used decision table testing on a permissions system and found that admins couldn't delete users who had pending invitations — a combination nobody had tested because the individual features worked fine. The bug had been in production for months.

Boundary analysis caught an off-by-one in a pagination endpoint: requesting page 0 returned the same results as page 1 instead of an error, which broke the frontend's "no results" detection.

The test design technique doesn't need to be formal or documented in a matrix. What matters is the thinking — systematically asking "where are the edges?" and "what combinations haven't I tried?" That habit finds more bugs in one afternoon than weeks of random clicking.