An experience of unit testing with the Arrange, Act, Assert (AAA) pattern: Part II

Recap: in Part I, we looked at some “rules” we used to apply the “Arrange, Act, Assert” (AAA) pattern. In this part, we’ll look at some nuances, and how it sometimes highlighted issues in either the production or test code.

Phase labels

@Test
fun shouldReverseStringInput() {
// arrange
val exclaim = "!!"
val
firstInput = "cba"
val
secondInput = "gfed"
val
compoundInput = "$exclaim$secondInput$firstInput"

val
expectedReversedString = "abcdefg!!"

//act
val result = compoundInput.reversed()

//assert
assertThat(result).isEqualTo(expectedReversedString)
}

Although we can see where the three phases start and end, we should heed the words of Mark Seeman:

Whenever I need to add code comments to indicate the three AAA phases, an alarm goes off in my head. Something is wrong; the test is too complex. It would be better if I could refactor either the test or the SUT to become simpler.

It’s worth repeating that this could be either the test code or the SUT. If you find yourself with an overly chunky test, then it’s worth looking at the SUT, to see if that gives any clues as to why.

Simplify?

@Test
fun shouldReverseStringInput() {
val compoundInput = createCompoundInput()
val expectedReversedString = "abcdefg!!"

val
result = compoundInput.reversed()

assertThat(result).isEqualTo(expectedReversedString)
}

private fun createCompoundInput(): String {
val exclaim = "!!"
val
firstInput = "cba"
val
secondInput = "gfed"
val
compoundInput = "$exclaim$secondInput$firstInput"
return
compoundInput
}

We found that for our team, usually this wasn’t a good idea. It makes it harder to see the relationship between the actual and expected results in the assert phase. Plus, it doesn’t really simplify the test, it just adds indirection. With a modern IDE one could argue it’s only a click away, but it all adds to the cognitive load. And if it’s complicated to set up the data for a test, then perhaps the SUT is doing too much, and is too complex.

Sometimes though, we found ourselves repeating the same lines for a significant number of the tests. In these cases, wouldn’t extracting a common method be better?

Perhaps. If it’s in the “Arrange” phase, it could possibly be an indication that there’s another class that could be extracted from the SUT. Once again, there’s a possibility the SUT is doing too much.

If it’s in the “Assert” phase, then we need to be mindful of what Jeff Grigg warned us about:

Test methods that try to test too many different things at once.

We’ll add this caveat while we’re on this topic, as outlined by Roy Osherove:

My guideline is usually that you test one logical CONCEPT per test. You can have multiple asserts on the same object. They will usually be the same concept being tested.

We did sometimes have methods in our code which had multiple asserts on properties of the same object. It’s a bit more indirection, but with careful naming of the method, it can be manageable.

However, we also tolerated a little more repetition in our tests than we would do in our production code. For example we wouldn’t always extract two identical assert lines repeated two or three times. It’s a trade-off between readability/ease of understanding of an individual unit test, and making the test code DRY.

Did we have some larger tests with the phase labels? We did, but not very many. For example, some older legacy classes that we were planning on changing to a different architecture in future. Your mileage may vary, but we felt that we could live with the occasional chunky test for older code.

Multiple “Act” lines?

For example, say we have a test on a repository class, to ensure we use the cached data, and don’t make another http request:

@Test
fun shouldUseCachedData() {
// some setup in the "Arrange" phase here, which we'll
// leave out for the sake of simplicity

repository.makeARequestForData()
repository.makeARequestForData()

verify(exactly = 1) { repository.makeHttpRequestToApi() }
}

(Note: I’m using Mockk here for the verification, but hopefully the intention in this simplified test is clear)

The first repository.makeARequestForData() line will make the call to the makeHttpRequestToApi() method, which makes it feel a bit like an Action, so it’s tempting to put it in the “Act” phase.

However, this is really a way for us to set the data in the SUT in a convenient way, which makes sense in the context of this test. So, as it’s part of the setup of this test, it should go into the “Arrange” phase instead. The second repository.makeARequestForData() line is the real “Act” line. We could also amend the test method name to better reflect what we’re testing:

@Test
fun shouldNotMakeSecondCallToApiIfDataCached() {
// some setup in the "Arrange" phase here, which we'll
// leave out for the sake of simplicity
repository.makeARequestForData()

repository.makeARequestForData()

verify(exactly = 1) { repository.makeHttpRequestToApi() }
}

We found that it’s worth pondering what you’re really testing. It’s also possible that the unit test could be broken into two (or more) tests if you find it’s tempting to have multiple “Act” lines.

AAA with TDD

Assert First

@Test
fun shouldInitialiseSearchUseCase() {
assertThat(searchUseCase has been initialised with location)
}

then thinking about the “Act” phase, you may decide to go with passing in an object to an initialise() method:

@Test
fun shouldInitialiseSearchUseCase() {
searchUseCase.initialise(initialData)

assertThat(searchUseCase has been initialised with location)
}

then finally the setup pseudocode in the “Arrange” phase:

@Test
fun shouldInitialiseSearchUseCase() {
mock fake user location
create initialData object which includes user location

searchUseCase.initialise(initialData)

assertThat(searchUseCase has been initialised with location)
}

Then, start to implement each phase, once again starting with the “Assert” phase, and working back through “Act” to “Arrange”. We found breaking the process of writing tests down into the phases in this way could be helpful. Whether writing with pseudocode or real code.

Guard Asserts

@Test
fun shouldNotMakeSecondCallToApiIfDataCached() {
repository.makeARequestForData()
assertThat(repository.hasData()).isTrue

repository.makeARequestForData()

verify(exactly = 1) { repository.makeHttpRequestToApi() }
}

In our team we tended not to view these too favourably. It can make it harder to quickly eyeball the test, as there appears to be multiple “Assert” phases. Also, our thinking was that as this code is running in a test environment, we should really have control over how the test data is created. If we don’t, then that could indicate other problems in the test code. Perhaps we haven’t mocked a collaborating object correctly. Perhaps we haven’t scheduled tasks to run synchronously.

Of course, if a test crashes at some point due to an unforeseen error, you won’t get as nice an error message as you would with a guard assert. But hopefully your unit tests don’t crash too often. When ours sometimes did, we found the stack trace tended to pinpoint the error anyway.

For our team, a test crashing for an obscure reason was a rare occurrence. So we thought the trade-off of less noise in the tests, by having no guard asserts, was one we were happy to make.

Conclusion

One minor issue was having to explain it to new team members. To overcome that, we incorporated it into our onboarding process for all developers joining the team.

I’m interested in whether your team uses this pattern, and if so, whether it works for you. Let me know in the comments!

I’m a Developer, working in Mobile for the last seven years or so.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store