Python · Testing with pytest · Beginner

Your First Test

5 tasks

Write and run your first pytest test. Learn how pytest discovers tests and reads output.

What Is Testing and Why pytest

#
Testing is the practice of writing code that checks whether your other code works correctly. More than that — tests give you the confidence to *change* code. When you refactor, fix a bug, or add a feature, the test suite immediately tells you if something broke elsewhere. Without tests, every change carries silent risk. The most basic test has three steps: 1. Call your production function with a specific input 2. Capture the result 3. Assert the result equals what you expected If the assertion is true, the test passes. If not, pytest shows you exactly what differed. The `assert` keyword is Python's built-in mechanism for this: it evaluates an expression and raises `AssertionError` if the result is falsy. In a test you write `assert result == expected` — that's the whole foundation. The next topic (*Assertions in Depth*) explains how pytest dramatically improves `assert` output and covers patterns for comparing floats, lists, dicts, and more. **Why pytest, not unittest?** Python's standard library includes `unittest`, but it's verbose. You must inherit from `TestCase`, use `self.assertEqual()` instead of plain `assert`, and wrap everything in classes. pytest needs none of that: ```python # unittest — a lot of ceremony import unittest class TestMath(unittest.TestCase): def test_add(self): self.assertEqual(add(2, 3), 5) # pytest — just a function def test_add(): assert add(2, 3) == 5 ``` Beyond syntax, pytest's failure messages are far more informative — it shows the actual values that were compared. And pytest's fixtures, parametrize, and plugin ecosystem are vastly better. **Installation:** ```bash pip install pytest ``` **How test discovery works:** pytest walks your project directory and collects tests by following these naming rules: - Files: `test_*.py` or `*_test.py` - Functions at module level: start with `test_` - Classes: start with `Test` (no `__init__` method) - Methods inside `Test*` classes: start with `test_` Everything else is ignored. `helper.py`, `utils.py`, `main.py` — not collected. A function named `check_result()` — not collected. Only the pattern `test_*` triggers collection. **Running tests:** ```bash pytest # run all discovered tests pytest -v # verbose: show each test name and result pytest -s # show print() output (don't suppress stdout) pytest --tb=short # shorter tracebacks on failure pytest --collect-only # show what would run without running it ```

A Complete First Test File

#
Let's write a small module and a full test file for it — then see what pytest output looks like for both passing and failing tests. ```python # string_utils.py def reverse(s): return s[::-1] def truncate(text, max_len): if len(text) <= max_len: return text return text[:max_len] def slugify(text): return text.lower().strip().replace(' ', '-') ``` ```python # test_string_utils.py from string_utils import reverse, truncate, slugify def test_reverse_basic(): assert reverse('hello') == 'olleh' def test_reverse_empty(): assert reverse('') == '' def test_truncate_longer_than_limit(): assert truncate('hello world', 5) == 'hello' def test_truncate_within_limit(): result = truncate('hi', 10) assert result == 'hi' assert len(result) == 2 def test_slugify_spaces(): assert slugify('Hello World') == 'hello-world' def test_slugify_strips_whitespace(): assert slugify(' python ') == 'python' ``` Running `pytest -v` produces: ``` test_string_utils.py::test_reverse_basic PASSED test_string_utils.py::test_reverse_empty PASSED test_string_utils.py::test_truncate_longer_than_limit PASSED test_string_utils.py::test_truncate_within_limit PASSED test_string_utils.py::test_slugify_spaces PASSED test_string_utils.py::test_slugify_strips_whitespace PASSED 6 passed in 0.04s ``` Now deliberately break `reverse` — change `s[::-1]` to `s` — and run again: ``` FAILED test_string_utils.py::test_reverse_basic AssertionError: assert 'hello' == 'olleh' ``` pytest shows the actual value (`'hello'`) vs what you expected (`'olleh'`). No print statements needed — the assertion output is the diagnostic. **Three test result states:** | Symbol | Meaning | |--------|---------| | `.` | PASSED | | `F` | FAILED (assertion failed) | | `E` | ERROR (exception outside an assert — setup or teardown crashed) |

pytest CLI Reference

