Test Generator with One-Assertion Rule
Overview
This skill generates tests following the project's mandatory testing standards:
- One assertion per test (critical rule - violations will break the build)
- Descriptive test names using
test_should_[expected]_when_[condition]pattern - Arrange-Act-Assert (AAA) structure for clarity
- Proper mocking to isolate behavior under test
- 100% coverage requirement
When to Use This Skill
Use this skill when:
- Creating tests for new code
- Adding missing test coverage
- Refactoring tests that violate the one-assertion rule
- Converting multi-assertion tests into separate test functions
Critical Rule: ONE ASSERTION PER TEST
Each test function MUST contain exactly ONE assertion. If you need multiple assertions, create separate test functions.
Wrong (Multiple Assertions)
def test_user_creation(self):
user = create_user()
assert_that(user.name).is_equal_to("John") # ❌ Multiple assertions
assert_that(user.email).is_equal_to("john@example.com") # ❌
Correct (One Assertion Each)
def test_should_set_user_name_when_user_is_created(self):
user = create_user()
assert_that(user.name).is_equal_to("John") # ✓ Single assertion
def test_should_set_user_email_when_user_is_created(self):
user = create_user()
assert_that(user.email).is_equal_to("john@example.com") # ✓ Single assertion
Test Naming Convention
Test names must be descriptive sentences following this pattern:
test_should_[expected_behavior]_when_[condition]
Examples
test_should_return_404_when_resource_is_not_found()test_should_create_user_when_valid_data_is_provided()test_should_raise_validation_error_when_email_is_invalid()test_should_call_repository_read_method_when_use_case_executes()test_should_increment_counter_when_event_is_processed()
Naming Tips
- Start with
test_should_ - Describe the expected outcome (what should happen)
- End with
_when_followed by the condition or trigger - Be specific and descriptive
- Use full words, not abbreviations
Test Structure: Arrange-Act-Assert (AAA)
Every test should follow this three-part structure:
def test_should_return_coconut_when_id_exists(self):
# Arrange - Set up test data and mocks
repository = Mock()
use_case = GetCoconutUseCase(repository)
coconut_id = uuid.uuid4()
expected_coconut = Coconut(id=coconut_id)
repository.read.return_value = expected_coconut
# Act - Execute the behavior being tested
result = use_case.execute(coconut_id)
# Assert - Verify the expected outcome (ONE assertion only)
assert_that(result).is_equal_to(expected_coconut)
Section Guidelines
Arrange:
- Create test data
- Set up mocks and their return values
- Initialize the object under test with dependencies
- Configure the test scenario
Act:
- Execute the single method or behavior being tested
- Store the result if needed for assertion
- Should typically be one line
Assert:
- Verify ONE expected outcome
- MUST use
assertpylibrary (assert_that) - bareassertstatements are FORBIDDEN - Use
pytest.raisesfor exception testing
Testing by Layer
Domain Model Tests
Test Pydantic models and domain entities:
from python_sprint_zero.domain.model.coconut import Coconut
def test_should_create_coconut_with_id(self):
# Arrange
coconut_id = uuid.uuid4()
# Act
coconut = Coconut(id=coconut_id)
# Assert
assert_that(coconut.id).is_equal_to(coconut_id)
def test_should_support_none_id(self):
# Arrange & Act
coconut = Coconut(id=None)
# Assert
assert_that(coconut.id).is_none()
Repository Interface Tests (Domain)
Test the interface definition exists and has correct signatures:
from python_sprint_zero.domain.repository.coconut_repository import CoconutQueryRepository
def test_should_define_read_method(self):
# Arrange & Act
method = getattr(CoconutQueryRepository, 'read', None)
# Assert
assert_that(method).is_not_none()
Repository Implementation Tests (Infrastructure)
Test concrete repository implementations with proper setup:
from python_sprint_zero.infrastructure.persistence.in_memory.in_memory_coconut_query_repository import (
InMemoryCoconutQueryRepository
)
class TestInMemoryCoconutQueryRepository:
@pytest.fixture
def storage(self):
return {}
@pytest.fixture
def repository(self, storage):
return InMemoryCoconutQueryRepository(storage)
def test_should_return_coconut_when_id_exists(self, repository, storage):
# Arrange
coconut_id = uuid.uuid4()
coconut = Coconut(id=coconut_id)
storage[coconut_id] = coconut
# Act
result = repository.read(coconut_id)
# Assert
assert_that(result).is_equal_to(coconut)
def test_should_raise_exception_when_id_not_found(self, repository):
# Arrange
coconut_id = uuid.uuid4()
# Act & Assert
with pytest.raises(Exception) as excinfo:
repository.read(coconut_id)
assert_that(str(excinfo.value)).contains("not found")
Use Case Tests (Application)
Test use cases with mocked dependencies:
from unittest.mock import Mock
from python_sprint_zero.application.use_case.coconut_use_case import GetCoconutUseCase
class TestGetCoconutUseCase:
@pytest.fixture
def mock_repository(self):
return Mock()
@pytest.fixture
def use_case(self, mock_repository):
return GetCoconutUseCase(mock_repository)
def test_should_call_repository_read_method(self, use_case, mock_repository):
# Arrange
coconut_id = uuid.uuid4()
mock_repository.read.return_value = Coconut(id=coconut_id)
# Act
use_case.execute(coconut_id)
# Assert
mock_repository.read.assert_called_once_with(coconut_id)
def test_should_return_coconut_from_repository(self, use_case, mock_repository):
# Arrange
coconut_id = uuid.uuid4()
expected_coconut = Coconut(id=coconut_id)
mock_repository.read.return_value = expected_coconut
# Act
result = use_case.execute(coconut_id)
# Assert
assert_that(result).is_equal_to(expected_coconut)
Controller Tests (Interface)
Test API controllers with TestClient:
from fastapi.testclient import TestClient
from unittest.mock import Mock
class TestCoconutController:
@pytest.fixture
def mock_use_case(self):
return Mock()
@pytest.fixture
def controller(self, mock_use_case):
return CoconutController(
get_use_case=mock_use_case,
authentication_dependency=lambda x: None
)
@pytest.fixture
def client(self, controller):
app = FastAPI()
app.include_router(controller.router)
return TestClient(app)
def test_should_return_200_when_coconut_exists(self, client, mock_use_case):
# Arrange
coconut_id = uuid.uuid4()
mock_use_case.execute.return_value = Coconut(id=coconut_id)
# Act
response = client.get(f"/coconut/{coconut_id}")
# Assert
assert_that(response.status_code).is_equal_to(200)
def test_should_return_coconut_data_when_requested(self, client, mock_use_case):
# Arrange
coconut_id = uuid.uuid4()
mock_use_case.execute.return_value = Coconut(id=coconut_id)
# Act
response = client.get(f"/coconut/{coconut_id}")
# Assert
assert_that(response.json()["id"]).is_equal_to(str(coconut_id))
Mocking Guidelines
When to Mock
- External dependencies (databases, APIs, file systems)
- Repository implementations in use case tests
- Use cases in controller tests
- Authentication dependencies
What NOT to Mock
- The object under test itself
- Simple domain models (Pydantic models)
- Data transfer objects (DTOs)
Mock Setup
from unittest.mock import Mock
# Create mock
mock_repository = Mock()
# Configure return value
mock_repository.read.return_value = Coconut(id=uuid.uuid4())
# Configure exception
mock_repository.read.side_effect = Exception("Not found")
# Verify call
mock_repository.read.assert_called_once_with(coconut_id)
# Verify call with any arguments
mock_repository.read.assert_called_once()
Common Testing Patterns
Testing Exceptions
def test_should_raise_exception_when_not_found(self, use_case, mock_repository):
# Arrange
coconut_id = uuid.uuid4()
mock_repository.read.side_effect = Exception("Not found")
# Act & Assert
with pytest.raises(Exception) as excinfo:
use_case.execute(coconut_id)
assert_that(str(excinfo.value)).contains("Not found")
Testing with Fixtures
class TestMyFeature:
@pytest.fixture
def sample_id(self):
return uuid.uuid4()
@pytest.fixture
def repository(self):
return InMemoryRepository({})
def test_should_use_fixture(self, repository, sample_id):
# Use fixtures in test
pass
Testing HTTP Status Codes
def test_should_return_404_when_not_found(self, client, mock_use_case):
# Arrange
coconut_id = uuid.uuid4()
mock_use_case.execute.side_effect = Exception("Not found")
# Act
response = client.get(f"/coconut/{coconut_id}")
# Assert
assert_that(response.status_code).is_equal_to(404)
Test File Organization
File Naming
- Test files mirror source structure exactly
- Prefix with
test_:test_coconut_use_case.py
Class Organization
class TestGetCoconutUseCase:
# Group related tests in classes
# Use fixtures for setup
# One behavior = one class
def test_should_call_repository(self):
pass
def test_should_return_result(self):
pass
class TestCreateCoconutUseCase:
# Separate class for separate use case
pass
Splitting Multi-Assertion Tests
When you encounter tests with multiple assertions, split them:
Before (Violates Rule)
def test_user_creation(self):
user = create_user("John", "john@example.com")
assert_that(user.name).is_equal_to("John")
assert_that(user.email).is_equal_to("john@example.com")
assert_that(user.is_active).is_true()
After (Follows Rule)
def test_should_set_name_when_user_created(self):
# Arrange & Act
user = create_user("John", "john@example.com")
# Assert
assert_that(user.name).is_equal_to("John")
def test_should_set_email_when_user_created(self):
# Arrange & Act
user = create_user("John", "john@example.com")
# Assert
assert_that(user.email).is_equal_to("john@example.com")
def test_should_activate_user_when_created(self):
# Arrange & Act
user = create_user("John", "john@example.com")
# Assert
assert_that(user.is_active).is_true()
Note: Duplicating the Arrange-Act code is acceptable and encouraged. Each test should be independently readable.
Assertion Libraries
Using assertpy (Preferred)
from assertpy import assert_that
# Equality
assert_that(result).is_equal_to(expected)
# None checks
assert_that(value).is_none()
assert_that(value).is_not_none()
# Boolean
assert_that(value).is_true()
assert_that(value).is_false()
# Contains
assert_that(string).contains("substring")
assert_that(list).contains(item)
# Type checks
assert_that(value).is_instance_of(MyClass)
# Comparisons
assert_that(value).is_greater_than(5)
assert_that(value).is_less_than(10)
Using pytest.raises
import pytest
with pytest.raises(ValueError) as excinfo:
function_that_raises()
assert_that(str(excinfo.value)).contains("expected message")
Coverage Requirements
- 100% test coverage is mandatory
- Every function, class, and method must have tests
- Tests must be meaningful, not just coverage-seeking
- Use
toxto verify coverage
# Run all tests with coverage
tox
# Run specific tests during TDD iteration
tox -- tests/specific_test.py
# View coverage report
coverage report
Test Checklist
Before completing test generation, verify:
- Each test has exactly ONE assertion
- Test names follow
test_should_X_when_Ypattern - Tests use Arrange-Act-Assert structure
- Appropriate use of mocks for dependencies
- Tests are isolated and independent
- All edge cases covered (happy path, errors, edge cases)
- Tests verified with
tox(NOT justpytest- must pass all quality gates) - Coverage is 100%: confirmed by
toxoutput
Common Mistakes to Avoid
- Multiple assertions - Split into separate tests
- Using bare assert statements - ALWAYS use
assert_thatfrom assertpy - Vague test names - Use descriptive names with should/when
- Testing implementation details - Test behavior, not internals
- Not using mocks - Mock external dependencies
- Shared state between tests - Each test should be independent
- Missing edge cases - Test happy path, errors, and boundaries
- No AAA structure - Always use Arrange-Act-Assert
- Testing multiple behaviors - One test = one behavior
- Using pytest for final verification - Use
toxto verify all quality gates pass
References
See the references/ directory for:
- Complete test examples from the codebase
- Test patterns by layer
- Common testing scenarios
Converted and distributed by TomeVault — claim your Tome and manage your conversions.