New: Try Voli The Bear, Fast package manager (and not only) for Windows
Updated Jul 10, 2026 Edit on GitHub

What Makes a Good Test

You can write a test and run it now. The next thing - the thing that separates tests that genuinely protect you from tests that just sit there looking responsible - is a handful of habits, none of them hard. They're the difference between a test suite you trust at 5pm on a Friday and one you secretly suspect is lying to you.

Let's go through them with the same total_with_tax function you've been using.

Test one behavior at a time

A good test checks one thing. When it fails, the failure should point at a single, specific behavior - so the red line tells you not just "something broke" but "this broke."

Compare these two:

# Cramming several checks into one test
def test_total_with_tax():
    assert total_with_tax(100, 0.10) == 110
    assert total_with_tax(0, 0.10) == 0
    assert total_with_tax(100, 0) == 100

What just happened: this runs three different checks in one test. The problem shows up the moment one fails: pytest stops at the first failing assert and never reaches the rest. If the middle line is broken, you won't even hear about the third. And the failure just says test_total_with_tax failed - which of the three? You have to go read the code to find out.

Split them so each behavior stands on its own:

def test_adds_ten_percent_tax():
    assert total_with_tax(100, 0.10) == 110

def test_zero_price_is_zero():
    assert total_with_tax(0, 0.10) == 0

def test_zero_tax_rate_returns_price_unchanged():
    assert total_with_tax(100, 0) == 100

What just happened: now each behavior is its own test. If the zero-tax case breaks, pytest tells you test_zero_tax_rate_returns_price_unchanged failed by name, and the other two still run and still report. One red line, one precise meaning.

Name the test for what it checks

The name is documentation that can't go stale, because it runs. When a test fails six months from now, the name in the output is the first thing you read - make it a sentence about the behavior, not a label.

   test_1                                    ← tells you nothing
   test_total_with_tax                       ← which behavior?
   test_zero_tax_rate_returns_price_unchanged ← you know exactly what broke

💡 Key point. Read the test name out loud. If it doesn't describe a behavior - "returns price unchanged when the tax rate is zero" - rename it. Future-you, staring at a red failure, will be grateful.

Keep tests fast and independent

Two qualities you want in every unit test:

  • Fast. A unit test should run in a blink. The pytest runs in Phase 2 finished in hundredths of a second, and that's the point - when the whole suite runs in a second or two, you run it constantly, after every small change. A slow suite is one you stop running, and a suite you don't run can't protect you.
  • Independent. Each test must pass or fail entirely on its own, no matter what ran before it. Tests shouldn't share state or depend on running in a particular order.

⚠️ Gotcha: the order-dependent test. If one test quietly leaves something behind - a changed global variable, a file on disk, a row in a database - and another test depends on that leftover, your tests pass together but fail when run alone or in a different order. pytest can even run tests in a different order between machines. The fix: each test arranges its own inputs from scratch (that's why "Arrange" is step one) and cleans up after itself. If two tests can't run in either order and both pass, one of them is lying.

Cover the edge cases

The happy path - total_with_tax(100, 0.10) == 110 - is the easy case, and the one least likely to be broken. The bugs hide at the edges. When you've written the obvious test, ask: what are the weird inputs?

For our function, the edges worth a test each:

  • Zero - total_with_tax(0, 0.10) should be 0. (A free item stays free.)
  • Zero rate - total_with_tax(100, 0) should be 100. (No tax means no change.)
  • Negative - what should a refund of -100 do? Decide the behavior, then pin it with a test.
def test_negative_price_keeps_the_sign():
    # A refund of 100 at 10% tax should refund 110, not 90.
    assert total_with_tax(-100, 0.10) == -110
$ pytest
========================= test session starts =========================
collected 4 items

test_tax.py ....                                                 [100%]

========================== 4 passed in 0.02s ==========================

What just happened: four green dots, one per test - the happy path plus three edge cases, each pinning down one behavior. If a future change accidentally mishandles a refund, this suite catches it. Each edge case you write is a future bug you've already fenced off.

📝 Terminology. An edge case is an input at the boundary of what the code handles - zero, empty, negative, the largest allowed value, the unexpected-but-legal. Edge cases are where bugs live, because they're the cases people forget while writing the original code.

The test that passes even when the code is wrong

This is the most dangerous test there is, and it's worth naming so you can spot it. A test can be green for the wrong reason - it looks like protection but checks nothing real.

def test_total_with_tax():
    result = total_with_tax(100, 0.10)
    # Oops - no assert. This test can never fail.

What just happened: this test calls the function, gets a result, and then... never checks it. There's no assert. It will pass forever - even if total_with_tax returns garbage, even if you break the code the way you did in Phase 2 - because passing only requires that the function doesn't crash. It gives you a green dot and zero protection.

⚠️ Gotcha. This is exactly why Phase 2 made you break the code and watch the test go red. A test you have never seen fail might be a test that can't fail. The cure is the same habit: when you write a test, make the code wrong once and confirm the test catches it. If it stays green while the code is broken, the test is the thing that's broken.

Recap

  1. One behavior per test - so a failure names exactly what broke, and one bad line doesn't hide the rest.
  2. Name it for the behavior - the name is documentation you read first when it fails.
  3. Fast and independent - quick enough to run constantly; passes or fails alone, in any order.
  4. Cover the edges - zero, empty, negative, boundaries; that's where bugs hide.
  5. Never trust a test you haven't watched fail - a test with no real assertion is worse than none.

You now have the full beginner's toolkit: the shape of a test, how to run it, how to read both outcomes, and the habits that keep your tests trustworthy. From here, the next question is which kinds of tests to write and where they fit - unit tests are the smallest of several layers.

Ready for the bigger picture? Unit, Integration, E2E explains how unit tests fit alongside the larger tests that check whole systems - and when to reach for each.

Related: Why Test At All? · Unit, Integration, E2E · Mocking and Test Doubles


← Phase 2: Write It and Run It · Guide overview

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. Why test one behavior per test instead of cramming several asserts in?

2. Why must tests be independent (not order-dependent)?

3. What is the most dangerous kind of test?