# Your First Unit Test

> Write and run a real unit test from scratch: learn the Arrange-Act-Assert shape, watch a test pass green and fail red, and pick up the habits that make a test worth keeping.


---

# Your First Unit Test

You've heard you "should write tests." Maybe you've opened a project full of `test_` files and felt
that quiet dread - everyone seems to know how this works except you. Here's the secret nobody tells you:
a unit test is one of the smallest, simplest pieces of code you'll ever write. It's a little function
that calls *your* function and checks the answer. That's the whole idea.

In this guide you'll write a real test, run it with a real test runner, watch it pass, then break your
code on purpose to watch it fail - because a test you've never seen fail is a test you can't trust. By
the end you'll have done the full loop yourself, and the dread will be gone.

We'll use **Python** with **pytest** because it's the gentlest place to start: the tests read almost like
plain English, and you check results with the ordinary `assert` keyword. The *shape* you'll learn is the
same in every language and every test framework - only the spelling changes.

## How to read this
- **Want it to finally make sense?** Read in order - each phase builds on the last, and you'll be typing
  along in your own terminal.
- **Already know the shape and just want to run one?** Jump to [Phase 2: Write It and Run It](02-write-it-and-run-it.md).

## The phases
1. **[Arrange, Act, Assert](01-arrange-act-assert.md)** - the universal shape every test takes: set up
   the inputs, call your code, check the result. We'll map out a tiny function and its test on paper first.
2. **[Write It and Run It](02-write-it-and-run-it.md)** - actually do it: create the test file, run
   pytest, read a green pass, then break the code on purpose and read the red failure (expected vs. actual).
3. **[What Makes a Good Test](03-what-makes-a-good-test.md)** - the habits that separate a test you trust
   from one that lies to you: one behavior per test, a clear name, speed and independence, and the edge
   cases that catch real bugs.

