Python unittest
QA Skills Directory QA Skills is a curated directory of testing-specific skills for AI coding agents (Claude Code, Cursor, Copilot, etc.).
npx -y skills add PramodDutta/qaskills --skill python-unittestAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Comprehensive Python unittest skill covering TestCase patterns, assertion methods, mocking with unittest.mock, subtests, fixtures, and test organization for robust Python application testing.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
15.1 KB, ~3.1k tokens by cl100k_base, as published. Nobody here has run it
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
- Test behavior, not implementation -- Verify what the code does from a caller's perspective, not how it achieves the result internally.
- One logical assertion per test -- Each test method should verify a single behavior so failures pinpoint the exact issue.
- Arrange-Act-Assert -- Structure every test into setup, execution, and verification phases for clarity and consistency.
- Isolate external dependencies -- Use
unittest.mock.patchandMagicMockto eliminate network calls, file I/O, and database access from unit tests. - Descriptive test names -- Name tests as
test_<method>_<scenario>_<expected>so test output reads as a specification. - Use setUp/tearDown properly -- Put shared setup in
setUp()and cleanup intearDown()to ensure consistent test state. - 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
[tool:pytest]
# If using pytest as runner for unittest tests
testpaths = tests
[unittest]
start-dir = tests
pattern = test_*.py
Running Tests
# 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
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': '[email protected]',
'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, '[email protected]')
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
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
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': '[email protected]'
}
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': '[email protected]'})
mock_email.send.assert_called_once()
call_args = mock_email.send.call_args
self.assertEqual(call_args[1]['to'], '[email protected]')
Using Context Manager
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
class TestValidator(unittest.TestCase):
def test_email_validation_with_valid_emails(self):
"""Should accept various valid email formats."""
valid_emails = [
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
]
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
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
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
- Use
setUpandtearDownconsistently -- Initialize shared test objects insetUpand clean up resources intearDownto ensure each test starts with a clean slate. - Prefer
assertRaisescontext manager -- Usewith self.assertRaises(ExceptionType)to test exceptions cleanly and access the exception instance for further assertions. - Use
subTestfor data-driven tests -- Instead of writing separate test methods for each input, useself.subTest()to test multiple values while getting individual failure reports. - Mock at the right level -- Patch dependencies where they are imported (e.g.,
@patch('src.services.user_service.database')) not where they are defined. - Keep test files parallel to source -- Mirror the source directory structure in your test directory so developers can quickly find related tests.
- Use
MagicMockfor complex dependencies -- It automatically creates attributes and methods on access, reducing boilerplate for complex mock setups. - Test edge cases explicitly -- Include tests for empty inputs, None values, boundary conditions, and maximum-size inputs.
- Use
setUpClassfor expensive setup -- Database connections and file system setup that can be shared across tests should use class-level fixtures. - Run tests with discovery -- Use
python -m unittest discoverto automatically find and run all tests rather than importing them manually. - Avoid test interdependencies -- Each test must pass regardless of execution order; never rely on another test's side effects.
Anti-Patterns
- Testing private methods directly -- Accessing
_private_method()couples tests to implementation; test through the public API instead. - Using
assertEqual(True, result)instead ofassertTrue-- Use the specific assertion method for better failure messages and readability. - Not cleaning up resources -- Forgetting
tearDownfor file handles, database connections, or temporary files causes resource leaks and flaky tests. - Mocking too much -- If you mock every dependency, your test proves nothing about real behavior; mock only external I/O and non-deterministic code.
- Catching exceptions in test methods -- Wrapping code in try/except swallows real failures; let exceptions propagate and use
assertRaisesinstead. - Hardcoding file paths -- Using absolute paths in tests breaks on other machines; use
tempfileandos.path.joinfor portability. - Sharing mutable state between tests -- Class-level mutable objects modified in tests cause order-dependent failures that are painful to debug.
- Ignoring test output -- Not using
-vflag or not reading test names means you miss the specification value of well-named tests. - Writing tests without setup/teardown -- Duplicating setup code in every test method makes tests verbose and fragile when setup requirements change.
- Skipping exception testing -- Not testing error paths means half your code's behavior is unverified and may fail silently in production.