On this page
article
Python Testing and Quality Assurance
Write reliable Python code with unittest, pytest, fixtures, mocking, coverage, and code quality tools like flake8 and black.
Testing gives you confidence that code works and keeps working as you change it. Quality tools enforce consistency across teams.
unittest — Built-In Framework
# math_utils.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# test_math_utils.py
import unittest
from math_utils import add, divide
class TestMathUtils(unittest.TestCase):
def test_add_positive(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative(self):
self.assertEqual(add(-1, 1), 0)
def test_divide(self):
self.assertAlmostEqual(divide(10, 3), 3.333, places=2)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(10, 0)
def setUp(self):
self.data = [1, 2, 3, 4, 5]
def test_data_length(self):
self.assertEqual(len(self.data), 5)
if __name__ == "__main__":
unittest.main()
Run: python -m unittest test_math_utils.py -v
pytest — Modern Testing
pip install pytest
# test_math.py
import pytest
from math_utils import add, divide
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
def test_divide():
assert divide(10, 2) == 5
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
Run: pytest test_math.py -v
Fixtures
@pytest.fixture
def sample_data():
return {"users": ["Alice", "Bob"], "count": 2}
def test_user_count(sample_data):
assert sample_data["count"] == len(sample_data["users"])
@pytest.fixture
def db_connection():
conn = create_test_database()
yield conn
conn.close()
Parametrize
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_add_parametrized(a, b, expected):
assert add(a, b) == expected
Mocking
Replace dependencies during tests:
from unittest.mock import patch, MagicMock
@patch("myapp.api.requests.get")
def test_fetch_data(mock_get):
mock_get.return_value = MagicMock(
status_code=200,
json=lambda: {"data": "test"},
)
result = fetch_data("https://api.example.com")
assert result == {"data": "test"}
mock_get.assert_called_once()
Test Coverage
pip install pytest-cov
pytest --cov=myapp --cov-report=html tests/
Aim for high coverage on critical paths — 100% is not always necessary, but untested error handling often causes production bugs.
Code Quality Tools
Linting with flake8
pip install flake8
flake8 src/ --max-line-length=88
Formatting with black
pip install black
black src/
Import sorting with isort
pip install isort
isort src/
Type checking with mypy
pip install mypy
mypy src/
Pre-commit Hooks
Automate checks before every commit:
pip install pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 24.3.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
pre-commit install
Testing Best Practices
- Test behavior, not implementation — tests should survive refactors
- One assertion concept per test — clear failure messages
- Use descriptive test names —
test_divide_by_zero_raises_error - Keep tests fast — mock external services
- Run tests in CI — every pull request
- Test edge cases — empty inputs, None, boundary values
Project Test Structure
myapp/
├── src/myapp/
│ ├── models.py
│ └── services.py
└── tests/
├── conftest.py # shared fixtures
├── test_models.py
└── test_services.py
Testing is not optional in professional Python development — it’s how you move fast without breaking things.
For production-scale patterns, see Advanced Testing.