In Test-Driven Development (TDD), it’s hard to know what the next test should be – even if you know about Zero-One-Many or ZOMBIES. We’ll look at the challenge of observability – understanding an object, and we’ll look at a couple basic tests.
Observability
Observability is a problem – how can you determine the state of an object?
You can divide an object’s methods into three categories:
- Observers – compute a read-only function of the object (and its collaborators)
- Mutators – change the object
- Other – e.g., combination mutator-observers, iterators, …
A key challenge is: Can my observer methods give me enough confidence that the object is working as intended?
For example, an ArrayList has very transparent observers: you can easily get the count and the element at any position. A stack has less visibility: the count and only the top element. A cache is even less transparent: you can ask for the item associated with a key, and may or may not get it.
We have a tradeoff between encapsulation – which hides information – and observability – which reveals information.
In creating code, we control how much representation is exposed. But the more we expose, the more fragile the class is. That is, its clients may have to change whenever it changes. (This reduces the benefit of programming to the class’ interface.)
Constructor Test – A Starter Test
One of the most common and natural early tests is to construct an object and verify its initial state. (This may correspond to the zero or one of Zero-One-Many.) Gerard Meszaros mentions this test in xUnit Test Patterns.
This test is popular because it helps us make some key decisions:
- How do we create the object?
- What are the most important things we need to observe it?
- How do we check those things?
It’s easier to make those decisions with a newly initialized object, without being bogged down by considering the effects of a bunch of actions.
For TDD tests, I try to restrict myself to the public interface of the tested class. This makes the test more resilient to changes.
If all the class is doing is storing some data I gave it in the constructor, I may not bother with this test – later tests will almost certainly catch any issues. I did a rough count in a recent project; I use this test style about half to two-thirds of the time.
Class Invariants – A Test Helper
Many classes have a class invariant (usually implicit): conditions that start true when the object is constructed, and remain true after every call to a public method. Knowing this invariant is especially helpful for objects that maintain relationships between other objects.
To build our confidence, it’s helpful if we can check the invariant in the tests. Consider three situations:
- The invariant is defined in terms of public observers
- The invariant is defined in terms of the object’s internals (and optionally public observers)
- The invariant is defined in terms of the history of the object, or other conceptual values (as well as the earlier cases)
Example – Case 1: Public Observers
A stack has the invariant count >= 0
. A test can check this before or after calling any mutator.
We’ll assume the test defines a checkInvariant()
helper method for the tested class (or it could be a “test-only” method on that class). The test now looks like:
Arrange - set up the test Act - trigger a behavior Assert - query object with regard to the behavior checkInvariant()
Example – Case 2: Internals
When we can only verify the invariant using the class’ internals, we face a dilemma about encapsulation: should the class reveal enough internal information so that callers can check the invariant? Or should the class provide its own checkInvariant()
method? (Either way, we prefer this is only visible to tests).
For example, the data viewer I’ve been working on has a cache that uses both a hash table and a doubly-linked list to implement the Least Recently Used replacement policy. The fact that these structures exist is an implementation decision, not visible to its clients. One invariant is: hashtable.count == linkedlist.count
.
Another example from that cache is about its doubly-linked list. If the list is properly connected, it should be true that: the sequence of values seen traversing backward ("prev") should be the reverse of the sequence of values seen traversing forward ("next")
.
When I’d “finished” implementing it, I got suspicious that it was wrong even though the tests passed. Adding a test for the invariant revealed that my suspicions were correct. I threw away that implementation and kept the invariant, then I re-implemented the code using a better data structure with a header node.
(This reminds me, again – TDD isn’t perfect – and weak tests or a weak implementation can still bring you down.)
Example – Case 3 – History
With conceptual values, we’re deriving information from the history of the interactions with the class. Consider another stack invariant:
if no errors occur, then stack.count() = # pushes - # pops
Our stack could count these values, but it’s not needed for the behaviors we’re providing.
We have a few choices (and if I can find three, there are probably more):
A. Make the underlying class record the history. I typically don’t take this approach, but might if the history provided some benefit to the implementation.
B. Use a wrapper class for testing purposes. It would count the calls, then delegate to the underlying class being tested. (You may have a mocking package that can “spy” to do this.)
C. Use inductive reasoning to apply the invariant to the test.
Let me demonstrate the last option:
s = stack() s.push(1) s.push(2) s.pop() s.push(4) assertEquals(2, s.count) // 2 = #pushes - #pops
By induction, if empty stacks meet the invariant, and stacks formed by other mutators do so as well, we trust all stacks to meet it. (See the article on sufficient completeness for some ideas about this.)
I’ve typically used the invariant idea informally in a few tests, rather than systematically. I think I’ll be reaching for this tool more often. Furthermore, a class invariant is a bridge to property-based testing (that being one property you might check).
Conclusion
One consideration and two TDD patterns:
Consideration: There’s a tradeoff between encapsulation and observability; find a compromise that works for you.
Pattern 1: Constructor Test – “Arrange” by calling the constructor, skip the “Act”, and “Assert” about the initial state.
Pattern 2: Class Invariants – check class invariants after public method calls for “interesting” (tricky:) classes.
References
“Class invariant”, https://en.wikipedia.org/wiki/Class_invariant – retrieved 2020-12-14.
“Property based testing”, by Pierre Felgines. https://felginep.github.io/2019-03-20/property-based-testing – retrieved 2020-12-14
“Sufficient Completeness and TDD”, by Bill Wake. https://xp123.com/articles/sufficient-completeness-and-tdd/
“TDD Guided by ZOMBIES”, by James Grenning. http://blog.wingman-sw.com/tdd-guided-by-zombies – retrieved 2020-12-14.
xUnit Test Patterns, by Gerard Meszaros.
“Zero-One-Many in Test-Driven Development”, by Bill Wake. https://xp123.com/articles/zero-one-many-in-tdd/