Python unit testing with pytest as the primary framework - fixtures (`@pytest.fixture` scopes, `conftest.py`), `@pytest.mark.parametrize` table-driven tests, markers (`skip` / `xfail` / custom with `--strict-markers`), `pyproject.toml` config, mocking via pytest-mock, coverage gating with pytest-cov (`--cov-fail-under`), parallel runs with pytest-xdist, and CI wiring - plus stdlib `unittest` (TestCase, unittest.mock, discovery) and `doctest` (docstring examples, directives) as references. Includes framework choice (pytest for new code; match an existing unittest convention; doctest only for documented examples) and test-authoring conventions (framework detection from pyproject.toml/setup.cfg/tox.ini, layout matching, no fabricated attributes). Use for any Python unit-test task: setting up pytest, writing fixtures or parametrized tests, mocking, gating coverage, wiring CI, or maintaining unittest/doctest suites. For async tests, see pytest-asyncio-patterns.
72
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Companion reference for python-unit-tests. Consult when constrained to
stdlib-only (no pip install), maintaining a legacy unittest codebase, or
using unittest.mock patterns from pytest test bodies.
Per docs.python.org/3/library/unittest.html:
unittest is Python's stdlib testing framework, modeled on JUnit (xUnit
family): no pip install required, class-based tests as TestCase methods,
and unittest.mock bundled - the canonical Python mocking library even in
pytest projects.
# test_sum.py
import unittest
def sum(a, b):
return a + b
class TestSum(unittest.TestCase):
def test_adds_1_and_2(self):
self.assertEqual(sum(1, 2), 3)
if __name__ == '__main__':
unittest.main()Run python -m unittest test_sum.py. A passing run ends with OK after a
Ran N tests summary; FAILED (failures=N) prints the AssertionError
diff.
setUpClass / tearDownClass (classmethods, once per class) and setUp /
tearDown (per test). No fixture-scope concept beyond these two levels.
Per ut-docs - assert with the method specific to the check,
never assertTrue(x == y) (the specific method prints a useful diff on
failure):
| Method | Use |
|---|---|
assertEqual(a, b) / assertNotEqual(a, b) | Equality |
assertTrue(x) / assertFalse(x) | Boolean |
assertIs(a, b) / assertIsNot(a, b) | Identity (is) |
assertIsNone(x) / assertIsNotNone(x) | None |
assertIn(a, b) / assertNotIn(a, b) | Membership |
assertIsInstance(a, type) | Type check |
assertRaises(Exception) | Sync raise (context manager + decorator forms) |
assertRaisesRegex(Exception, regex) | Raise + message match |
assertWarns(Warning) | Warning emission |
assertAlmostEqual(a, b, places=N) | Float comparison |
assertGreater(a, b) / assertGreaterEqual(a, b) | Numeric |
assertCountEqual(a, b) | Same elements regardless of order |
unittest.mock patternsPer docs.python.org/3/library/unittest.mock.html:
from unittest.mock import Mock, MagicMock, patch
# Standalone mocks
m = Mock()
m.method.return_value = 42
result = m.method(5)
m.method.assert_called_once_with(5)
# MagicMock supports magic methods (__len__, __iter__, etc.)
mm = MagicMock()
mm.__len__.return_value = 5
assert len(mm) == 5
# Patch a function in the target module
@patch('mymodule.fetch_user')
def test_with_patched_fetch(mock_fetch):
mock_fetch.return_value = {'id': 1}
...
# Context-manager form
with patch('mymodule.fetch_user') as mock_fetch:
mock_fetch.return_value = {'id': 1}
...
# Patch an attribute / a dictionary
@patch.object(SomeClass, 'method', return_value='mocked')
@patch.dict('os.environ', {'API_KEY': 'test-key'})Patch target rule: patch where the function is used, not where it's
defined. If mymodule.py does from api import fetch_user, patch
mymodule.fetch_user, not api.fetch_user.
Worked example - greeting.py builds a welcome string from a user fetched
over HTTP:
# tests/test_greeting.py
import unittest
from unittest.mock import patch
from greeting import welcome
class TestWelcome(unittest.TestCase):
@patch('greeting.fetch_user')
def test_welcome_names_user(self, mock_fetch):
mock_fetch.return_value = {'name': 'Ada'}
self.assertEqual(welcome(1), 'Hi Ada')
mock_fetch.assert_called_once_with(1)def test_addition_cases(self):
cases = [(1, 2, 3), (0, 0, 0), (-1, 1, 0)]
for a, b, expected in cases:
with self.subTest(a=a, b=b):
self.assertEqual(sum(a, b), expected)subTest reports each iteration as a separate failure; without it the loop
stops at the first failure.
@unittest.skip(reason), @unittest.skipIf(cond, reason), and
@unittest.expectedFailure (the test passes because it is expected to
fail).
python -m unittest discover # from cwd
python -m unittest discover -s tests/ -p 'test_*.py'
python -m unittest tests.test_user.TestUser.test_creation
# CI with coverage:
coverage run -m unittest discover && coverage report --fail-under=80pytest runs unittest.TestCase classes natively: keep TestCase classes,
write new tests as pytest functions, convert gradually. unittest.mock
works in either style.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
assertTrue(x == y) | Generic boolean; loses diff on failure | Specific assert method |
| Patch where defined, not where used | Patch silently doesn't apply | Patch where USED |
Loop over cases without subTest | First failure stops the loop | subTest |
Missing if __name__ == '__main__': unittest.main() | Can't run via python test.py | Always include |
subTest.unittest.IsolatedAsyncioTestCase (Python 3.8+);
less polished than pytest-asyncio.