← Back to Python series
🚀
Advanced
pytest · Fixtures · parametrize · Mock · Coverage

Week 6 — Testing with pytest

Write professional tests with pytest. Use fixtures for setup/teardown, parametrize for data-driven tests, mock external dependencies, and measure code coverage.

pytestfixtureparametrizemockcoverageTDD
Duration
2.5 hours
Level
📊 Advanced
Prerequisite
🎯 Intermediate Week 5
OUTCOME
Write a full test suite for a BankAccount class with 90%+ coverage

What you'll learn

  • 1Write test functions and classes with pytest
  • 2Use fixtures for reusable setup and teardown
  • 3Parametrize tests to cover multiple inputs
  • 4Mock external dependencies with unittest.mock
  • 5Measure and improve code coverage

1. pytest Basics

python
# test_calculator.py
import pytest

def add(a, b): return a + b
def divide(a, b):
    if b == 0: raise ZeroDivisionError
    return a / b

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

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

# Run: pytest test_calculator.py -v

2. Fixtures

python
import pytest
from bank import BankAccount

@pytest.fixture
def account():
    """Fresh account with $100 balance."""
    acct = BankAccount("test")
    acct.deposit(100)
    return acct

def test_deposit(account):
    account.deposit(50)
    assert account.balance == 150

def test_withdraw(account):
    account.withdraw(30)
    assert account.balance == 70

def test_overdraft(account):
    with pytest.raises(ValueError, match="Insufficient"):
        account.withdraw(200)

3. Parametrize & Mock

python
import pytest
from unittest.mock import patch, MagicMock

@pytest.mark.parametrize("a,b,expected", [
    (2, 3, 5), (-1, 1, 0), (0, 0, 0), (100, -50, 50)
])
def test_add_param(a, b, expected):
    assert add(a, b) == expected

def test_with_mock():
    with patch("requests.get") as mock_get:
        mock_get.return_value = MagicMock(status_code=200, json=lambda: {"ok": True})
        # code that calls requests.get will use the mock
        resp = requests.get("http://example.com")
        assert resp.status_code == 200

💻 Examples

Run these examples and check the output yourself.

test_bank.pyFull BankAccount test suite
CODE
import pytest

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance

    @property
    def balance(self): return self._balance

    def deposit(self, amount):
        if amount <= 0: raise ValueError("Must be positive")
        self._balance += amount

    def withdraw(self, amount):
        if amount > self._balance: raise ValueError("Insufficient funds")
        self._balance -= amount


@pytest.fixture
def empty_acct(): return BankAccount("Test")

@pytest.fixture
def funded_acct(): a = BankAccount("Test"); a.deposit(1000); return a

def test_initial_balance(empty_acct):
    assert empty_acct.balance == 0

@pytest.mark.parametrize("amount", [1, 100, 99999])
def test_deposit_valid(empty_acct, amount):
    empty_acct.deposit(amount)
    assert empty_acct.balance == amount

def test_deposit_invalid(empty_acct):
    with pytest.raises(ValueError):
        empty_acct.deposit(-10)

def test_withdraw_valid(funded_acct):
    funded_acct.withdraw(300)
    assert funded_acct.balance == 700

def test_withdraw_overdraft(funded_acct):
    with pytest.raises(ValueError, match="Insufficient"):
        funded_acct.withdraw(5000)

📝 Exercises

Try them yourself first, then open the solution to compare.

Exercise 1

TDD: Stack implementation

Goal: Write tests first, then implement a Stack class to make them pass.

Requirements
  • Write test_stack.py with 8+ test cases
  • Cover: push, pop, peek, is_empty, overflow (max_size)
  • Achieve 100% coverage: pytest --cov=stack test_stack.py
Toggle solution
SOLUTION
# test_stack.py
import pytest
from stack import Stack, StackOverflowError

@pytest.fixture
def s(): return Stack(max_size=3)

def test_empty(s): assert s.is_empty()
def test_push_pop(s): s.push(1); assert s.pop() == 1
def test_peek(s): s.push(99); assert s.peek() == 99; assert not s.is_empty()
def test_pop_empty(s): 
    with pytest.raises(IndexError): s.pop()
def test_overflow(s):
    for i in range(3): s.push(i)
    with pytest.raises(StackOverflowError): s.push(999)

# stack.py
class StackOverflowError(Exception): pass

class Stack:
    def __init__(self, max_size=None):
        self._data, self.max_size = [], max_size
    def push(self, v):
        if self.max_size and len(self._data) >= self.max_size:
            raise StackOverflowError
        self._data.append(v)
    def pop(self):
        if not self._data: raise IndexError
        return self._data.pop()
    def peek(self):
        if not self._data: raise IndexError
        return self._data[-1]
    def is_empty(self): return not self._data
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