How To Write Unit Tests With 1 Simple Acronym
Now that we have a firm understanding of what unit testing is, we’re finally ready to talk about how to do it. The good news is that the hard part is over and remembering the steps is really easy.
Developers love acronyms so of course they came up with another one: AAA. It’s not as fun as DRY (don’t repeat yourself) or KISS (keep it simple, silly), but AAA is pretty easy to remember. If you want your code to earn a AAA score you’ll need to have comprehensive unit tests. AAA stands for the three steps you’ll need while writing unit tests: Arrange, Act, and Assert.
Arranging your unit tests is the single most important step. It’s also sometimes the most confusing step so we’re going to spend quite a bit of time discussing it here.
Earlier in this guide we discussed the concept of decoupling and dependency injection when we talked about Inversion of Control and – surprise! – that’s a really important part of arranging unit tests. When the time comes to test our code there are two more terms we’ll need to learn and keep in mind. The first is System Under Test (SUT), which is the piece of code you’re trying to test – the “unit” we discussed in the previous section. The next term is Dependencies. We discussed this earlier, but it’s important to really understand what Dependencies mean in relation to our SUT. A Dependency is any outside component that affects our SUT in any way. The whole purpose of arranging tests is to isolate our SUT from its dependencies.
A common pattern when writing code is to log actions as they occur. Consider a simple calculator application that has a function called Add. Add takes in two numbers and returns the sum of those two numbers. We decide to log our input values and the output value every time the Add function is invoked, and in order to do that we want to use a special class called Logger. As we arrange our tests for the Add function we want to control how the Logger dependency behaves. That way we can make sure that our tests are actually only testing the Add function and not anything in the Logger class. Arranging the Logger class also allows us to ensure that the Logger class itself isn’t interfering with our unit tests.
Arranging dependencies can be done using a variety of something called test doubles. It is generally accepted that there are five different types of test doubles: stubs, mocks, spies, fakes, and dummy objects. Each type of test double serves a different purpose so it’s important to know when to use each one.
Stubs are the most frequently used form of test double and are used to return static results instead of executing an actual method. It is important to note that stubs cannot be checked for compliance and interactivity. If our Log function is synchronous and returns a Boolean we might use a stub in its place for our unit testing. We can specify that the stub should always return true. When the time comes for the Add method to be called during our test, the stub is used in place of the actual Log method, but remember that if we use a stub we can’t confirm that the stub was actually called.
Mocks are a slightly different form of test double from stubs, but are also used very frequently during unit testing. A mock implements a real interface that is used by the SUT, takes no action, and can be checked for compliance and interactivity. Most testing frameworks allow mocks to be configured to return values if desired, but the key component of mocks to keep in mind is that you can check whether they were called, and usually what values were passed to them during each call.
Fakes are fully functional objects that replace other fully functional objects. A fake has the same interface as the actual object it is faking, but it is implemented completely differently. A common example of when to use a fake is when the real code accesses a database. Since database access can have side effects and also tends to take a while – at least compared to how long we want our test to run – we typically wouldn’t want to access a database during a unit test. Instead, we can use a fake database to stand in for our real database and essentially pretend to do what the database would do.
Spies are observation points that can be configured on the dependencies of the SUT. By this definition a spy doesn’t do anything other than watch an interaction point between the SUT and a dependency. When the SUT calls the dependency, the spy makes a note of the fact that the dependency was called, what parameter values were passed to the dependency, and possibly what result was returned from the dependency.
Dummies are the most straightforward test double. They are simply objects that we define in our test to pass to our SUT in order for our code to compile. The values on dummy objects aren’t typically used in our test, but our test can’t work without supplying a dummy object to the SUT as a parameter.
If we go back to our calculator example from above we now know that we want to use a mock object to arrange our first test of the Add function because we want to confirm that the Log function was called when we called the Add function, but we don’t want the actual Log function to be called. If we used a stub we wouldn’t be able to tell if Log was called and if we used a spy we would still be calling the Log function. We might be able to use a fake instead of a mock, but that would require a lot more setup than a mock and wouldn’t provide any additional benefit on top of a mock.
Arranging dependencies is an important step in arranging the overall test, but it’s not all we need to do. Part of arranging a unit test is deciding what values to pass to a function if that function accepts parameters. In the case of our Add function we know that it accepts two parameters that are numbers so we need to decide what numbers to pass as parameters. This part of arranging the test will be very important when we get to checking our results because we can only know what to expect as a result if we know what values we put in.
Acting should be the simplest part of unit testing. The act shouldn’t usually comprise more than one line because its only purpose is to invoke the SUT with the arranged dependencies and parameters. In the case of our Add example this would be when we actually call the Add function with our two number parameters.
"For us, the real goal is to make it so that the software ecosystem is as healthy as possible." – James Gosling
The assert step is where we check to see that we got the expected results from our act step. The whole point of our unit test is to put in certain values and get different values back and this is where we check to see whether that happened. In the case of our Add function we’d be checking to see whether the Log function was called in one test and we’d probably have another test to make sure that the two numbers we provided were added together.
Remember that earlier we said each test should only test one item. In the case of our Add function we could write a single test and check whether the Log function was called and also check that the result of the function call was the sum of the two input parameters. While we could do that, it is considered bad practice for documentation purposes. By testing each of these items in separate tests we can name each of those tests clearly and with a name that describes what that test is doing. For example, we might name one test AddShouldLogTheParametersPassedToIt and the other test might be named AddShouldReturnTheSumOfTheTwoInputValues. Now, our tests are documenting what they do while also documenting what should happen when the Add function is called.
Even though each test should only make assertions about one item, we can still have multiple assertion statements in a single test. That’s confusing so let’s clarify. In the test called AddShouldLogTheParametersPassedToIt we might have one assertion that checks whether Log was called once (as opposed to being called multiple times) and another assertion that checks whether Log was called with the correct parameters. In both cases we’re testing whether the Log function was properly called so it’s acceptable to group those assertions into a single test.
Unit Tests For The Win!
Remember the acronym AAA (Arrange, Act, Assert) and continue practicing how you write unit tests. You won't get it right or perfect the first time, but over time you will improve and make your code cleaner, more stable, and more effective.
Andrew Webster is a software extraordinaire who loves to write code that is clean, testable, and stable. He is a Certified Scrum Master, was a Co-Founder of a tech startup, and worked for one of the largest software consulting companies. You can find him on LinkedIn here.