One of the most challenging tasks in writing reliable, consistent, maintainable, and easy-to-read tests is structuring them correctly. Failing to follow a specific structure in each unit test can lead to flaky or unreliable tests—the nightmare of every QA developer.
Over the years, many approaches to unit testing have emerged, with the Arrange-Act-Assert (AAA, or 3A) pattern standing out as the most popular. This pattern involves structuring your unit tests into three steps:
- Arrange: Set up the test environment.
- Act: Execute the code to test.
- Assert: Verify the results.
In this guide, you will learn what the AAA pattern is, how it works, the benefits it offers, and its role in unit test automation.
Let’s dive in!
The AAA Pattern in Unit Testing
The Arrange-Act-Assert pattern, also known as the AAA or 3A pattern, is a widely recognized approach to structuring tests. It was originally proposed by Bill Wake in 2001 and then mentioned in Kent Beck’s influential book “Test Driven Development: By Example” in 2002.
The AAA pattern promotes clarity by recommending that tests be structured into three distinct phases:
- Arrange everything necessary to perform the test.
- Act on the target code to be tested by executing it.
- Assert the expected outcomes.
The AAA pattern enhances readability and maintainability, closely mirroring the Given-When-Then structure developed by Daniel Terhorst-North and Chris Matts as part of BDD (Behavior-Driven Development). Today, nearly all modern unit testing tools with BDD syntax encourage using the AAA pattern.
Time to explore the three phases it consists of!
Arrange
The Arrange step is where you prepare everything needed for executing the test to make sure it produces accurate results. This involves initializing objects, configuring dependencies, and setting up the environment required for the test scenario.
Examples of operations you may need to perform in this phase include:
- Creating instances of the classes to be tested
- Initializing global or local variables with specific values
- Setting up mock objects to simulate external services
- Populating a sample database with test data
- Configuring specific settings and configurations
By thoroughly arranging the test context and state, you ensure that the actions and assertions that follow occur in a controlled and predictable environment. This approach enhances test reliability and reduces the likelihood of flaky tests.
Act
The Act step is where you execute the specific functionality you want to test. This phase involves interacting with the SUT (System Under Test) by calling a method or executing a function that you arranged in the previous step.
In most unit tests, this phase corresponds to calling a function or method of an object/class to perform a specific operation. The goal is to execute the action that will generate the outcome you intend to verify in the next step.
To keep the test easy to read and maintain, this action should be concise and focused. A single line of code where you call the method or function to test is generally enough. This simplicity makes it easier to observe and understand the effect of the specific code under test.
Assert
The Assert step is where you verify that the outcome of the unit test matches your expectations. This phase entails checking the results produced by the Act phase against the expected values to confirm that the SUT behaves as desired.
That typically involves using assertion methods to compare the actual results with the expected ones. Examples of assertions include:
- Verifying that a method returns the correct value
- Checking that an object’s state has changed as anticipated
- Guaranteeing that an exception is thrown under certain conditions
To maintain a test’s readability and effectiveness, the Assert step should be clear and specific. This is why it is recommended to have a single assertion—or a small set of related assertions—per unit test.
Example of a Unit Test Written With the AAA Pattern
To better understand how the Arrange-Act-Assert pattern works, take a look at the sample Mocha unit test below organized according to the AAA pattern:
import { expect } from "chai"
import { MathUtils } from "src/utils/MathUtils";
describe("MathUtils Tests", function () {
describe("#getFibonacciNumber()", function () {
it("should return the correct Fibonacci number", function () {
// Arrange: Initialize the class to test
const mathUtils = new MathUtils()
// Act: Test the method of a class
const result = mathUtils.getFibonacciNumber(6)
// Assert: Verify that the method produces the expected outcome
expect(result).to.equal(8)
})
})
})
Now, let’s break down the code to highlight which lines correspond to each of the three phases!
For more complex examples, check this and this Mocha tests on GitHub.
Arrange
const mathUtils = new MathUtils()
Here, you set up the test by creating an instance of the MathUtils
class.
In this example, the Arrange step occurs directly within the it()
function. However, when the test requires a more complex setup like a sample database, the phase is often handled in beforeAll()
or beforeEach()
hooks.
Note that imports and the describe()
functions are not considered part of the Arrange phase, which typically involves only the first lines of it()
functions and/or the code in beforeAll()
and beforeEach()
hooks.
Act
const result = mathUtils.getFibonacciNumber(6)
In this step, you use the mathUtility
object you initialized earlier to execute the getFibonacciNumber()
method with a given input.
Note: In this example, the input is simple and can be passed directly to the function under test. When dealing with more complex or multiple inputs, you should assign them to dedicated variables in the Arrange step and then use those variables in the Actphase.
Assert
expect(result).to.equal(8)
Finally, you use the expect()
method from Chai to verify that the result from the getFibonacciNumber()
method matches the expected output.
Note: In this case, the output is simple and can be asserted directly. For more complex outputs, you should assign them to dedicated variables in the Arrange step and then use them in the Assert step.
7 Good Reasons to Use the AAA Pattern
Explore the main benefits of adopting the AAA unit testing pattern.
Programming Language and Testing Framework Agnostic
Like other software design patterns, the Arrange-Act-Assert pattern is not tied to any specific programming language or tool. That means you can apply it in any testing framework, including Mocha and Jest in JavaScript, JUnit in Java, and pytest in Python.
This consistency allows the development team to use a unified approach to unit testing across different projects, regardless of the testing technology in use.
Better Code Organization
The AAA pattern promotes better code organization by encouraging you to separate unit tests into three distinct phases. This fixed structure helps maintain a consistent format across tests referred to different sections of your code.
By isolating the setup, execution, and verification stages, the pattern reduces clutter and confusion. This ensures that each test is focused and well-structured, resulting in more maintainable test code.
Makes Unit Tests Easier to Understand
The direct consequences of more organized code are improved readability and enhanced clarity. A unit test written using the Arrange-Act-Assert pattern is inherently straightforward, as it follows three neat steps.
You can easily understand tests following the pattern, even if you are unfamiliar with the specific testing tool or technology used. That is especially true when the chosen testing technology comes with an intuitive assertion API.
Encourages Test-Driven Development
The AAA unit testing pattern supports TDD (Test-Driven Development) by imposing an explicit structure for your tests. Each unit test that follows the pattern must consist of three steps, regardless of the code being tested.
As a result, you can define the expected behavior before the code is actually implemented. That is in line with TDD principles, where tests are created first to specify the desired functionality. This guides development so that the code meets the specified requirements.
Supports Refactoring
The Arrange-Act-Assert pattern supports refactoring by isolating changes to specific phases of the test, without affecting the overall test structure.
For example, if you modify how a method in a class accepts inputs, you only need to update the Act step. As long as the class uses the same logic to be instantiated and the method produces the same output, the Arrange and Assert phases can remain unchanged.
This allows you to refactor code confidently, knowing that your tests will require minimal updates.
Leads to Separations of Concerns
The AAA approach to unit testing fosters separation of concerns by ensuring that each of the three phases—Arrange, Act, and Assert—remains focused on its specific task. This clear division makes tests more modular and secure, as each step is responsible for a distinct part of the test process. By keeping setup, execution, and verification separate, you also reduce overlap in test logic.
Respect Testing Best Practices
Over the years, the Arrange-Act-Assert pattern has become a de facto standard in the industry. It is currently mentioned and recommended in several best practices guides, including:
- “JavaScript Testing Best Practices” on GitHub
- “Node.js Integration Test Best Practices” on GitHub
- “Unit Test Basics” in Visual Studio documentation
- “How To Write a Test” guide on Cypress’ blog
- Pytest’s documentation
The Role of AAA in Test Automation
The structured approach of the Arrange-Act-Assert pattern plays a key role in effective unit test automation. Particularly, the Assert step makes it easier to understand test results when executing them in CI/CD pipelines.
In the AAA unit test pattern, each test ends with the Assert phase, which typically includes a single or a few specific assertions. That makes the results of a test easier to evaluate consistently and objectively, even through logs.
For instance, consider the test results image from Mocha’s documentation:
From Mocha’s results, it is evident that the “#indexOf()” test suite in the “Array” collection contains two unit tests. The first fails, while the second passes.
By examining the failing unit test, which follows the AAA pattern, you can quickly pinpoint where to intervene to correct the issue:
describe("Array", function () {
describe("#indexOf()", function () {
it("should return -1 when not present", function () {
// arrange
const inputArray = [1, 2, 3]
// act
const result = inputArray.indexOf(4)
// assert
expect(result).to.equal(1)
})
})
// other test...
})
The Arrange step does not contain errors. Similarly, the Act step is implemented as intended. The problem lies in the Assert phase, where the -
sign before 1
is missing.
The test asserts the output against a wrong value and fails accordingly. You can fix that with:
expect(result).to.equal(-1)
The AAA pattern leads to tests that are straightforward for developers and QA engineers to interpret, even starting from result logs alone. This is essential for swiftly identifying and addressing issues, thereby speeding up the process of fixing broken deployments.
As demonstrated, the AAA pattern facilitates unit test automation. To learn more about this practice, check out our guide on the six principles of test automation.
Conclusion
In this guide, you learned about the Arrange-Act-Assert pattern and what a unit test structured using this approach looks like. You explored each step that the pattern recommends for designing readable and maintainable tests, and the benefits it brings to your testing operations. Backed by the community and widely regarded as a de facto standard, the AAA pattern also proves to be a valuable tool in unit test automation.