> This guide gets you writing tests for plain functions. Mocking, fixtures, and testing code that talks to
> databases or the network are their own skills - see the [related guides](#) below when you're ready.

**Related:** [Why Test At All?](/guides/why-test-at-all) · [Unit, Integration, E2E](/guides/unit-integration-e2e) · [Mocking and Test Doubles](/guides/mocking-and-test-doubles)


---

# Arrange, Act, Assert

Before you write a single line, it helps to know what you're aiming at - because a test isn't a mysterious
ritual. It's a tiny, repeatable experiment. You set something up, you do one thing to it, and you check
that what came out is what you expected. If a junior on your team can read a test top-to-bottom and say
"ah, this checks *that*," the test is doing its job.

That three-part shape has a name people use everywhere: **Arrange, Act, Assert** (often shortened to
"AAA"). Once you see it, you'll spot it in every test you ever read.

## What a unit test actually is

📝 **Terminology.** A **unit test** is a small piece of code that runs *one* other piece of code - usually
a single function - with known inputs, and checks that it produces the result you expect. The "unit" is
the small thing under test, normally one function.

It is not a special kind of program. It's just a function you write that *calls your function and checks
the answer*. The test runner finds it, runs it, and tells you pass or fail. That's the entire arrangement.

The reason tests feel scary is usually that people meet them as a wall of unfamiliar files before anyone
explains the shape. Once you have the shape, the wall turns back into a stack of small, boring functions -
which is exactly what you want.

## The three parts

Here's the shape, drawn out:

```mermaid
flowchart TD
  A["ARRANGE - set up the inputs (price = 100, tax_rate = 0.10)"]
  B["ACT - call the function once (result = total_with_tax(100, 0.10))"]
  C["ASSERT - check the result (assert result == 110)"]
  A --> B --> C
```

- **Arrange** - get everything ready. The inputs you'll pass in, and any setup the code depends on.
- **Act** - do the one thing you're testing. Call the function. Capture what it returns.
- **Assert** - state what *should* be true. If it's true, the test passes silently. If it's false, the
  test fails and tells you.

📝 **Terminology.** An **assertion** is a statement of what must be true. In Python you write it with the
built-in `assert` keyword: `assert result == 110` means "I claim `result` equals 110." If the claim holds,
nothing happens and execution continues. If it doesn't, Python raises an error - and that error is how the
test runner knows the test failed.

## A tiny function to test

Let's pick something real but small. Imagine a shop: you have a price, you add tax, you get a total. Here's
the function we'll test throughout the guide:

```python
def total_with_tax(price, tax_rate):
    return price + (price * tax_rate)
```

*What just happened:* nothing ran yet - this is the code *under test*, the thing we want to make sure
works. It takes a `price` and a `tax_rate` (like `0.10` for 10%), adds the tax on top, and returns the
total. For a price of `100` and a rate of `0.10`, we'd expect `110`.

Notice we picked a function that takes inputs and returns a value, with no surprises - no printing, no
saving to a file, no calling the internet. That's the easiest kind of code to test, and it's worth
*writing* your code this way partly because it makes testing this simple. (Code that talks to databases or
the network needs extra techniques - see [Mocking and Test Doubles](/guides/mocking-and-test-doubles) when
you get there.)

## Mapping the three parts onto our function

Now line up Arrange-Act-Assert against `total_with_tax`. Don't run anything yet - just read how the parts
map. We'll type the real file in the next phase.

```python
def test_total_with_tax_adds_ten_percent():
    # Arrange: set up the inputs
    price = 100
    tax_rate = 0.10

    # Act: call the function once, capture the result
    result = total_with_tax(price, tax_rate)

    # Assert: state what should be true
    assert result == 110
```

*What just happened:* you read your first complete test. It's an ordinary function whose name starts with
`test_` (that prefix is how pytest finds it - more on that next phase). Inside, the three parts are right
there in order: arrange the inputs, act by calling `total_with_tax`, assert the answer is `110`. The
comments aren't required, but writing them while you learn keeps the shape clear.

💡 **Key point.** A test is a function that calls your function and checks the result. Arrange the inputs,
Act by calling it, Assert the answer. Hold onto that one sentence - everything else in testing is a
variation on it.

⚠️ **Gotcha.** Keep the **Act** step to a single call to the thing you're testing. If a test calls three
different functions and then asserts, and it fails, you won't know *which* of the three broke. One test,
one behavior, one act - we'll come back to why this matters in [Phase 3](03-what-makes-a-good-test.md).

## Recap

1. A **unit test** is a small function that runs one piece of your code and checks the result.
2. Every test has the same three parts: **Arrange** (set up inputs), **Act** (call the code once),
   **Assert** (check the result).
3. An **assertion** (`assert result == 110`) states what must be true; if it's false, the test fails.
4. The easiest code to test is a function that takes inputs and returns a value - like `total_with_tax`.

You've got the shape. Next, let's turn this from a diagram into a real file you run - and watch it pass,
then fail.


---

# Write It and Run It

You've seen the shape on paper. Now you'll make it real - a file on disk, a command in your terminal, and
output you can read. This is the part that turns "I sort of get testing" into "I've done it." Type along;
running it once yourself teaches more than reading it ten times.

We'll do the full loop: write the code and the test, run it and read a **green** pass, then deliberately
break the code and read a **red** failure. That last step matters more than it sounds - a test you've only
ever seen pass might be passing for the wrong reason. Watching it fail is how you earn the right to trust it.

## Step 1: Install pytest

📝 **Terminology.** **pytest** is a *test runner* - a program that finds your test functions, runs them,
and reports pass or fail. It's the most common test runner in Python and not part of the language itself,
so you install it once:

```console
$ pip install pytest
Collecting pytest
...
Successfully installed pytest-8.2.0 ...
```

*What just happened:* `pip` (Python's package installer) downloaded pytest and put it on your system, so
the `pytest` command now works in your terminal. Your version number may differ from `8.2.0`; that's fine.
If `pip` isn't found, try `python -m pip install pytest` instead - same result, called a slightly
different way.

## Step 2: Put the code and the test in a file

Make a folder, move into it, and create one file. (`mkdir` makes a directory, `cd` moves into it - same
two commands you'd use to start any project.)

```console
$ mkdir tax-test
$ cd tax-test
```

*What just happened:* you're now standing inside an empty folder called `tax-test`. Everything next goes
in here.

Create a file named `test_tax.py` with this content - the function under test *and* the test for it,
together for now so there's nothing to wire up:

```python
def total_with_tax(price, tax_rate):
    return price + (price * tax_rate)

def test_total_with_tax_adds_ten_percent():
    # Arrange
    price = 100
    tax_rate = 0.10
    # Act
    result = total_with_tax(price, tax_rate)
    # Assert
    assert result == 110
```

*What just happened:* you wrote a file with two functions. `total_with_tax` is your real code. The second
one, `test_total_with_tax_adds_ten_percent`, is the test from Phase 1 - Arrange the inputs, Act by calling
the function, Assert the result is `110`.

⚠️ **Gotcha.** The file name and the test function name both need to start with `test`. By default pytest
only looks in files named `test_*.py` (or `*_test.py`) and only runs functions whose names start with
`test`. Name your test `check_tax` instead of `test_tax` and pytest will silently skip it - and a test
that never runs is worse than no test, because you'll *think* you're covered.

## Step 3: Run it and read the green

From inside the `tax-test` folder, run pytest:

```console
$ pytest
========================= test session starts =========================
platform linux -- Python 3.11.4, pytest-8.2.0, pluggy-1.5.0
rootdir: /home/ada/tax-test
collected 1 item

test_tax.py .                                                    [100%]

========================== 1 passed in 0.01s ==========================
```

*What just happened:* pytest searched the folder, **collected 1 item** (your one test), ran it, and it
passed. Two things tell you it's green:

- The single dot after `test_tax.py` - pytest prints one `.` for each passing test.
- The last line: `1 passed`. That's the summary you're looking for.

No assertion failed, so pytest stayed quiet about the details and just reported success. **You've now
written and run a real unit test.** That green line is the whole reward - it means your code did what you
claimed it would.

💡 **Key point.** Green means every assertion held. Red means at least one didn't. You read the *last
line* first - `1 passed` or `1 failed` - then dig into the details only when something's red.

## Step 4: Break the code on purpose

Here's the step beginners skip and seniors never do. A passing test only tells you something if it would
*fail* when the code is wrong. So let's make the code wrong and confirm the test catches it.

Change `total_with_tax` to add the tax twice - a realistic bug, the kind of slip that happens for real:

```python
def total_with_tax(price, tax_rate):
    return price + (price * tax_rate) + (price * tax_rate)
```

*What just happened:* you introduced a bug on purpose. For a price of `100` at `0.10`, this now returns
`120` instead of `110` - it adds the tax twice. The test still expects `110`. Leave the test exactly as it
was.

Now run it again:

```console
$ pytest
========================= test session starts =========================
platform linux -- Python 3.11.4, pytest-8.2.0, pluggy-1.5.0
rootdir: /home/ada/tax-test
collected 1 item

test_tax.py F                                                    [100%]

============================== FAILURES ===============================
________________ test_total_with_tax_adds_ten_percent _________________

    def test_total_with_tax_adds_ten_percent():
        # Arrange
        price = 100
        tax_rate = 0.10
        # Act
        result = total_with_tax(price, tax_rate)
        # Assert
>       assert result == 110
E       assert 120 == 110

test_tax.py:11: AssertionError
========================= 1 failed in 0.02s ===========================
```

*What just happened:* the test caught the bug - exactly as it should. Read the output from the bottom up,
because that's where the answer is:

- The last line is now `1 failed` (and the dot became an `F` up top, for "fail").
- `assert 120 == 110` is pytest showing you the comparison that failed, with the **real values filled in**.
  The `>` marks the line that blew up.
- `E   assert 120 == 110` is the punchline: your code produced **120** (the *actual*), but the test
  expected **110** (the *expected*). That gap - 120 vs. 110 - is the bug, named in numbers.
- `test_tax.py:11` tells you the exact file and line, so you know where to look.

This is what a test failure *is*: a precise report of "you said this should be 110, but it was 120, on this
line." It doesn't just say "something's wrong" - it hands you the expected value, the actual value, and the
location.

⚠️ **Gotcha.** When a test goes red, resist the urge to "fix the test" so it passes again. The test is the
messenger. Nine times out of ten the *code* is wrong, and the test just did its only job. Change the test
only when you've decided the expected behavior itself is genuinely different.

## Step 5: Fix it and go back to green

Put the function back the way it was - one tax, not two:

```python
def total_with_tax(price, tax_rate):
    return price + (price * tax_rate)
```

```console
$ pytest
========================= test session starts =========================
collected 1 item

test_tax.py .                                                    [100%]

========================== 1 passed in 0.01s ==========================
```

*What just happened:* back to green. You've now seen the full lifecycle with your own eyes - a test that
passes when the code is right, *and* fails when the code is wrong. That round trip is what makes the green
mean something. A test you've watched fail is a test you can trust.

## Recap

1. **pytest** is the test runner; install it once with `pip install pytest`.
2. Tests live in files named `test_*.py`; test functions start with `test`. Misname either and pytest
   silently skips it.
3. Run `pytest` from your folder. Read the **last line** first: `1 passed` (green) or `1 failed` (red).
4. On failure, pytest shows you `assert <actual> == <expected>` with real values, plus the file and line.
   Read it bottom-up.
5. Always watch a test fail at least once. A test that can't fail can't protect you.

You can now write a test, run it, and read both outcomes. Next: the difference between a test that protects
you and one that quietly lies - the habits that make a test worth keeping.

---

Run the tests below - then break the function on purpose and watch them turn red:

```playground-unittest
```


---

# 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:

```python
# 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:

```python
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.

```text
   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.

```python
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
```

```console
$ 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.

```python
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](/guides/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?](/guides/why-test-at-all) · [Unit, Integration, E2E](/guides/unit-integration-e2e) · [Mocking and Test Doubles](/guides/mocking-and-test-doubles)
