Skip to content
beginner Phase 5 · Python Testing

pytest Fundamentals

Write unit tests with pytest — fixtures, assertions, and parametrize.

1h 15m
0 problems
Topic Progress 0%

pytest Basics

Installing and Running

# Install
pip install pytest

# Run tests
pytest                    # Run all tests
pytest test_file.py       # Run specific file
pytest -v                 # Verbose output
pytest -x                 # Stop on first failure
pytest -k 'test_name'     # Run tests matching pattern
pytest --tb=short         # Shorter traceback

Writing Tests

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

def test_add_positive():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-1, -1) == -2

def test_add_zero():
    assert add(0, 0) == 0

# pytest automatically finds functions starting with 'test_'

Assertions

def test_basic_assertions():
    assert 1 + 1 == 2                    # Equality
    assert 1 + 1 != 3                    # Inequality
    assert 10 > 5                        # Comparison
    assert 'hello' in 'hello world'      # Membership
    assert [1, 2, 3]                     # Truthy
    assert not []                        # Falsy

def test_with_message():
    result = add(2, 3)
    assert result == 5, f'Expected 5 but got {result}'

# pytest shows diff on failure
assert add(2, 3) == 6
# E       assert 5 == 6
# E        +  where 5 = add(2, 3)

Exception Testing

import pytest

def divide(a, b):
    if b == 0:
        raise ValueError('Cannot divide by zero')
    return a / b

def test_divide_by_zero():
    with pytest.raises(ValueError) as exc_info:
        divide(10, 0)
    assert str(exc_info.value) == 'Cannot divide by zero'

def test_divide_normal():
    assert divide(10, 2) == 5.0

Fixtures

Basic Fixtures

import pytest

# Fixtures provide test setup/teardown
@pytest.fixture
def sample_data():
    return {
        'users': ['Alice', 'Bob', 'Charlie'],
        'count': 3
    }

def test_sample_data(sample_data):
    assert len(sample_data['users']) == 3
    assert sample_data['count'] == 3

# Fixtures are automatically discovered

Fixture Scope

# scope='function' (default) - runs for each test
@pytest.fixture
def fresh_database():
    db = create_database()
    yield db
    db.cleanup()

# scope='class' - runs once per class
@pytest.fixture(scope='class')
def shared_resource():
    return create_expensive_resource()

# scope='module' - runs once per module
@pytest.fixture(scope='module')
def module_resource():
    return create_resource()

# scope='session' - runs once per test session
@pytest.fixture(scope='session')
def session_resource():
    return create_resource()

Setup and Teardown

@pytest.fixture
def database():
    # Setup
    db = create_connection()
    db.execute('CREATE TABLE users (id INT, name TEXT)')
    
    yield db  # Test runs here
    
    # Teardown
    db.execute('DROP TABLE users')
    db.close()

# Using yield fixture
def test_insert_user(database):
    database.execute('INSERT INTO users VALUES (1, "Alice")')
    result = database.execute('SELECT * FROM users')
    assert result == [(1, 'Alice')]

Fixture Composition

@pytest.fixture
def user_factory():
    def create_user(name='Test', age=25):
        return {'name': name, 'age': age}
    return create_user

@pytest.fixture
def alice(user_factory):
    return user_factory(name='Alice', age=30)

@pytest.fixture
def bob(user_factory):
    return user_factory(name='Bob', age=25)

def test_users(alice, bob):
    assert alice['name'] == 'Alice'
    assert bob['name'] == 'Bob'

conftest.py

# conftest.py - shared fixtures for entire directory
import pytest

@pytest.fixture
def shared_database():
    # Available to all tests in this directory
    return create_database()

# No need to import - pytest discovers it automatically

Parameterized Tests

@pytest.mark.parametrize

import pytest

def add(a, b):
    return a + b

# Basic parametrize
@pytest.mark.parametrize('a, b, expected', [
    (2, 3, 5),
    (-1, 1, 0),
    (0, 0, 0),
    (100, 200, 300)
])
def test_add(a, b, expected):
    assert add(a, b) == expected

# Equivalent to writing:
def test_add_2_3():
    assert add(2, 3) == 5
def test_add_neg():
    assert add(-1, 1) == 0
def test_add_zero():
    assert add(0, 0) == 0

Multiple Parameters

@pytest.mark.parametrize('a, b', [
    (1, 2),
    (3, 4),
    (5, 6)
])
@pytest.mark.parametrize('expected', [3, 7, 11])
def test_add_matrix(a, b, expected):
    assert add(a, b) == expected

# Creates cartesian product: 3 x 3 = 9 tests

Parametrize with IDs

@pytest.mark.parametrize('input,expected', [
    ('hello', 'HELLO'),
    ('world', 'WORLD'),
    ('', '')
], ids=['uppercase', 'another', 'empty'])
def test_to_upper(input, expected):
    assert input.upper() == expected

# Output shows IDs:
# test_uppercase PASSED
# test_another PASSED
# test_empty PASSED

Fixtures and Parametrize

@pytest.fixture(params=[1, 2, 3])
def number(request):
    return request.param

def test_is_positive(number):
    assert number > 0

# Runs 3 times, once for each param value

Skip and Xfail

import pytest
import sys

# Skip test
@pytest.mark.skip(reason='Not implemented yet')
def test_not_implemented():
    pass

# Skip if condition
@pytest.mark.skipif(sys.platform == 'win32', reason='Linux only')
def test_linux_only():
    pass

# Expected failure
@pytest.mark.xfail(reason='Known bug')
def test_known_bug():
    assert 1 + 1 == 3  # Expected to fail

# xfail with strict - fails if test passes
@pytest.mark.xfail(strict=True)
def test_should_fail():
    assert 1 + 1 == 3  # If this passes, test fails