In the previous article, we covered why testing matters. Now it's time to dissect what makes a test GOOD, not just "exists and runs." Think of it as getting familiar with the vocabulary and structure before we write our first real test in the next article — so that every line we write later has a reason behind it.
The AAA Pattern: Arrange, Act, Assert
Nearly every good test, in any language or framework, follows the same three-part structure:
def test_verify_password_accepts_correct_password():
# Arrange — set up the initial state we need
hashed = hash_password("supersecret123")
# Act — run the thing being tested
result = verify_password("supersecret123", hashed)
# Assert — check the result matches what we expect
assert result is True
assert statements to verify that behavior.This pattern matters for more than tidiness — tests that follow AAA are far easier to re-read months later, and easier to diagnose when they fail, because it's clear which part is wrong: the setup, the action, or the expectation.
Unit vs. Integration vs. E2E — The Details That Set Them Apart
In the previous article we already met the testing pyramid. Now let's get more specific about what distinguishes each layer, using real examples from task-tracker-api:
| Unit Test | Integration Test | E2E Test | |
|---|---|---|---|
| Example | verify_password("x", hash) tested on its own | POST /auth/login via TestClient, a real SQLite database | Open a real browser, fill in the login form, click submit |
| Speed | Milliseconds | Tens to hundreds of milliseconds | Seconds to minutes |
| Needs a database/network? | No | Yes (usually a test/in-memory version) | Yes, real systems |
| Easy to tell why it failed? | Very easy — just one function | Fairly easy — a handful of components | Hard — many possible causes |
task-tracker-api will be split into tests/unit/ and tests/integration/ — not just for tidiness, but so we can run the fast SUBSET (pytest -m unit) when we need instant feedback while writing code, and the full suite before committing.
Test Doubles — Vocabulary for "Standing In" for a Dependency
Often, the code we want to test depends on something we do NOT want to involve for real during a test — a real database, an external API, actually sending an email. A test double is the general term for a stand-in object used in tests, replacing the real dependency. There are several kinds, each with a different purpose:
FakeRedis — an in-memory implementation that genuinely counts and stores values, exactly like real Redis, just without needing an actual Redis process running.These terms are sometimes used loosely in everyday conversation (many people say "mock" for all of the above), but understanding the difference helps you pick the right tool: if you just need a dependency to "stay quiet and not get in the way," a stub is enough. If you need to PROVE something was actually called, that's a mock's job.
The FIRST Principles — Traits of a Healthy Test
Another useful framework for judging test quality:
assert), without a human having to read the output and draw their own conclusion.We'll come back to the Independent and Repeatable principles concretely in the next article, when we cover why every integration test needs a truly clean, isolated database, rather than a shared database reused across tests.