Difficulty: Intermediate
How do you write tests in Python? Explain pytest and testing best practices.
Python has unittest (built-in) and pytest (third-party, preferred by most).
pytest advantages over unittest: - Simple assert statements (no self.assertEqual) - Powerful fixtures for setup/teardown - Parametrize for running tests with multiple inputs - Rich plugin ecosystem - Better output and error messages
Testing best practices: - Test one thing per test function - Use descriptive test names - Arrange-Act-Assert pattern - Mock external dependencies - Aim for meaningful coverage, not 100%
# test_calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# Simple assertions
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
def test_divide():
assert divide(10, 2) == 5.0
assert divide(7, 2) == 3.5
# Testing exceptions
import pytest
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
# Approximate comparisons (for floats)
def test_float_comparison():
assert divide(1, 3) == pytest.approx(0.333, rel=1e-2)
# Run with: pytest test_calculator.py -v
# Output:
# test_calculator.py::test_add PASSED
# test_calculator.py::test_divide PASSED
# test_calculator.py::test_divide_by_zero PASSED
# test_calculator.py::test_float_comparison PASSED
pytest uses plain assert statements with detailed failure messages. pytest.raises() tests that exceptions are raised. pytest.approx() handles floating point comparison.
import pytest
# Fixtures: setup and teardown
@pytest.fixture
def sample_users():
"""Provides test data for user tests."""
return [
{'name': 'Alice', 'age': 30},
{'name': 'Bob', 'age': 25},
{'name': 'Charlie', 'age': 35},
]
def test_user_count(sample_users):
assert len(sample_users) == 3
def test_youngest_user(sample_users):
youngest = min(sample_users, key=lambda u: u['age'])
assert youngest['name'] == 'Bob'
# Parametrize: run test with multiple inputs
@pytest.mark.parametrize('input,expected', [
('hello', 'HELLO'),
('world', 'WORLD'),
('Python', 'PYTHON'),
('', ''),
])
def test_uppercase(input, expected):
assert input.upper() == expected
# Parametrize with IDs for better output
@pytest.mark.parametrize('a,b,result', [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
(100, 200, 300),
], ids=['positive', 'negative', 'zeros', 'large'])
def test_add_params(a, b, result):
assert add(a, b) == result
# Fixture with teardown
@pytest.fixture
def temp_file(tmp_path):
filepath = tmp_path / 'test.txt'
filepath.write_text('test data')
yield filepath # Test runs here
# Cleanup happens automatically (tmp_path handles it)
Fixtures provide reusable test data and setup/teardown. @parametrize runs the same test with different inputs, reducing code duplication.
from unittest.mock import patch, MagicMock
import pytest
# Code under test
class UserService:
def __init__(self, api_client):
self.api = api_client
def get_user(self, user_id):
response = self.api.get(f'/users/{user_id}')
if response.status_code == 200:
return response.json()
return None
def create_user(self, name, email):
response = self.api.post('/users', json={'name': name, 'email': email})
return response.status_code == 201
# Test with MagicMock
def test_get_user_success():
mock_api = MagicMock()
mock_api.get.return_value.status_code = 200
mock_api.get.return_value.json.return_value = {'id': 1, 'name': 'Alice'}
service = UserService(mock_api)
user = service.get_user(1)
assert user == {'id': 1, 'name': 'Alice'}
mock_api.get.assert_called_once_with('/users/1')
def test_get_user_not_found():
mock_api = MagicMock()
mock_api.get.return_value.status_code = 404
service = UserService(mock_api)
user = service.get_user(999)
assert user is None
# Patch decorator for module-level mocking
@patch('builtins.open', create=True)
def test_read_config(mock_open):
mock_open.return_value.__enter__ = lambda s: s
mock_open.return_value.__exit__ = MagicMock(return_value=False)
mock_open.return_value.read.return_value = '{"key": "value"}'
# Test code that reads a file...
mock_open.assert_called_once()
MagicMock creates flexible mock objects. patch replaces real objects with mocks during tests. assert_called_once_with verifies the mock was called correctly.
pytest, unittest, Fixtures, Mocking, Parametrize, Coverage