Summary
Pytest is Python's most powerful testing framework, featuring simple syntax, powerful fixtures, and extensive plugin ecosystem. Master fixtures, parametrization, markers, mocking with monkeypatch, and configuration to write clean, maintainable test suites that leverage pytest's automatic test discovery and detailed assertion introspection.
1. Installation & Basic Setup
pip install pytest
pip install pytest-cov # For coverage
pip install pytest-mock # For mocking
2. Test Discovery & Naming Conventions
Pytest automatically discovers tests following these patterns:
- Test files:
test_*.pyor*_test.py - Test classes:
Test*(without__init__method) - Test functions:
test_*
# test_example.py
def test_addition():
assert 1 + 1 == 2
class TestMath:
def test_multiplication(self):
assert 2 * 3 == 6
3. Running Tests
pytest # Run all tests
pytest test_file.py # Run specific file
pytest test_file.py::test_func # Run specific test
pytest -v # Verbose output
pytest -s # Show print statements
pytest -k "keyword" # Run tests matching keyword
pytest -x # Stop on first failure
pytest --maxfail=2 # Stop after 2 failures
4. Assertions
# Basic assertions
assert value == expected
assert value != expected
assert value > threshold
assert value in collection
assert value is None
assert value is not None
# Exception testing
import pytest
def test_exception():
with pytest.raises(ValueError):
raise ValueError("Error message")
# Check exception message
with pytest.raises(ValueError, match="Error.*"):
raise ValueError("Error message")
# Approximate assertions
assert pytest.approx(0.1 + 0.2) == 0.3
assert [0.1, 0.2] == pytest.approx([0.1, 0.2])
5. Fixtures
Basic Fixtures
import pytest
@pytest.fixture
def sample_data():
return {"name": "John", "age": 30}
def test_with_fixture(sample_data):
assert sample_data["name"] == "John"
# Fixture with teardown
@pytest.fixture
def database():
# Setup
db = Database()
db.connect()
yield db # This is where the test runs
# Teardown
db.disconnect()
Fixture Scopes
@pytest.fixture(scope="function") # Default - per test
def func_fixture():
return "data"
@pytest.fixture(scope="class") # Per test class
def class_fixture():
return "shared_class_data"
@pytest.fixture(scope="module") # Per module
def module_fixture():
return "shared_module_data"
@pytest.fixture(scope="session") # Once per session
def session_fixture():
return "shared_session_data"
Built-in Fixtures
def test_tmp_path(tmp_path):
# tmp_path is a pathlib.Path object
file = tmp_path / "test.txt"
file.write_text("content")
assert file.read_text() == "content"
def test_capsys(capsys):
print("Hello")
captured = capsys.readouterr()
assert captured.out == "Hello\n"
def test_monkeypatch(monkeypatch):
monkeypatch.setenv("USER", "testuser")
assert os.environ["USER"] == "testuser"
6. Parametrization
# Single parameter
@pytest.mark.parametrize("input,expected", [
(2, 4),
(3, 9),
(4, 16)
])
def test_square(input, expected):
assert input ** 2 == expected
# Multiple parameters
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(5, 5, 10),
(-1, 1, 0)
])
def test_add(a, b, expected):
assert a + b == expected
# Parameter IDs for better test names
@pytest.mark.parametrize("value", [1, 2, 3], ids=["one", "two", "three"])
def test_values(value):
assert value > 0
7. Markers
# Built-in markers
@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
pass
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python 3.8+")
def test_new_feature():
pass
@pytest.mark.xfail(reason="Known bug")
def test_broken_feature():
assert False
# Custom markers (define in pytest.ini)
@pytest.mark.slow
def test_heavy_computation():
pass
@pytest.mark.integration
def test_api_call():
pass
# Run specific markers
# pytest -m slow
# pytest -m "not slow"
8. Mocking & Monkeypatching
Using unittest.mock
from unittest.mock import Mock, patch
def test_with_mock():
mock = Mock(return_value=42)
assert mock() == 42
mock.assert_called_once()
# Patching
@patch('module.function')
def test_with_patch(mock_func):
mock_func.return_value = "mocked"
result = module.function()
assert result == "mocked"
# Context manager
def test_patch_context():
with patch('os.path.exists') as mock_exists:
mock_exists.return_value = True
assert os.path.exists('/fake/path')
Monkeypatch
def test_monkeypatch_function(monkeypatch):
def mock_func():
return "mocked"
monkeypatch.setattr("module.function", mock_func)
assert module.function() == "mocked"
def test_monkeypatch_env(monkeypatch):
monkeypatch.setenv("API_KEY", "test_key")
assert os.environ["API_KEY"] == "test_key"
9. Configuration Files
pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --strict-markers
markers =
slow: marks tests as slow
integration: marks tests as integration tests
conftest.py
# conftest.py - Shared fixtures across tests
import pytest
@pytest.fixture(autouse=True)
def reset_database():
# Runs before each test automatically
yield
# Cleanup after each test
@pytest.fixture
def client():
return TestClient()
# Hooks
def pytest_collection_modifyitems(items):
"""Modify test collection"""
for item in items:
# Add markers based on test names
if "slow" in item.nodeid:
item.add_marker(pytest.mark.slow)
10. Advanced Patterns
Testing Classes
class TestUserService:
@pytest.fixture(autouse=True)
def setup(self):
self.service = UserService()
def test_create_user(self):
user = self.service.create("John")
assert user.name == "John"
def test_delete_user(self):
user = self.service.create("Jane")
self.service.delete(user.id)
assert self.service.get(user.id) is None
Async Testing
import pytest
import asyncio
@pytest.mark.asyncio
async def test_async_function():
result = await async_function()
assert result == expected
# Install: pip install pytest-asyncio
Test Doubles Pattern
class FakeRepository:
def __init__(self):
self.data = {}
def save(self, id, value):
self.data[id] = value
def get(self, id):
return self.data.get(id)
def test_service_with_fake():
fake_repo = FakeRepository()
service = Service(fake_repo)
service.process("data")
assert fake_repo.get(1) == "processed_data"
11. Coverage
# Run with coverage
pytest --cov=myproject tests/
pytest --cov=myproject --cov-report=html
pytest --cov=myproject --cov-report=term-missing
# Coverage configuration in .coveragerc
[run]
source = myproject
omit = */tests/*,*/venv/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
12. Best Practices & Interview Tips
Test Organization
# Arrange-Act-Assert pattern
def test_user_creation():
# Arrange
user_data = {"name": "John", "email": "john@example.com"}
# Act
user = create_user(user_data)
# Assert
assert user.name == "John"
assert user.email == "john@example.com"
Critical Interview Topics
Test Isolation Principles
# Bad
class TestCounter:
counter = 0
def test_increment(self):
self.counter += 1
assert self.counter == 1 # Fails if run after other tests
# Good
class TestCounter:
def test_increment(self):
counter = Counter()
counter.increment()
assert counter.value == 1
- Testing Edge Cases
@pytest.mark.parametrize("input,expected", [
([], 0), # Empty list
([1], 1), # Single element
([1, 2, 3], 6), # Normal case
([-1, -2, -3], -6), # Negative numbers
([1.5, 2.5], 4.0), # Floats
])
def test_sum_function(input, expected):
assert sum(input) == expected
- Testing Exceptions
def test_division_by_zero():
with pytest.raises(ZeroDivisionError) as exc_info:
1 / 0
assert "division by zero" in str(exc_info.value)
- Mocking External Dependencies
def test_api_call(mocker):
# Using pytest-mock
mock_response = mocker.Mock()
mock_response.json.return_value = {"status": "success"}
mocker.patch("requests.get", return_value=mock_response)
result = make_api_call()
assert result["status"] == "success"
13. Quick Reference Commands
# Discovery
pytest --collect-only # Show what tests would run
# Output
pytest -v # Verbose
pytest -q # Quiet
pytest --tb=short # Short traceback
pytest --tb=no # No traceback
# Selection
pytest -k "test_method" # Keyword expression
pytest -m "not slow" # Marker expression
# Debugging
pytest --pdb # Drop to debugger on failures
pytest --lf # Run last failed
pytest --ff # Run failed first
# Parallel execution (requires pytest-xdist)
pytest -n 4 # Run on 4 CPUs
14. Common Gotchas
- Mutable Default Arguments in Fixtures
# Bad
@pytest.fixture
def data():
return [] # Same list for all tests!
# Good
@pytest.fixture
def data():
return [] # New list for each test
- Import Errors
# Ensure test files can import your module
# Add __init__.py to test directories
# Or use PYTHONPATH: export PYTHONPATH=.
- Fixture Dependency
@pytest.fixture
def user(database): # Depends on database fixture
return database.create_user("test")
def test_user_exists(user, database):
assert database.get_user(user.id) is not None
Interview Success Strategies
Pytest vs Unittest Comparison
- Pytest: Simple assert statements, powerful fixtures, better output
- Unittest: Class-based, more verbose, standard library
- Migration: Pytest can run unittest tests
Fixture Expertise
- Scopes: function (default), class, module, session
- Autouse: Automatic fixture application
- Yield Fixtures: Setup and teardown in one function
- Fixture Dependencies: Fixtures can depend on other fixtures
Mocking Strategies
- Monkeypatch: Built-in pytest fixture for patching
- Mock/patch: unittest.mock integration
- pytest-mock: Enhanced mocking plugin
- Fake Objects: Test doubles for complex dependencies
Advanced Testing Patterns
- Async Testing: pytest-asyncio for async/await
- Parallel Execution: pytest-xdist for speed
- Property Testing: hypothesis integration
- Benchmark Testing: pytest-benchmark for performance
Best Practices to Demonstrate
- AAA Pattern: Arrange-Act-Assert structure
- Descriptive Names: test_should_return_error_when_invalid_input
- Parametrization: Cover edge cases efficiently
- Markers: Organize tests with custom markers
- Coverage Goals: Aim for 80%+ with meaningful tests
Key Points to Remember
- Pytest's assert introspection provides detailed failure messages
- Fixtures promote reusability and reduce boilerplate
- Parametrization eliminates test duplication
- conftest.py shares fixtures across test modules
- pytest.ini configures project-wide settings
Found this useful? Pass it on.