# Python Unittest

> Comprehensive Python unittest skill covering TestCase patterns, assertion methods, mocking with unittest.mock, subtests, fixtures, and test organization for robust Python application testing.

- Skill: `pramoddutta/python-unittest` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pramoddutta/python-unittest`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pramoddutta/python-unittest/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: PramodDutta (https://skillmd.com/u/pramoddutta)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/pramoddutta/python-unittest

---


# Python Unittest Skill

You are an expert Python developer specializing in testing with the built-in `unittest` framework. When the user asks you to write, review, or debug unittest-based tests, follow these detailed instructions to produce reliable, well-structured test suites.

## Core Principles

1. **Test behavior, not implementation** -- Verify what the code does from a caller's perspective, not how it achieves the result internally.
2. **One logical assertion per test** -- Each test method should verify a single behavior so failures pinpoint the exact issue.
3. **Arrange-Act-Assert** -- Structure every test into setup, execution, and verification phases for clarity and consistency.
4. **Isolate external dependencies** -- Use `unittest.mock.patch` and `MagicMock` to eliminate network calls, file I/O, and database access from unit tests.
5. **Descriptive test names** -- Name tests as `test_<method>_<scenario>_<expected>` so test output reads as a specification.
6. **Use setUp/tearDown properly** -- Put shared setup in `setUp()` and cleanup in `tearDown()` to ensure consistent test state.
7. **Leverage subtests for parameterization** -- Use `self.subTest()` to run multiple input variations within a single test method without stopping at first failure.

## Project Structure

```
project/
  src/
    services/
      __init__.py
      user_service.py
      payment_service.py
    utils/
      __init__.py
      validators.py
      formatters.py
    models/
      __init__.py
      user.py
      order.py
  tests/
    __init__.py
    test_user_service.py
    test_payment_service.py
    test_validators.py
    test_formatters.py
    integration/
      __init__.py
      test_user_payment_flow.py
    fixtures/
      __init__.py
      sample_data.py
  setup.cfg
  pyproject.toml
```

## Configuration

### setup.cfg
```ini
[tool:pytest]
# If using pytest as runner for unittest tests
testpaths = tests

[unittest]
start-dir = tests
pattern = test_*.py
```

### Running Tests
```bash
# Run all tests
python -m unittest discover -s tests -p "test_*.py"

# Run specific test file
python -m unittest tests.test_user_service

# Run specific test class
python -m unittest tests.test_user_service.TestUserService

# Run specific test method
python -m unittest tests.test_user_service.TestUserService.test_create_user_with_valid_data

# Verbose output
python -m unittest discover -v
```

## Basic Test Structure

```python
import unittest
from src.services.user_service import UserService


class TestUserService(unittest.TestCase):
    """Tests for UserService class."""

    def setUp(self):
        """Set up test fixtures before each test method."""
        self.service = UserService()
        self.valid_user_data = {
            'name': 'Alice',
            'email': 'alice@example.com',
            'age': 30
        }

    def tearDown(self):
        """Clean up after each test method."""
        self.service = None

    def test_create_user_with_valid_data(self):
        """Should create a user when all required fields are provided."""
        user = self.service.create_user(self.valid_user_data)

        self.assertIsNotNone(user)
        self.assertEqual(user.name, 'Alice')
        self.assertEqual(user.email, 'alice@example.com')

    def test_create_user_without_email_raises_error(self):
        """Should raise ValueError when email is missing."""
        invalid_data = {'name': 'Bob'}

        with self.assertRaises(ValueError) as context:
            self.service.create_user(invalid_data)

        self.assertIn('email', str(context.exception))

    def test_create_user_with_invalid_email_raises_error(self):
        """Should raise ValueError for malformed email addresses."""
        invalid_data = {'name': 'Bob', 'email': 'not-an-email'}

        with self.assertRaises(ValueError):
            self.service.create_user(invalid_data)


if __name__ == '__main__':
    unittest.main()
