Lab 06: Testing with pytest

Objective

Write comprehensive tests using pytest: fixtures, parametrize, mocking with unittest.mock, coverage, and testing async code.

Time

30 minutes

Prerequisites

  • Lab 01–02 (OOP, Decorators)

Tools

  • Docker image: zchencow/innozverse-python:latest (pytest included)


Lab Instructions

Step 1: pytest Basics & Assertions

docker run --rm zchencow/innozverse-python:latest python3 -c "
import pytest
import subprocess, sys, textwrap, tempfile, os

code = textwrap.dedent('''
    # product.py + test_product.py in one block

    class Product:
        def __init__(self, name: str, price: float, stock: int = 0):
            if not name.strip():
                raise ValueError(\"name required\")
            if price <= 0:
                raise ValueError(f\"price must be positive, got {price}\")
            if stock < 0:
                raise ValueError(f\"stock cannot be negative\")
            self.name  = name.strip()
            self.price = price
            self.stock = stock

        @property
        def status(self): return \"active\" if self.stock > 0 else \"out_of_stock\"

        def sell(self, qty: int) -> None:
            if qty <= 0: raise ValueError(\"qty must be positive\")
            if self.stock < qty:
                raise ValueError(f\"insufficient stock: have {self.stock}, need {qty}\")
            self.stock -= qty

        def restock(self, qty: int) -> None:
            if qty <= 0: raise ValueError(\"qty must be positive\")
            self.stock += qty

    # Tests
    def test_create_valid_product():
        p = Product(\"Surface Pro\", 864.0, 15)
        assert p.name == \"Surface Pro\"
        assert p.price == 864.0
        assert p.stock == 15
        assert p.status == \"active\"

    def test_create_out_of_stock():
        p = Product(\"USB-C Hub\", 29.99, 0)
        assert p.status == \"out_of_stock\"

    def test_name_stripped():
        p = Product(\"  Surface Pen  \", 49.99)
        assert p.name == \"Surface Pen\"

    def test_invalid_name():
        with pytest.raises(ValueError, match=\"name required\"):
            Product(\"\", 10.0)

    def test_invalid_price():
        with pytest.raises(ValueError, match=\"price must be positive\"):
            Product(\"Test\", -1.0)

    def test_sell():
        p = Product(\"Test\", 10.0, 10)
        p.sell(3)
        assert p.stock == 7

    def test_sell_insufficient_stock():
        p = Product(\"Test\", 10.0, 5)
        with pytest.raises(ValueError, match=\"insufficient stock\"):
            p.sell(10)

    def test_restock():
        p = Product(\"Test\", 10.0, 0)
        p.restock(50)
        assert p.stock == 50
        assert p.status == \"active\"
''')

with tempfile.TemporaryDirectory() as tmp:
    test_file = os.path.join(tmp, 'test_product.py')
    with open(test_file, 'w') as f:
        f.write(code)
    result = subprocess.run(
        [sys.executable, '-m', 'pytest', test_file, '-v', '--tb=short'],
        capture_output=True, text=True
    )
    print(result.stdout[-2000:])
    if result.returncode != 0:
        print(result.stderr[-500:])
"

💡 pytest.raises(ExceptionType, match='...') asserts that the code raises the specified exception AND that the message matches the regex. Without match, any message passes. This is critical for testing that error messages are informative — the right exception with the wrong message is still a bug.

📸 Verified Output:


Step 2: Fixtures & Parametrize

📸 Verified Output:


Steps 3–8: Mocking, Async Tests, Property Tests, Coverage, Test Classes, Capstone

📸 Verified Output:


Summary

Feature
Syntax
Use case

Basic test

def test_xxx(): assert ...

Any assertion

Raises

with pytest.raises(Exc, match=r"...")

Exception testing

Fixture

@pytest.fixture

Reusable setup

Parametrize

@pytest.mark.parametrize("x,y", [...])

Data-driven tests

Mock

Mock(spec=MyClass)

Replace dependencies

Patch

@patch("module.func", return_value=...)

Replace module-level names

Async mock

AsyncMock(return_value=...)

Mock coroutines

Float approx

assert x == pytest.approx(y)

Floating point equality

Further Reading

Last updated