Skip to Content
Tech PrinciplesArchitecturePatternsAAA Testing Pattern

AAA Testing Pattern 🧪

The AAA pattern divides test methods into three clear sections:

  • Arrange - Set up the preconditions and inputs needed for the test
  • Act - Execute the test method itself
  • Assert - Verify the test results and outputs

This structure promotes readable, maintainable test code. Tests are easier to understand when divided into logical arrange, act, and assert stages.

Example

public class DepositHandlerTest { private static final String ACCOUNT_NUMBER = "ACCOUNT_NUMBER"; private static final double ACCOUNT_VALUE = 100.0; private DepositHandler handler = new DepositHandler(); @Test public void testDeposit() { // Arrange BankAccount account = new BankAccount(ACCOUNT_NUMBER, ACCOUNT_VALUE); double amount = 50.0; // Act BankAccount updatedAccount = handler.deposit(amount, account); // Assert assertEquals(ACCOUNT_VALUE + amount, updatedAccount.getBalance()); } }

Recommendations

Arrange

  • Mock only the dependencies of the class under test - test data can be built directly in a reusable and immutable way.
// DO NOT DO THIS @Mock private Customer customer;
  • Use different test data for each test case - reusing the same test data across multiple test functions can lead to unexpected failures, forcing you to repeatedly debug all the impacted tests.
public class CustomerServiceTest { private static final String CUSTOMER_NAME = "CUSTOMER_NAME"; private static final String ADDRESS = "ADDRESS"; // DO NOT DO THIS private Customer customer = new Customer(...); public void testCustomerShould...() { // DO THIS Customer customer = buildCustomer(CUSTOMER_NAME); } private Customer buildCustomer(String name) { return new Customer(name, ADDRESS); } }
  • Extracting data-building logic into separate builder classes can be beneficial - it’s fine as long as the builders are generic enough, immutable (return new instances), use never-changing constants, inherit from each other, and optionally remain extensible by providing a base builder open for extension.
public class CustomerBuilder { // DO NOT DO THIS public static buildCustomer(String name, String address, String phone, List<Orders> orders, Profile profile, String neverEndingListOfProperties) { // Above 5 constructor parameters, something has to be refactored. return Customer(...); } // DO THIS public static String ADDRESS = "ADDRESS"; public static buildCustomer(String name, List<Order> orders) { return new Customer(name, orders, ADDRESS); } public static buildCustomer(String name) { return buildCustomer(name, List.of(buildOrder())); } public Customer.Builder prepareCustomer() { Profile profile = buildProfile(); List<Order> orders = List.of(buildOrder()); return Customer.Builder() .name(NAME) .phone(PHONE_NUMBER) .orders(orders) .profile(profile); } }
  • Languages like Kotlin can reduce boilerplate code - they provide better support through default values and ways to copy objects while setting only what you need.
// In this case, leveraging an extension function in your test sources, and uses // a default value while keeping the flexibility of defining a value if needed. fun Customer.sample(name: String = NAME): Customer { return Customer(name) } // Avoids implementing an open builder pattern in a function. @Test fun `given valid customer should return ...` { val customer = buildCustomer().copy(address = "@address with special characters") }

Act

  • Strive for clarity when naming variables in tests.
// OKAY: Generic approach Event event = buildEvent(); Event result = eventService.update(event); assertEquals(EXPECTED_VALUE_A, result.getValueA()); // GOOD: Generic but symmetry conveying the relationship clearly Event input = buildEvent(); Event output = eventService.process(input); assertEquals(EXPECTED_VALUE_A, output.getValueA()); // GREAT: Context-based approach and the most descriptive Event event = buildEvent(); Event processedEvent = eventService.process(event); assertEquals(EXPECTED_VALUE_A, updatedEvent.getValueA());
  • Avoid any() at all costs, and strictly verify what each test is intended for.
// DO NOT DO THIS Event result = eventService.update(event, any(), any()); // DO THIS Event result = eventService.update(event, eq(VALUE1), eq(VALUE2));

Assert

  • Use descriptive constants over magic numbers, hard-coded values, or the input as the expected value. Add meaningful context through constant names, but keep them concise.
public void testEventTransformation() { // DO NOT DO THIS assertEquals(input.getValueA(), result.getValueA()) assertEquals(12, result.getValueA()) assertEquals("myExpectedValue", result.getValueA()) // DO THIS (GOOD) assertEquals(EXPECTED_VALUE_A, result.getValueA()); // DO THIS (GREAT) assertEquals(STANDARDIZED_US_ADDRESS, result.getValueA()) }
Last updated on