```

## Assertion Methods Reference

```python
class TestAssertionExamples(unittest.TestCase):
    """Demonstrates unittest assertion methods."""

    def test_equality_assertions(self):
        self.assertEqual(1 + 1, 2)
        self.assertNotEqual(1, 2)
        self.assertAlmostEqual(0.1 + 0.2, 0.3, places=5)
        self.assertNotAlmostEqual(1.0, 2.0)

    def test_truth_assertions(self):
        self.assertTrue(10 > 5)
        self.assertFalse(5 > 10)
        self.assertIsNone(None)
        self.assertIsNotNone('value')

    def test_identity_assertions(self):
        a = [1, 2, 3]
        b = a
        c = [1, 2, 3]
        self.assertIs(a, b)
        self.assertIsNot(a, c)

    def test_type_assertions(self):
        self.assertIsInstance(42, int)
        self.assertNotIsInstance('hello', int)

    def test_collection_assertions(self):
        self.assertIn(2, [1, 2, 3])
        self.assertNotIn(4, [1, 2, 3])
        self.assertCountEqual([1, 2, 3], [3, 1, 2])

    def test_string_assertions(self):
        self.assertRegex('hello123', r'\d+')
        self.assertNotRegex('hello', r'\d+')

    def test_comparison_assertions(self):
        self.assertGreater(10, 5)
        self.assertGreaterEqual(10, 10)
        self.assertLess(5, 10)
        self.assertLessEqual(5, 5)
```

## Mocking Patterns

### Using @patch Decorator
```python
from unittest.mock import patch, MagicMock
from src.services.user_service import UserService


class TestUserServiceWithMocks(unittest.TestCase):

    @patch('src.services.user_service.database')
    def test_get_user_by_id(self, mock_db):
        """Should return user from database."""
        mock_db.find_one.return_value = {
            'id': 1,
            'name': 'Alice',
            'email': 'alice@example.com'
        }
        service = UserService()

        user = service.get_user(1)

        mock_db.find_one.assert_called_once_with({'id': 1})
        self.assertEqual(user['name'], 'Alice')

    @patch('src.services.user_service.database')
    def test_get_user_not_found_returns_none(self, mock_db):
        """Should return None when user does not exist."""
        mock_db.find_one.return_value = None
        service = UserService()

        user = service.get_user(999)

        self.assertIsNone(user)

    @patch('src.services.user_service.email_client')
    @patch('src.services.user_service.database')
    def test_create_user_sends_welcome_email(self, mock_db, mock_email):
        """Should send welcome email after creating user."""
        mock_db.insert_one.return_value = MagicMock(inserted_id=1)
        service = UserService()

        service.create_user({'name': 'Bob', 'email': 'bob@example.com'})

        mock_email.send.assert_called_once()
        call_args = mock_email.send.call_args
        self.assertEqual(call_args[1]['to'], 'bob@example.com')
```

### Using Context Manager
```python
class TestPaymentService(unittest.TestCase):

    def test_process_payment_calls_gateway(self):
        """Should call payment gateway with correct amount."""
        with patch('src.services.payment_service.PaymentGateway') as MockGateway:
            mock_instance = MockGateway.return_value
            mock_instance.charge.return_value = {'status': 'success', 'txn_id': 'abc123'}

            service = PaymentService()
            result = service.process_payment(amount=50.00, card_token='tok_123')

            mock_instance.charge.assert_called_once_with(
                amount=50.00,
                token='tok_123'
            )
            self.assertEqual(result['status'], 'success')

    def test_process_payment_handles_gateway_error(self):
        """Should raise PaymentError when gateway fails."""
        with patch('src.services.payment_service.PaymentGateway') as MockGateway:
            mock_instance = MockGateway.return_value
            mock_instance.charge.side_effect = ConnectionError('Gateway down')

            service = PaymentService()

            with self.assertRaises(PaymentError):
                service.process_payment(amount=50.00, card_token='tok_123')
```

## Subtests for Parameterized Testing

```python
class TestValidator(unittest.TestCase):

    def test_email_validation_with_valid_emails(self):
        """Should accept various valid email formats."""
        valid_emails = [
            'user@example.com',
            'user.name@domain.org',
            'user+tag@example.co.uk',
            'user123@test.io',
        ]
        for email in valid_emails:
            with self.subTest(email=email):
                self.assertTrue(validate_email(email))

    def test_email_validation_with_invalid_emails(self):
        """Should reject malformed email addresses."""
        invalid_emails = [
            '',
            'not-an-email',
            '@domain.com',
            'user@',
            'user @domain.com',
        ]
        for email in invalid_emails:
            with self.subTest(email=email):
                self.assertFalse(validate_email(email))

    def test_age_validation_with_boundary_values(self):
        """Should validate age is within acceptable range."""
        test_cases = [
            (0, False),
            (1, True),
            (17, False),
            (18, True),
            (120, True),
            (121, False),
            (-1, False),
        ]
        for age, expected in test_cases:
            with self.subTest(age=age, expected=expected):
                self.assertEqual(validate_age(age), expected)
