Arrange, Act, Assert: What is AAA Testing?

by Hit Subscribe 24. January 2019 02:25

What is AAA Testing? It almost sounds like another certification, doesn't it? Do you need to pass your AAA Test ASAP so you're not SOL with the PMO? Or can you use AAA in GCP to get better SEO?

No, AAA isn't just another technology acronym. It stands for arrange, act, assert. AAA is a way to structure your unit tests so they're easier to read, maintain, and enhance.

Let's take a look at how arrange, act, assert improves your unit tests.

AAA_arrange_act_assert_explained

What Is AAA Testing?

AAA is a pattern for organizing tests. It breaks tests down into three clear and distinct steps:

1. Arrange—Perform the setup and initialization required for the test.

2. Act—Take action(s) required for the test

3. Assert—Verify the outcome(s) of the test.

This pattern has several significant benefits. It creates a clear separation between a test's setup, operations, and results. This structure makes the code easier to read and understand. If you place the steps in order and format your code to separate them, you can scan a test and quickly comprehend what it does.

It also enforces a certain degree of discipline when you write your tests. You have to think clearly about the three steps your test will perform. But it makes tests more natural to write at the same time since you already have an outline.

Let's compare AAA testing to a test that doesn't use it.

A Unit Test Scenario

We'll use a C# implementation of Martin Fowler's Warehouse unit test example. We'll write tests for the warehouse class to illustrate the dos and don'ts of AAA testing.

We have a Warehouse object for adding, removing, and counting inventory items.

public class Warehouse
{

    public void add(string item, int quantity)
    {...}

    public bool remove(string item, int quantity)
    {...}

    public int count(string item)
    {...}
}

The add method accepts an item name and a quantity.  If the item doesn't exist, the warehouse stores the name and count in its inventory. If the article does exist, it adds the new amount to its current inventory.

You check the current inventory with the count method. The count for an item that doesn't exist is always zero.

When you sell an item, you remove the quantity sold from the inventory with the remove method. If the item doesn't exist, or if there aren't enough in inventory for the transaction, it returns false. If it does exist and there are enough in stock, it decrements the inventory and returns true.

What Isn't AAA Testing?

A Proposed Test Class

This a simple class, and testing it shouldn't require a lot of code. Let's take a quick stab at it.

Here's a first draft of a test class:

[TestClass]
public class TestWarehouse
{

    [TestMethod]
    public void testAddRemove()
    {
        // Initialize a warehouse             
        Warehouse testWarehouse = new Warehouse();
        testWarehouse.add("widget", 100);

        // Make sure initialize succeeded
        Assert.AreEqual(100, testWarehouse.count("widget"));

        // Make a simple sale
        Assert.IsTrue(testWarehouse.remove("widget", 64));
        Assert.AreEqual(36, testWarehouse.count("widget"));

        // Try to make a bad sale
        Assert.IsFalse(testWarehouse.remove("widget", 50));

        // Put enough items back in stock, then try again
        testWarehouse.add("widget", 14);
        Assert.IsTrue(testWarehouse.remove("widget", 50));
        Assert.AreEqual(0, testWarehouse.count("widget"));
    }
}

We have a single test method. It does the following:
1. Creates a warehouse and adds some inventory.
2. Verifies the number of items in the warehouse.
2. Removes some items and confirms that the correct number remain.
3. Tries to remove too many things and verifies the result.
4. Adds enough items back to complete the sale.
5. Tries the transaction again and confirms that it works.

This test covers the behavior I described above in ten lines of code. This test class isn't perfect, but it does the job.

What's Wrong With It?

There are an awful lot of steps in that test method, but it verifies the class' important behavior in just ten lines. But how well does this code scan? How long did it take you to work out what the test method is doing?

Imagine saving this code, checking it in, and coming back a few months later to make some changes. How easy would it be to remember what it's doing? In the case of a simple class like this warehouse, it might not take long. But most of the code we work with every day is more complicated than this example.

Now, let's consider adding a new method to the warehouse class.

 public bool inStock(string item, int quantity)
 {...}

This method tells you whether an item has enough in stock to meet a sale.

Where would you add it to this test? Do you add another line and comment at the beginning? Somewhere toward the middle before we force a failure? Do you add two lines of code—one for too many items and one for a sufficient amount? Or is it time to create an additional test method?

This test is functionally correct, but with the addition of a just one new feature, it needs refactoring. Setting aside the merits of AAA testing, this test method violates the single responsibility principle. It has several reasons to change. You can take it one step further and say the test has more than one reason to fail. If this test method failed after a small code change, it might take some digging to discover why.

So What Is AAA Testing?

A Refactored Test

Let's refactor this test into an example of the arrange, act, assert pattern.

