AAA and the Object Lifecycle

We’ll look at how Arrange-Act-Assert (AAA) tests reflect the lifecycle of the objects we’re working with.

Object Lifecycle

An object has a basic lifecycle:

create (command | query)* (abandon | delete)

That is, create the object, do a series of commands or queries, then abandon the object, or delete it if your language requires that. (I’m not going to mention abandon or delete further, but do what ya gotta do.)

We’ll assume you follow “command-query separation”: your methods act as either a command or query, but not both. There are situations where you can’t avoid blending them, as for test-and-set to assist with concurrency; you’ll have to adjust your analysis in those cases.

For a particular object, “command” and “query” represent any of a number a methods. For example, a stack will have push() and pop() for commands, and isEmpty and top for queries. 

Arrange-Act-Assert

AAA is a guideline suggesting a good structure for Test-Driven-Development (TDD) tests:

Arrange – put one or more objects into a known situation
Act – trigger one command
Assert – check that the objects are as you expect

AAA tests usually come in one of these two variants:

  1. Constructor Test
create — Arrange/Act
query+ — Assert

2. Command Test (most common)

create     } — Arrange
act+ }

act — Act

query+ —Assert

There’s a third variant that’s “almost” AAA:

x. Setup Test (avoid this)

create     } — Arrange
act+     }

query+ — Verify setup

act — Act

query+ — Assert

Why “almost” AAA? Because it’s got the extra queries in there – a step on the road to a run-on test.

You can transform this to true AAA style by breaking it into two tests. The first only goes as far as the setup verification. The second is like the original but skips setup verification.

Note that setup that’s complicated enough to worry about is a test smell – is there a way to simplify things?

The Tie-In

AAA is a subset of the object lifecycle.

Let’s compare:
create act* query+ — AAA
vs.
create (act | query)+ — Object lifecycle 

Basically, our tests run a series of actions, followed by assertions (using queries). Full use of objects doesn’t stop there – it usually goes on to do other actions and queries.

The Infinite Test Problem

Testing has a challenge: there’s an infinite number of sequences we could test. But who has time for that? And that doesn’t even consider all the possible orders of tests.

So, we have to choose a finite set of action sequences that we believe will be revealing enough.

That’s where one of the creative parts of TDD comes in. You identify various states of the object, partitions of possible inputs or outputs, or whatever other techniques you find helpful. 

Examples:

  •  A list doesn’t care what values are in it. But it needs to distinguish empty from non-empty lists.
  • A set acts differently for identical vs. unique values. It also cares about being empty.
  • A list zip operation turns a pair of lists into a list of pairs. We want to handle empty lists, lists of the same length, and lists of different lists.
  • A tile that can rotate 90 degrees can represent 1, 2, or 4 shapes, depending how symmetric it is. Rotating four times should take you back to the original shape. 

Interacting Objects

Not all objects are self-contained. An object that needs other objects may use “real” objects or test doubles. These objects require setup – their creation and perhaps initial actions.

The overall test form remains the same:

Arrange – set up  the main and related objects (creation and actions)
Act – trigger one command on the main object
Assert – assert on the main and related objects

Hidden State

Sometimes, a test needs to verify some internal state or aspect that you don’t want to expose to its clients.

You have two ways to do this: One way is to compromise the encapsulation – ideally for tests only. Some languages let you add extensions that are only visible to the tests.

A second alternative is to split the object. Clients still use the facade part, but you can test the extracted object directly. 

A caching collection can be split into a cache and collection being separate objects. Clients may use the cache as a facade, but the test has access to both objects.

For example, suppose you have a “caching collection”. From a behavioral perspective, a client doesn’t care whether a cached value is returned. But a test might like to verify that the caching is really happening. 

Conclusion

We looked at the generic lifecycle of objects:
create (command | query)* (abandon | delete)
and matched it against Arrange-Act-Assert:
create command* query+

We also looked at a couple challenges:

  • the need for a strategy for choosing sequences of actions in the face of infinite testing possibilities
  • the fact that some tests may need hidden information

The Arrange-Act-Assert guideline creates small, black-box style tests. (Though they need not be truly black-box.) The approach is grounded in a high-level view of the lifecycle of objects.

Further Reading

Wake, Bill. “3A – Arrange, Act, Assert”. https://xp123.com/3a-arrange-act-assert/; retrieved 2026-02-14.