by thetestingacademy
Comprehensive Python unittest skill covering TestCase patterns, assertion methods, mocking with unittest.mock, subtests, fixtures, and test organization for robust Python application testing.
npx @qaskills/cli add python-unittestAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
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.
unittest.mock.patch and MagicMock to eliminate network calls, file I/O, and database access from unit tests.test_<method>_<scenario>_<expected> so test output reads as a specification.setUp() and cleanup in tearDown() to ensure consistent test state.self.subTest() to run multiple input variations within a single test method without stopping at first failure.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
[tool:pytest]
# If using pytest as runner for unittest tests
testpaths = tests
[unittest]
start-dir = tests
pattern = test_*.py
# 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
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()
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)
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')
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')
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 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')
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'], [])
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.assertRaises context manager -- Use with self.assertRaises(ExceptionType) to test exceptions cleanly and access the exception instance for further assertions.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.@patch('src.services.user_service.database')) not where they are defined.MagicMock for complex dependencies -- It automatically creates attributes and methods on access, reducing boilerplate for complex mock setups.setUpClass for expensive setup -- Database connections and file system setup that can be shared across tests should use class-level fixtures.python -m unittest discover to automatically find and run all tests rather than importing them manually._private_method() couples tests to implementation; test through the public API instead.assertEqual(True, result) instead of assertTrue -- Use the specific assertion method for better failure messages and readability.tearDown for file handles, database connections, or temporary files causes resource leaks and flaky tests.assertRaises instead.tempfile and os.path.join for portability.-v flag or not reading test names means you miss the specification value of well-named tests.- name: Install QA Skills
run: npx @qaskills/cli add python-unittest10 of 29 agents supported