[TestClass]
public class UnitTest1
{

    [TestMethod]
    public void testAddItems()
    {
        // Arrange
        Warehouse testWareHouse = new Warehouse();

        // Act
        testWareHouse.add("widget", 100);

        // Assert
        Assert.AreEqual(100, testWareHouse.count("widget"));
    }

    [TestMethod]
    public void testRemoveItems()
    {
        // Arrange
        Warehouse testWareHouse = new Warehouse();
        testWareHouse.add("widget", 100);

        // Act
        bool result = testWareHouse.remove("widget", 64);

        // Assert
        Assert.IsTrue(result);
        Assert.AreEqual(36, testWareHouse.count("widget"));
    }

    [TestMethod]
    public void testInsufficientItems()
    {
        Warehouse testWareHouse = new Warehouse();
        testWareHouse.add("widget", 24);

        bool result = testWareHouse.remove("widget", 25);

        Assert.IsFalse(result);
        Assert.AreEqual(24, testWareHouse.count("widget"));
    }

    [TestMethod]
    public void testRestockItems()
    {
        Warehouse testWareHouse = new Warehouse();
        testWareHouse.add("widget", 20);
        testWareHouse.add("widget", 44);

        bool result = testWareHouse.remove("widget", 63);

        Assert.IsTrue(result);
        Assert.AreEqual(1, testWareHouse.count("widget"));
    }
}

Our single test method is now four methods. We've doubled the lines of code to 20. But the class is easier to scan. Each function has a section for arrange, act, assert. There's a beginning, a middle, and an end. I omitted the comments from two of the methods to illustrate how clear each step is.

Almost half of our 20 lines are duplicates. What happened to DRY? Does AAA testing mean repeating code? No, not at all. Stick with me for a bit.

Why Is This a Better Test?

Readability is a major advantage to arrange, act, assert tests. Tests often do double duty. They verify behavior and act as documentation. So the easier it is to open up a test class and decipher what it's doing, the better.

We have four test methods instead of one. If one of them fails, we'll immediately know what went wrong. Unlike the first example, each method has only one reason to change and one reason to fail. In a large and complicated code base, tests that honor the single responsibility principle are much easier to troubleshoot.

The test class is also easier to work with. How would we add a test for inStock? The answer is obvious with AAA. We add two more test methods.

[TestMethod]
public void testItemsInStock()
{
    Warehouse testWareHouse = new Warehouse();
    testWareHouse.add("widget", 20);

    bool result = testWareHouse.inStock("widget", 19);

    Assert.IsTrue(result);
}

[TestMethod]
public void testItemsNotInStock()
{
    Warehouse testWareHouse = new Warehouse();
    testWareHouse.add("widget", 20);

    bool result = testWareHouse.inStock("widget", 21);

    Assert.IsFalse(result);
}

We add two methods, one for each case. If this looks a lot like how things work with TDD, that's not a coincidence. Think about new behavior in terms of how to set it up and exercise it, and then verifying the result is what test-driven development is.

But What About DRY?

Every method in this class creates a warehouse and adds items to it. In the first method, that's the test. In the rest, it's all or a part of the arrangements. Some refactoring might make these tests a bit easier to read.

We could add a private utility method

private Warehouse setupWarehouse(string item, int quantity)
{
    Warehouse testWareHouse = new Warehouse();
    testWareHouse.add(item, quantity);
    return testWareHouse;
}

And then use it in the tests

[TestMethod]
public void testRemoveItems()
{
    Warehouse testWareHouse = setupWarehouse("widget", 100);

    bool result = testWareHouse.remove("widget", 64);

    Assert.IsTrue(result);
    Assert.AreEqual(36, testWareHouse.count("widget"));
}

This cleans the code up a bit and makes some of the tests easier to read since it's clear what setupWarehouse is there for.

I would leave the first test, testAddItems, as it is to keep the arrange, act, assert steps clear.

Is it always this easy to clean up test arrangements? Of course not. But it's often worth a try, especially if you're closing up some tests that you won't be looking at for a long time.

AAA Testing Is Your Test Outline

When you structure your tests with arrange, act, assert, they tell a story. Almost anyone can open them up, read the code, and learn what behavior is being verified and why it should work.

The next time you write a test, take a step back and see how AAA can make it easier to write and understand. Better yet, open up some of your old tests and see how refactoring them with AAA testing would make them better. And while you're in there, take advantage of tools like NCrunch to make your test suite clean and efficient.

This post was written by Eric Goebelbecker. Eric has worked in the financial markets in New York City for 25 years, developing infrastructure for market data and financial information exchange (FIX) protocol networks. He loves to talk about what makes teams effective (or not so effective!)

Tags:

Blog

Month List