Steve Kinney

Introduction to Testing

Arrange-Act-Assert (AAA) Pattern

Arrange-Act-Assert (AAA) Pattern

The Arrange-Act-Assert (AAA) pattern is a structured approach to writing unit tests that enhances readability and maintainability. It divides each test into three distinct sections:

  1. Arrange: Set up the conditions and inputs required for the test.
  2. Act: Execute the code or function being tested.
  3. Assert: Verify that the outcome matches the expected result.

Why It’s a Best Practice

  • Clarity: Clearly separating setup, execution, and verification makes tests easier to read and understand.
  • Consistency: A uniform structure across tests simplifies writing and maintaining them.
  • Maintainability: Well-organized tests are easier to update as the codebase evolves.
  • Debugging Efficiency: Identifying where a test fails becomes straightforward.
  • Single Responsibility: Encourages testing one functionality at a time, leading to more precise tests.

Examples in Practice

Example 1: Testing a Simple Function

Consider a function that calculates the factorial of a number:

function factorial(n) {
	if (n < 0) throw new Error('Negative input not allowed');
	return n === 0 ? 1 : n * factorial(n - 1);
}

Test Using AAA Pattern:

test('calculates factorial of a positive integer', () => {
	// Arrange
	const input = 5;
	const expectedOutput = 120;

	// Act
	const result = factorial(input);

	// Assert
	expect(result).toBe(expectedOutput);
});

Explanation:

  • Arrange: Set up the input value and expected output.
  • Act: Call the factorial function with the input.
  • Assert: Check that the result matches the expected output.

Example 2: Testing Error Handling

Testing how a function handles invalid input:

function divide(a, b) {
	if (b === 0) throw new Error('Cannot divide by zero');
	return a / b;
}

Test Using AAA Pattern:

test('throws an error when dividing by zero', () => {
	// Arrange
	const numerator = 10;
	const denominator = 0;

	// Act and Assert
	expect(() => {
		divide(numerator, denominator);
	}).toThrow('Cannot divide by zero');
});

Explanation:

  • Arrange: Initialize the numerator and set the denominator to zero.
  • Act and Assert: Execute the function inside an assertion to check for the expected error.

Example 3: Testing Asynchronous Code

Suppose there’s an asynchronous function that fetches data from an API:

async function fetchData(url) {
	const response = await fetch(url);
	return response.json();
}

Test Using AAA Pattern:

test('fetches data successfully from an API', async () => {
	// Arrange
	const url = 'https://api.example.com/data';
	const mockData = { id: 1, name: 'Test Data' };
	global.fetch = vi.fn(() =>
		Promise.resolve({
			json: () => Promise.resolve(mockData),
		}),
	);

	// Act
	const data = await fetchData(url);

	// Assert
	expect(data).toEqual(mockData);
	expect(global.fetch).toHaveBeenCalledWith(url);

	// Cleanup
	global.fetch.mockClear();
	delete global.fetch;
});

Explanation:

  • Arrange: Mock the fetch function to return predefined data.
  • Act: Call the fetchData function.
  • Assert: Verify that the returned data matches the mock data and that fetch was called with the correct URL.

Example 4: Testing a Class Method

Consider a simple Calculator class:

class Calculator {
	add(a, b) {
		return a + b;
	}
}

Test Using AAA Pattern:

test('adds two numbers correctly using Calculator class', () => {
	// Arrange
	const calculator = new Calculator();
	const num1 = 7;
	const num2 = 3;
	const expected = 10;

	// Act
	const result = calculator.add(num1, num2);

	// Assert
	expect(result).toBe(expected);
});

Explanation:

  • Arrange: Create an instance of Calculator and set up the numbers.
  • Act: Invoke the add method with the numbers.
  • Assert: Check that the result equals the expected sum.

Example 5: Testing a Function with Side Effects

Suppose a function logs a message to the console:

function logMessage(message) {
	console.log(message);
}

Test Using AAA Pattern:

test('logs the correct message to the console', () => {
	// Arrange
	const consoleSpy = vi.spyOn(console, 'log');
	const message = 'Hello, World!';

	// Act
	logMessage(message);

	// Assert
	expect(consoleSpy).toHaveBeenCalledWith(message);

	// Cleanup
	consoleSpy.mockRestore();
});

Explanation:

  • Arrange: Spy on the console.log method.
  • Act: Call logMessage with a test message.
  • Assert: Verify that console.log was called with the correct message.
  • Cleanup: Restore the original console.log method.

Example 6: Testing Edge Cases

Testing a function that processes an array:

function getFirstElement(array) {
	if (!Array.isArray(array)) throw new Error('Input must be an array');
	return array[0];
}

Test Using AAA Pattern:

test('returns undefined for an empty array', () => {
	// Arrange
	const inputArray = [];
	const expectedOutput = undefined;

	// Act
	const result = getFirstElement(inputArray);

	// Assert
	expect(result).toBe(expectedOutput);
});

Explanation:

  • Arrange: Prepare an empty array.
  • Act: Call getFirstElement with the empty array.
  • Assert: Verify that the result is undefined.

Example 7: Testing with Mock Service Worker (MSW)

Testing a function that makes an API call, using MSW to mock the network request:

// Function to fetch user data
async function getUser(id) {
	const response = await fetch(`/api/users/${id}`);
	return response.json();
}

Test Using AAA Pattern with MSW:

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
	rest.get('/api/users/:id', (req, res, ctx) => {
		const { id } = req.params;
		return res(ctx.json({ id, name: 'John Doe' }));
	}),
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('fetches user data successfully', async () => {
	// Arrange
	const userId = '123';

	// Act
	const user = await getUser(userId);

	// Assert
	expect(user).toEqual({ id: '123', name: 'John Doe' });
});

Explanation:

  • Arrange: Set up MSW to intercept network requests and return mock data.
  • Act: Call the getUser function with a test user ID.
  • Assert: Verify that the returned user data matches the mock data.

Key Takeaways

With all of that said, here’s the gist:

  • Separation of Concerns: The AAA pattern enforces a clear separation between setup, execution, and verification.
  • Readability: Tests become self-explanatory, serving as documentation for the code’s expected behavior.
  • Consistency: Following a standard pattern reduces cognitive load when switching between different tests or projects.
  • Maintainability: Easier to update tests when changes occur in the codebase.
  • Debugging Efficiency: Simplifies identifying where a test might be failing.

Last modified on .