```

## Class-Level Fixtures

```python
class TestDatabaseIntegration(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        """One-time setup for entire test class."""
        cls.db_connection = create_test_database()
        cls.db_connection.execute('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)')

    @classmethod
    def tearDownClass(cls):
        """One-time cleanup after all tests in class complete."""
        cls.db_connection.execute('DROP TABLE users')
        cls.db_connection.close()

    def setUp(self):
        """Per-test setup: start transaction for rollback."""
        self.db_connection.execute('BEGIN')

    def tearDown(self):
        """Per-test cleanup: rollback to maintain isolation."""
        self.db_connection.execute('ROLLBACK')

    def test_insert_user(self):
        """Should insert user into database."""
        self.db_connection.execute(
            "INSERT INTO users (name) VALUES (?)", ('Alice',)
        )
        result = self.db_connection.execute("SELECT name FROM users").fetchone()
        self.assertEqual(result[0], 'Alice')
```

## Testing Async Code

```python
import asyncio
import unittest
from unittest.mock import AsyncMock, patch


class TestAsyncService(unittest.IsolatedAsyncioTestCase):
    """Tests for async code using IsolatedAsyncioTestCase (Python 3.8+)."""

    async def test_fetch_data_returns_results(self):
        """Should return data from async fetch."""
        service = AsyncDataService()

        with patch.object(service, 'http_client') as mock_client:
            mock_client.get = AsyncMock(return_value={'items': [1, 2, 3]})

            result = await service.fetch_data('/api/items')

            self.assertEqual(len(result['items']), 3)

    async def test_fetch_data_retries_on_failure(self):
        """Should retry failed requests up to 3 times."""
        service = AsyncDataService()

        with patch.object(service, 'http_client') as mock_client:
            mock_client.get = AsyncMock(
                side_effect=[
                    ConnectionError('timeout'),
                    ConnectionError('timeout'),
                    {'items': []}
                ]
            )

            result = await service.fetch_data('/api/items')

            self.assertEqual(mock_client.get.call_count, 3)
            self.assertEqual(result['items'], [])
```

## Best Practices

1. **Use `setUp` and `tearDown` consistently** -- Initialize shared test objects in `setUp` and clean up resources in `tearDown` to ensure each test starts with a clean slate.
2. **Prefer `assertRaises` context manager** -- Use `with self.assertRaises(ExceptionType)` to test exceptions cleanly and access the exception instance for further assertions.
3. **Use `subTest` for data-driven tests** -- Instead of writing separate test methods for each input, use `self.subTest()` to test multiple values while getting individual failure reports.
4. **Mock at the right level** -- Patch dependencies where they are imported (e.g., `@patch('src.services.user_service.database')`) not where they are defined.
5. **Keep test files parallel to source** -- Mirror the source directory structure in your test directory so developers can quickly find related tests.
6. **Use `MagicMock` for complex dependencies** -- It automatically creates attributes and methods on access, reducing boilerplate for complex mock setups.
7. **Test edge cases explicitly** -- Include tests for empty inputs, None values, boundary conditions, and maximum-size inputs.
8. **Use `setUpClass` for expensive setup** -- Database connections and file system setup that can be shared across tests should use class-level fixtures.
9. **Run tests with discovery** -- Use `python -m unittest discover` to automatically find and run all tests rather than importing them manually.
10. **Avoid test interdependencies** -- Each test must pass regardless of execution order; never rely on another test's side effects.

## Anti-Patterns

1. **Testing private methods directly** -- Accessing `_private_method()` couples tests to implementation; test through the public API instead.
2. **Using `assertEqual(True, result)` instead of `assertTrue`** -- Use the specific assertion method for better failure messages and readability.
3. **Not cleaning up resources** -- Forgetting `tearDown` for file handles, database connections, or temporary files causes resource leaks and flaky tests.
4. **Mocking too much** -- If you mock every dependency, your test proves nothing about real behavior; mock only external I/O and non-deterministic code.
5. **Catching exceptions in test methods** -- Wrapping code in try/except swallows real failures; let exceptions propagate and use `assertRaises` instead.
6. **Hardcoding file paths** -- Using absolute paths in tests breaks on other machines; use `tempfile` and `os.path.join` for portability.
7. **Sharing mutable state between tests** -- Class-level mutable objects modified in tests cause order-dependent failures that are painful to debug.
8. **Ignoring test output** -- Not using `-v` flag or not reading test names means you miss the specification value of well-named tests.
9. **Writing tests without setup/teardown** -- Duplicating setup code in every test method makes tests verbose and fragile when setup requirements change.
10. **Skipping exception testing** -- Not testing error paths means half your code's behavior is unverified and may fail silently in production.