#
```bash pytest # run all tests pytest -v # verbose: show each test name pytest -s # show print() output (don't capture stdout) pytest test_foo.py # run one file pytest -k "add" # run tests whose name contains "add" pytest --tb=short # shorter tracebacks pytest --tb=no # no tracebacks (just pass/fail count) ``` **Naming rules pytest follows:** - Files: `test_*.py` or `*_test.py` - Functions and methods: `test_*` - Classes: `Test*` (no `__init__` method) **What pytest outputs:** ``` collected 4 items test_math_utils.py::test_add_positive_numbers PASSED test_math_utils.py::test_add_negative PASSED test_math_utils.py::test_clamp_within_range PASSED test_math_utils.py::test_clamp_below_lower PASSED 4 passed in 0.05s ``` **When to add `import pytest`:** Simple tests need only `assert` and your own imports — no `import pytest`. You add it when using pytest-specific features: `pytest.approx` (float comparisons), `pytest.raises` (exception testing), `@pytest.fixture`, or marks like `@pytest.mark.parametrize`. These are introduced in the following topics.
01

Test the add Function

#

Write a test function `test_add` that calls `add(a, b)` with at least three different input pairs and asserts the correct result for each.

# math_utils.py
def add(a, b):
    return a + b


# test_math_utils.py
from math_utils import add

def test_add():
    # assert at least three input pairs
    pass
Solution
# math_utils.py
def add(a, b):
    return a + b


# test_math_utils.py
from math_utils import add

def test_add():
    assert add(2, 3) == 5
    assert add(0, 0) == 0
    assert add(-1, 1) == 0
    assert add(100, -50) == 50
02

Test is_even

#

Write test functions for `is_even(n)` that verify it returns `True` for even numbers and `False` for odd numbers. Write at least two test functions — one for even inputs, one for odd.

def is_even(n):
    return n % 2 == 0


def test_even_numbers():
    pass

def test_odd_numbers():
    pass
Solution
def is_even(n):
    return n % 2 == 0


def test_even_numbers():
    assert is_even(0) is True
    assert is_even(2) is True
    assert is_even(100) is True

def test_odd_numbers():
    assert is_even(1) is False
    assert is_even(-3) is False
    assert is_even(99) is False
03

Read Failing Test Output

#

The test below contains a deliberate bug — the expected value is wrong. Run pytest and read the failure output, then fix the expected value to make the test pass.

def multiply(a, b):
    return a * b


def test_multiply():
    assert multiply(3, 4) == 11   # wrong expected value
    assert multiply(0, 99) == 0
    assert multiply(-2, 5) == -10
Solution
def multiply(a, b):
    return a * b


def test_multiply():
    assert multiply(3, 4) == 12   # fixed: 3 * 4 = 12
    assert multiply(0, 99) == 0
    assert multiply(-2, 5) == -10
04

Test a clamp Function

#

Write three separate test functions for `clamp(value, lo, hi)`: one for values within range, one for values below the lower bound, and one for values above the upper bound.

def clamp(value, lo, hi):
    return max(lo, min(value, hi))


def test_within_range():
    pass

def test_below_lower_bound():
    pass

def test_above_upper_bound():
    pass
Solution
def clamp(value, lo, hi):
    return max(lo, min(value, hi))


def test_within_range():
    assert clamp(5, 0, 10) == 5
    assert clamp(0, 0, 10) == 0
    assert clamp(10, 0, 10) == 10

def test_below_lower_bound():
    assert clamp(-5, 0, 10) == 0
    assert clamp(-100, 0, 10) == 0

def test_above_upper_bound():
    assert clamp(15, 0, 10) == 10
    assert clamp(1000, 0, 10) == 10
05

Test the greet Function

#

Write tests for `greet(name)` that returns `'Hello, {name}!'`. Verify that the return value starts with 'Hello', ends with '!', and contains the provided name.

def greet(name):
    return f'Hello, {name}!'


def test_greet_alice():
    pass

def test_greet_empty_name():
    pass
Solution
def greet(name):
    return f'Hello, {name}!'


def test_greet_alice():
    result = greet('Alice')
    assert result == 'Hello, Alice!'
    assert result.startswith('Hello')
    assert result.endswith('!')
    assert 'Alice' in result

def test_greet_empty_name():
    result = greet('')
    assert result == 'Hello, !'