TE
Unit Testing
Testing core v1.0.0
Unit Testing
Overview
Unit tests validate individual classes or functions in complete isolation from external dependencies. They form the base of the test pyramid — fast, deterministic, and numerous. Every domain service, utility class, mapper, and business rule should have comprehensive unit tests. In the full-lifecycle pipeline, @testing-qa generates unit test code for all services created in Phase 6 (backend) and Phase 7 (frontend).
Key Concepts
Unit Test Anatomy (Given-When-Then)
@Test
@DisplayName("should calculate total with discount when order has VIP customer")
void shouldCalculateTotalWithDiscount() {
// Given — arrange preconditions
var customer = CustomerFixture.vip();
var items = List.of(
OrderItem.of("Widget", Money.of(100)),
OrderItem.of("Gadget", Money.of(200))
);
// When — execute the behavior
var total = pricingService.calculateTotal(customer, items);
// Then — assert expected outcome
assertThat(total).isEqualTo(Money.of(270)); // 10% VIP discount
}
Test Double Types
| Type | Purpose | Example |
|---|---|---|
| Stub | Returns canned answers | when(repo.findById(1L)).thenReturn(Optional.of(user)) |
| Mock | Verifies interactions | verify(emailService).send(any(Email.class)) |
| Spy | Wraps real object, records calls | spy(realService) |
| Fake | Working implementation (simplified) | In-memory repository |
| Dummy | Fills parameter, never used | new Object() for unused param |
Test Naming Conventions
| Convention | Example |
|---|---|
| BDD-style | should_ReturnDiscount_When_CustomerIsVIP |
| Method-based | calculateTotal_vipCustomer_appliesDiscount |
| @DisplayName | @DisplayName("should apply 10% discount for VIP customers") |
Best Practices
- One logical concept per test — Don’t test create, update, and delete in one method
- Use @DisplayName — Human-readable test descriptions for documentation
- Use AssertJ over Hamcrest —
assertThat(result).isEqualTo(expected)is more readable - Use test data builders — Factory methods for complex objects, not constructors in every test
- Mock at boundaries — Mock external dependencies (DB, HTTP, MQ), not internal classes
- Test exception cases — Use
assertThatThrownBy()for expected failures - Keep tests fast — Under 50ms each; no I/O, no sleep, no real network
- No logic in tests — No if/else/loops in test methods; each path is a separate test
Code Examples
✅ Good: Java Unit Test (JUnit 5 + Mockito + AssertJ)
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentGateway paymentGateway;
@InjectMocks
private OrderService orderService;
@Nested
@DisplayName("placeOrder")
class PlaceOrder {
@Test
@DisplayName("should create order and process payment for valid request")
void shouldCreateOrderAndProcessPayment() {
// Given
var request = OrderFixture.validRequest();
var savedOrder = OrderFixture.pendingOrder(request);
when(orderRepository.save(any(Order.class))).thenReturn(savedOrder);
when(paymentGateway.charge(any())).thenReturn(PaymentResult.success());
// When
var result = orderService.placeOrder(request);
// Then
assertThat(result.status()).isEqualTo(OrderStatus.CONFIRMED);
verify(orderRepository).save(argThat(order ->
order.getCustomerId().equals(request.customerId())
));
verify(paymentGateway).charge(argThat(charge ->
charge.amount().equals(request.totalAmount())
));
}
@Test
@DisplayName("should throw InsufficientFundsException when payment fails")
void shouldThrowWhenPaymentFails() {
// Given
var request = OrderFixture.validRequest();
when(orderRepository.save(any())).thenReturn(OrderFixture.pendingOrder(request));
when(paymentGateway.charge(any())).thenReturn(PaymentResult.declined());
// When & Then
assertThatThrownBy(() -> orderService.placeOrder(request))
.isInstanceOf(InsufficientFundsException.class)
.hasMessageContaining("Payment declined");
}
}
}
✅ Good: React Unit Test (Jest + React Testing Library)
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { OrderForm } from './OrderForm';
describe('OrderForm', () => {
it('should display validation error when quantity is zero', async () => {
// Given
render(<OrderForm onSubmit={jest.fn()} />);
// When
fireEvent.change(screen.getByLabelText('Quantity'), {
target: { value: '0' },
});
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
// Then
await waitFor(() => {
expect(screen.getByText('Quantity must be at least 1'))
.toBeInTheDocument();
});
});
it('should call onSubmit with form data when valid', async () => {
// Given
const onSubmit = jest.fn();
render(<OrderForm onSubmit={onSubmit} />);
// When
fireEvent.change(screen.getByLabelText('Product'), {
target: { value: 'Widget' },
});
fireEvent.change(screen.getByLabelText('Quantity'), {
target: { value: '3' },
});
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
// Then
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
product: 'Widget',
quantity: 3,
});
});
});
});
❌ Bad: Common Unit Test Mistakes
@Test
void test1() {
// Multiple concepts in one test
var user = new User("test@example.com", "Test");
assertNotNull(userService.create(user)); // Create
user.setName("Updated");
assertNotNull(userService.update(user)); // Update
userService.delete(user.getId()); // Delete
assertNull(userService.findById(user.getId())); // Verify
// Should be 4 separate tests
}
@Test
void testWithRealDatabase() {
// Not a unit test — has real I/O dependency
var repo = new JdbcUserRepository(dataSource);
// This belongs in integration tests
}
Anti-Patterns
- Testing implementation — Verifying private method calls instead of public behavior
- Excessive mocking — Mocking everything including the SUT (system under test)
- Test per method — Tests should map to behaviors, not method names
- Magic values —
assertEquals(42, result)without explaining why 42 - Shared mutable state — Static fields or shared objects across tests
- Ignoring edge cases — Only testing the happy path
Testing Strategies
- TDD (red-green-refactor) — Write failing test → make it pass → refactor
- BDD naming — Use
should_expectedBehavior_when_conditionpattern - Parameterized tests —
@ParameterizedTest+@CsvSourcefor data-driven tests - Nested test classes —
@Nestedto group tests by method/behavior