Home / Courses / Testing for Beginners / I — Core Concepts
I — Core Concepts
Article 2 of 9

Anatomy of a Good Test: AAA, Test Doubles, and the FIRST Principles

The Arrange-Act-Assert pattern, what actually distinguishes unit, integration, and E2E tests, the vocabulary of test doubles (dummy, stub, fake, mock, spy), and the FIRST principles as a yardstick for a healthy test.

July 10, 2026

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
  • Arrange — set up everything needed: data, objects, initial state.
  • Act — call the ONE thing under test. Ideally a single line/single call — if this part needs many steps, that's usually a sign the unit under test is too large.
  • Assert — check the result. A single test should ideally focus on ONE behavior, though it can have several related 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 TestIntegration TestE2E Test
    Exampleverify_password("x", hash) tested on its ownPOST /auth/login via TestClient, a real SQLite databaseOpen a real browser, fill in the login form, click submit
    SpeedMillisecondsTens to hundreds of millisecondsSeconds to minutes
    Needs a database/network?NoYes (usually a test/in-memory version)Yes, real systems
    Easy to tell why it failed?Very easy — just one functionFairly easy — a handful of componentsHard — many possible causes
    This is exactly why the test folder structure in 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:

  • Dummy — an object that just "fills a slot" (e.g., a required parameter that isn't actually used in a particular test), with no behavior at all.
  • Stub — an object that returns a fixed/hardcoded answer when called, regardless of the input. Used when we just need the dependency to "return something" so the code can keep running.
  • Fake — a lightweight implementation that ACTUALLY works, just a simpler version of the real thing. A concrete example we've already used since the Security series: FakeRedis — an in-memory implementation that genuinely counts and stores values, exactly like real Redis, just without needing an actual Redis process running.
  • Mock — an object that doesn't just stand in, but also RECORDS how it was called (how many times, with what arguments), so a test can verify the INTERACTION, not just the end result. We'll use this in the article on mocking to verify "was the notification function actually called."
  • Spy — similar to a mock, but wraps the REAL object while still recording its interactions — so the real behavior still runs, it's just being "watched."
  • 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:

  • Fast — tests must be fast. Slow tests get run less often, and tests that run less often lose their value.
  • Independent — one test must not depend on another (run order, state left behind by a previous test). Every test must be able to run on its own, in any order.
  • Repeatable — the result must be THE SAME every time it runs, in any environment — not sometimes passing, sometimes failing with no code change (called a flaky test, covered further in the closing article of this series).
  • Self-validating — a test must automatically report pass/fail (via assert), without a human having to read the output and draw their own conclusion.
  • Timely — tests are written CLOSE to when the code itself is written (ideally at the same time, or even before — this is the spirit of TDD we'll practice in the article on TDD), not piled up as a separate "later" task.
  • 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.

    Summary

  • The Arrange-Act-Assert pattern gives tests a consistent structure that makes them easier to read and diagnose.
  • Unit, integration, and E2E tests differ in how many real components are involved — a trade-off between realism and speed.
  • Test doubles (dummy, stub, fake, mock, spy) are vocabulary for the different ways of replacing a dependency during a test — pick based on need: just "staying quiet" (stub) vs. needing the interaction verified (mock).
  • The FIRST principles (Fast, Independent, Repeatable, Self-validating, Timely) are a quick yardstick for judging the health of a test.
  • Topics

    TestingFundamentals