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