Python Tutorial menu

Python Testing With pytest

A test is a small piece of code that checks your real code gives the right answer, and pytest runs all your tests with one command.

Why Write Tests?

Every time you change a program, something old might break. Checking everything by hand is slow and easy to forget. Automatic tests check it for you in seconds.

  • You find bugs before your users do
  • You can change and improve code without fear
  • Tests show other developers how your functions are meant to work
  • Many freelance clients and companies expect tested code

Install pytest

Inside your virtual environment:

Example

bash
pip install pytest

The Code to Test

Here is a small module for a shop. Save it as shop.py:

Example

python
# shop.py
def price_with_tax(price, rate=0.18):
    if price < 0:
        raise ValueError("price cannot be negative")
    return round(price * (1 + rate))


def apply_discount(total, percent):
    return total - total * percent / 100

Your First Tests

Create test_shop.py in the same folder. Each test is a normal function whose name starts with test_. Inside it, use assert to state what must be true.

Example

python
# test_shop.py
import pytest
from shop import price_with_tax, apply_discount


def test_tax_is_added():
    assert price_with_tax(1000) == 1180


def test_custom_rate():
    assert price_with_tax(1000, rate=0.05) == 1050


def test_discount():
    assert apply_discount(2000, 10) == 1800


def test_negative_price_is_rejected():
    with pytest.raises(ValueError):
        price_with_tax(-50)

pytest.raises(ValueError) checks that the code inside does raise that error. If it does not, the test fails.

Running the Tests

In the terminal, in the project folder, type:

Example

bash
pytest
Output
============================= test session starts ==============================
platform linux -- Python 3.13.16, pytest-9.1.1, pluggy-1.7.0
rootdir: /home/ali/shop
collected 4 items

test_shop.py ....                                                        [100%]

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

Each dot is a passing test. pytest found the tests by itself, because the file name starts with test_ and so do the function names.

Add -v (verbose) to see each test by name:

Example

bash
pytest -v
Output
test_shop.py::test_tax_is_added PASSED                                   [ 25%]
test_shop.py::test_custom_rate PASSED                                    [ 50%]
test_shop.py::test_discount PASSED                                       [ 75%]
test_shop.py::test_negative_price_is_rejected PASSED                     [100%]

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

Reading a Failure

Suppose we write a test with a wrong expected value. 20% off Rs 1500 is Rs 1200, but we wrote 1300:

Example

python
def test_discount_wrong():
    assert apply_discount(1500, 20) == 1300
Output
test_fail.py F                                                           [100%]

=================================== FAILURES ===================================
_____________________________ test_discount_wrong ______________________________

    def test_discount_wrong():
>       assert apply_discount(1500, 20) == 1300
E       assert 1200.0 == 1300
E        +  where 1200.0 = apply_discount(1500, 20)

test_fail.py:5: AssertionError
=========================== short test summary info ============================
FAILED test_fail.py::test_discount_wrong - assert 1200.0 == 1300
============================== 1 failed in 0.02s ===============================

F means failed. The > points at the failing line, and the E lines show the real value (1200.0) next to the expected one (1300). Here the test was wrong, not the code. Sometimes it is the other way round, and that is a bug you just caught.

Many Inputs With parametrize

Instead of copying a test for every value, give pytest a table of inputs and expected results.

Example

python
import pytest
from shop import price_with_tax


@pytest.mark.parametrize(
    "price, expected",
    [
        (100, 118),
        (250, 295),
        (0, 0),
        (999, 1179),
    ],
)
def test_many_prices(price, expected):
    assert price_with_tax(price) == expected

pytest runs the test once for each row:

Output
test_more.py::test_many_prices[100-118] PASSED                           [ 16%]
test_more.py::test_many_prices[250-295] PASSED                           [ 33%]
test_more.py::test_many_prices[0-0] PASSED                               [ 50%]
test_more.py::test_many_prices[999-1179] PASSED                          [ 66%]

Fixtures: Shared Test Data

A fixture prepares something that several tests need. Mark a function with @pytest.fixture, then use its name as a parameter in a test. pytest passes in a fresh copy every time.

Example

python
import pytest


@pytest.fixture
def cart():
    return {"tea": 60, "samosa": 40, "juice": 120}


def test_cart_total(cart):
    assert sum(cart.values()) == 220


def test_cart_has_tea(cart):
    assert "tea" in cart
Output
test_more.py::test_cart_total PASSED                                     [ 83%]
test_more.py::test_cart_has_tea PASSED                                   [100%]

Useful Commands

CommandWhat it does
pytestRuns every test in the folder
pytest -vShows each test by name
pytest test_shop.pyRuns one file
pytest -k discountRuns tests whose name contains "discount"
pytest -xStops at the first failure
pytest --lfRuns only the tests that failed last time

Write a test every time you fix a bug. The test proves the bug is gone and makes sure it never quietly comes back.

Exercise

Exercise

How does pytest know which functions are tests?

Learn Python with a teacher

Join the Python Programming course at Workownix, online or in Orangi Town, Karachi, and get a certificate.

See the course Ask on WhatsApp