Unit Testing Standards¶
Status: 🟢 Active | Owner: Engineering Enablement | Last Reviewed: 2025-Q4
Overview¶
Unit tests are the foundation of a healthy test suite. They are fast (milliseconds per test), deterministic, and isolated — they verify that a single unit of behaviour works correctly without depending on external systems.
Coverage Requirements¶
| Code Type | Minimum Coverage | Target Coverage |
|---|---|---|
| Domain model (entities, value objects, domain services) | 90% | 100% |
| Application services / use cases | 80% | 90% |
| Infrastructure adapters | 60% (covered by integration tests) | 70% |
| Utility / helper classes | 80% | 90% |
Important
Coverage thresholds are enforced in CI. PRs that decrease overall coverage below the threshold are blocked from merging.
Coverage is measured with: - Java: JaCoCo (configured in pom.xml / build.gradle) - Python: pytest-cov (coverage.py under the hood) - TypeScript: Jest's built-in coverage (Istanbul) - Go: go test -cover
What to Unit Test¶
A unit test verifies the behaviour of a unit, not its internal implementation. A "unit" is typically a class or a module — the smallest piece of code that has a coherent responsibility.
Test: - Business logic and domain rules (the most critical tests). - Boundary conditions and edge cases. - Error paths and exception handling. - All branches of conditional logic.
Do not unit test: - Framework configuration (test it with integration tests). - Pure data containers with no logic. - Trivial getters and setters. - Infrastructure adapters (test those with integration tests against real infrastructure or TestContainers).
Test Structure: AAA Pattern¶
All unit tests follow the Arrange → Act → Assert (AAA) pattern:
@Test
void should_apply_10_percent_discount_for_vip_customers() {
// Arrange
Customer customer = Customer.builder()
.id(CustomerId.of("cust-1"))
.tier(CustomerTier.VIP)
.build();
Order order = Order.create(customer, List.of(
OrderItem.of(product("SKU-1"), 2, Money.of(50, GBP))
));
// Act
Money discountedTotal = pricingService.calculateTotal(order, customer);
// Assert
assertThat(discountedTotal).isEqualTo(Money.of(90, GBP)); // 100 - 10%
}
Test Naming Convention¶
Test names should read as a sentence describing the behaviour under test. They are the documentation of your system's expected behaviour.
Pattern: should_<expected_behaviour>_when_<condition>
# ✅ Good names — describe behaviour
def test_should_raise_exception_when_order_has_no_items(): ...
def test_should_calculate_correct_total_with_multiple_items(): ...
def test_should_not_apply_discount_for_standard_tier_customers(): ...
# ❌ Poor names — describe implementation, not behaviour
def test_process_order(): ...
def test_calculate(): ...
def test_method_x_returns_true(): ...
Isolation and Mocking¶
Unit tests must be isolated — they must not depend on databases, file systems, network calls, or other services.
Use mocking/stubbing for all external dependencies:
@ExtendWith(MockitoExtension.class)
class OrderProcessorTest {
@Mock
private OrderRepository orderRepository;
@Mock
private EventPublisher eventPublisher;
@InjectMocks
private OrderProcessor processor;
@Test
void should_save_order_and_publish_event_on_successful_processing() {
Order order = buildValidOrder();
when(orderRepository.save(any())).thenReturn(order);
processor.process(order);
verify(orderRepository).save(order);
verify(eventPublisher).publish(any(OrderProcessedEvent.class));
}
}
from unittest.mock import MagicMock, patch
def test_should_save_order_and_publish_event():
# Arrange
order_repo = MagicMock(spec=OrderRepository)
event_publisher = MagicMock(spec=EventPublisher)
processor = OrderProcessor(order_repo, event_publisher)
order = build_valid_order()
# Act
processor.process(order)
# Assert
order_repo.save.assert_called_once_with(order)
event_publisher.publish.assert_called_once()
describe('OrderProcessor', () => {
let orderRepository: jest.Mocked<OrderRepository>;
let eventPublisher: jest.Mocked<EventPublisher>;
let processor: OrderProcessor;
beforeEach(() => {
orderRepository = { save: jest.fn(), findById: jest.fn() };
eventPublisher = { publish: jest.fn() };
processor = new OrderProcessor(orderRepository, eventPublisher);
});
it('should save order and publish event on successful processing', async () => {
const order = buildValidOrder();
orderRepository.save.mockResolvedValue(order);
await processor.process(order);
expect(orderRepository.save).toHaveBeenCalledWith(order);
expect(eventPublisher.publish).toHaveBeenCalledWith(
expect.any(OrderProcessedEvent)
);
});
});
Mocking Rules¶
- Mock at the boundary — mock interfaces (ports), not concrete classes where possible.
- Don't mock what you own — value objects and entities should be created directly, not mocked.
- Mock sparingly — a test with 10 mocks is testing the wrong thing (likely integration, not unit behaviour).
- Avoid
@Spyon the class under test — if you need to spy on the class being tested, your class is doing too much.
Parameterised Tests¶
Use parameterised tests to test multiple inputs without duplicating test logic:
@ParameterizedTest
@MethodSource("discountScenarios")
void should_calculate_correct_discount(CustomerTier tier, BigDecimal price, BigDecimal expectedTotal) {
Order order = buildOrderWithPrice(price);
Customer customer = buildCustomerWithTier(tier);
Money total = pricingService.calculateTotal(order, customer);
assertThat(total.getAmount()).isEqualByComparingTo(expectedTotal);
}
static Stream<Arguments> discountScenarios() {
return Stream.of(
Arguments.of(CustomerTier.STANDARD, new BigDecimal("100"), new BigDecimal("100")),
Arguments.of(CustomerTier.VIP, new BigDecimal("100"), new BigDecimal("90")),
Arguments.of(CustomerTier.ENTERPRISE, new BigDecimal("100"), new BigDecimal("80"))
);
}
Test Data¶
- Use factory methods or test builders for creating test objects. Avoid duplicating complex object setup across tests.
- Object Mother pattern: A class that provides pre-built, named test object instances (
TestOrders.standardOrder(),TestCustomers.vipCustomer()). - Test data must be self-contained — a test must not depend on data set up by another test. No shared mutable state between tests.
- For domain objects with many required fields, use the Builder pattern to make test setup readable.
Performance¶
Unit tests must be fast:
| Suite Size | Maximum Total Runtime |
|---|---|
| < 100 tests | < 5 seconds |
| 100–1000 tests | < 30 seconds |
| > 1000 tests | < 2 minutes |
Tests exceeding these bounds suggest they are not proper unit tests (likely hitting a database or doing I/O). Profile and fix before the suite grows further.
References¶
Last reviewed: 2025-Q4 | Owner: Engineering Enablement