Testland
Browse all skills & agents

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-tests
View source

unittest-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:

  • Stdlib: no pip install required - runs anywhere Python runs.
  • Class-based: tests as TestCase methods (vs pytest's function-style).
  • Mock module bundled: unittest.mock is the canonical Python mocking library (used even by pytest projects).

For new projects, pytest-tests is generally preferred. unittest persists in stdlib-only contexts + legacy maintenance.

When to use

  • Constrained to Python stdlib (no third-party deps).
  • Maintaining legacy unittest codebase.
  • Specifically using unittest.mock patterns even with pytest (pytest test bodies can use unittest.mock directly).
  • Embedded environments where pip install isn't possible.

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_creation

Step 8 - pytest interop

pytest runs unittest.TestCase classes natively. Migration path:

  1. Keep TestCase classes for now; pytest discovers + runs them.
  2. New tests as pytest functions.
  3. Gradually convert TestCase to functions over time.
  4. unittest.mock continues to work in either style.

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=80

Worked 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-patternWhy it failsFix
assertTrue(x == y) instead of assertEqual(x, y)Generic boolean; loses diff in failureUse specific assert (Step 3)
Patch where defined, not where usedPatch silently doesn't applyPatch where USED (Step 4 rule)
Loop over cases without subTestFirst failure stops the loopUse subTest (Step 5)
Use setUp to populate self.* attributes that overlap test namesConfusion about what's fixture vs resultDistinct naming
Forget if __name__ == '__main__': unittest.main() for direct-runCan't run via python test.pyAlways include (Step 1)

Limitations

  • Class-based syntax is verbose vs pytest function-style.
  • No built-in parametrize beyond subTest (per-case reporting limited).
  • Async testing requires unittest.IsolatedAsyncioTestCase (Python 3.8+); less polished than pytest-asyncio.
  • No fixture-scope concept; setUp/tearDown is per-test only (setUpClass for per-class).

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):

MethodUse
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.