unittest-tests
Configures and runs Python's stdlib unittest - TestCase + setUp/tearDown lifecycle hooks, assertion catalog (assertEqual / assertRaises / assertIn / assertAlmostEqual), unittest.mock module (Mock / MagicMock / patch / patch.object / patch.dict), test discovery via `python -m unittest discover`, subTest for parametrized cases, expectedFailure decorator. Use when constrained to stdlib-only (no pip install) or migrating legacy unittest codebases.
Install with skills.sh (any agent)
npx skills add testland/qa --skill unittest-testsunittest-tests
Overview
Per docs.python.org/3/library/unittest.html (opens in new window):
unittest is Python's stdlib testing framework, modeled on JUnit (xUnit family). Distinguishing properties:
For new projects, pytest-tests is generally preferred. unittest persists in stdlib-only contexts + legacy maintenance.
When to use
Step 1 - First test
Per ut-docs (opens in new window):
# 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)
def test_adds_negative(self):
self.assertEqual(sum(-1, 1), 0)
if __name__ == '__main__':
unittest.main()Run:
python -m unittest test_sum.py # run this file (discovery invocations: Step 7)Verify: a passing run ends with OK after a Ran N tests summary line. If it prints FAILED (failures=N), read the AssertionError diff for expected-vs-actual, fix the code or the assertion, and re-run until you get OK before adding more tests.
Step 2 - TestCase lifecycle hooks
class TestUserService(unittest.TestCase):
@classmethod
def setUpClass(cls):
# runs once before all tests in class
cls.db = create_test_db()
@classmethod
def tearDownClass(cls):
cls.db.close()
def setUp(self):
# runs before each test
self.user = create_user(self.db)
def tearDown(self):
# runs after each test
self.db.rollback()
def test_user_creation(self):
self.assertEqual(self.user.id, 1)Step 3 - Assertions
Assert with the method specific to the check (assertEqual, assertIn, assertRaises, assertAlmostEqual, ...), never assertTrue(x == y). Full catalog + the diff-on-failure rationale: references/assertions-and-mock.md.
Step 4 - unittest.mock patterns
Patch where the name is used, not where it's defined. The full rule (with a worked import example) plus Mock / MagicMock / patch / patch.object / patch.dict patterns are in references/assertions-and-mock.md; the Worked example below runs the patch-where-used pattern end to end.
Step 5 - subTest for parametrization
class TestCalculator(unittest.TestCase):
def test_addition_cases(self):
cases = [(1, 2, 3), (0, 0, 0), (-1, 1, 0), (100, 200, 300)]
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 if it fails - without it, the loop stops at the first failure.
Step 6 - Skip + expected failure
@unittest.skip("Requires staging DB")
def test_skipped():
pass
@unittest.skipIf(sys.version_info < (3, 11), "Python 3.11+ syntax")
def test_modern():
pass
@unittest.expectedFailure
def test_known_bug():
self.assertEqual(1, 2) # passes the test (because it's expected to fail)Step 7 - Test discovery
# From cwd
python -m unittest discover
# From specific dir + pattern
python -m unittest discover -s tests/ -p 'test_*.py'
# Verbose
python -m unittest discover -v
# Specific test
python -m unittest tests.test_user.TestUser.test_creationStep 8 - pytest interop
pytest runs unittest.TestCase classes natively. Migration path:
Step 9 - CI integration
- run: pip install -e .[dev]
- run: python -m unittest discover -s tests/ -v
# Or with coverage:
- run: coverage run -m unittest discover && coverage report --fail-under=80Worked example
greeting.py builds a welcome string from a user fetched over HTTP:
# greeting.py
from api import fetch_user
def welcome(user_id):
user = fetch_user(user_id)
return f"Hi {user['name']}"Test it without hitting the network - patch fetch_user where greeting uses it (not where api defines it), then assert on the formatted result:
# 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)
if __name__ == '__main__':
unittest.main()Run python -m unittest discover -s tests/ -v. The test passes: the patch replaced the real fetch_user, assertEqual confirmed the greeting string, and assert_called_once_with confirmed the id was forwarded once.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
assertTrue(x == y) instead of assertEqual(x, y) | Generic boolean; loses diff in failure | Use specific assert (Step 3) |
| Patch where defined, not where used | Patch silently doesn't apply | Patch where USED (Step 4 rule) |
Loop over cases without subTest | First failure stops the loop | Use subTest (Step 5) |
Use setUp to populate self.* attributes that overlap test names | Confusion about what's fixture vs result | Distinct naming |
Forget if __name__ == '__main__': unittest.main() for direct-run | Can't run via python test.py | Always include (Step 1) |
Limitations
References
unittest assertions and mock reference
View source (opens in new window)unittest assertions and mock reference
Companion catalog for the unittest-tests skill: the full assertion table and the unittest.mock patterns, pulled out of the SKILL spine.
Assertion catalog
Per ut-docs (opens in new window):
| 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 |
assertDictContainsSubset(subset, dict) | Partial dict match (deprecated; use <= operator) |
Prefer the specific assert over assertTrue(x == y): the specific method prints a useful diff on failure.
unittest.mock patterns
Per docs.python.org/3/library/unittest.mock.html (opens in new window):
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}
result = my_function()
assert result == 'expected'
# Context-manager form
def test_with_context_patch():
with patch('mymodule.fetch_user') as mock_fetch:
mock_fetch.return_value = {'id': 1}
result = my_function()
# Patch an attribute
@patch.object(SomeClass, 'method', return_value='mocked')
def test_class_method(mock_method):
obj = SomeClass()
assert obj.method() == 'mocked'
# Patch a dictionary
@patch.dict('os.environ', {'API_KEY': 'test-key'})
def test_with_env():
assert 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.
Related skills
doctest-tests
Configures and runs Python's stdlib doctest - embeds executable test cases in docstrings using `>>>` Python interactive prompt convention; supports `# doctest: +ELLIPSIS` / `+NORMALIZE_WHITESPACE` / `+SKIP` directives; integrates with pytest via `--doctest-modules` flag; runs as `python -m doctest module.py -v`. Use for self-documenting reference implementations + simple smoke-test coverage embedded in API docs.
nose2-tests
Configures and runs nose2 - successor to nose (the original Python test discovery library, end-of-life 2015) and an alternative to pytest's discovery model; supports plugin architecture, layers (per-test-class setUp/tearDown shared across modules), parameterized tests via `nose2.tools.params`, multi-process parallelism via mp plugin. Use when migrating from legacy nose1 codebases or when the team prefers nose2's plugin model over pytest.
pytest-asyncio-patterns
Configures and runs async Python tests with pytest-asyncio: installs the plugin, selects asyncio_mode (auto vs strict), scopes event loops (function/class/module/session), writes async fixtures with @pytest_asyncio.fixture, mocks coroutines with AsyncMock, and tests FastAPI (httpx.AsyncClient + ASGITransport) and aiohttp (aiohttp_client fixture) applications. Use when a Python project contains async def test_ functions, FastAPI/aiohttp endpoints, or any asyncio-based code that needs pytest integration. Do NOT use for general pytest fixture design, parametrize patterns, or conftest.py structure without an asyncio-specific problem (event-loop scoping, mode config, AsyncMock, ASGI client): use pytest-tests for those.
pytest-tests
Configures and runs pytest - the de facto Python test framework with fixture-based dependency injection (`@pytest.fixture` with scopes module/session/function), parametrize for table-driven tests (`@pytest.mark.parametrize`), markers (`@pytest.mark.skip` / `xfail` / `slow`), `conftest.py` for shared fixtures, plugin ecosystem (pytest-cov, pytest-asyncio, pytest-mock, pytest-xdist), `--lf`/`--ff` for fail-loop, coverage gating. Use when working with Python and needing the modern test framework.