An object holds state, and you observe and control it (or both) through its public fields or methods. We’ll look at techniques you can use when an object provides no direct way to observe or control it the way a test wants.
To test-drive, or even just unit-test, we usually want to force an object into a particular state, trigger some mutation, and observe to check that the resulting state is what we expect. (I call this pattern Arrange-Act-Assert.)
We may want to observe state in the object itself, or in related objects.
Easy and Hard Cases
In easy cases, we can easily force the object into the state we want, and the object exposes what we want to observe. The standard array class in Java or C# is like this: you can put things in the array, get its count, and see each element.
Other times, the result of an interaction is hard to observe: what we want to know is not directly revealed. Perhaps the object knows the state, but it’s hidden. For example, a stack provides easy access to its top, but no easy way to check the rest of the stack. Or perhaps the object “exported” the state to other objects connected to it.
Still other times, control is the problem: it’s hard to force the object into the state we want. This may be “just” a matter of visibility, or it may be because the object is affected by randomness, timing, outside events, or other objects.
For example, I ran across code that would go into an infinite loop if an unlikely series of random numbers occurred, but the object didn’t provide an easy way to inject a those numbers to test that.
On another occasion, I wanted to test how a system would respond to a database failure, and it was very hard to force (or even simulate) that condition.
Toolbox: Extending Ways to Observe and Control
Let’s look at several approaches, and the tradeoffs among them.
Pattern: Reveal Internals
An object has the state or controls we want, but they’re private.
*
So, make them public.
This is the easiest way, but often not the best.
Positives
- It gives us the access we want.
- It’s minimal work, just making something public that was private.
Negatives
- This changes the encapsulation of our object, making it harder to change implementation details without affecting other objects.
- The extended interface may make it harder for the object to maintain its invariants.
- This approach doesn’t work if the object doesn’t already explicitly maintain the state we want, or have the controls we want, or if the state is maintained in other objects.
Pattern: Reveal Internals Only to Tests
An object has the state or controls we want, but they’re private.
*
So, make them accessible to tests, but not general clients.
Positives
- It gives us the access we want.
- It doesn’t change the existing public interface.
- It’s minimal work (depending on the approach used).
Negatives
- It weakens encapsulation slightly, as tests now depend on internals.
- It weakens the documentation aspect of the tests, as they no longer match real patterns of use.
- This approach doesn’t work if the object doesn’t already explicitly maintain the state we want, or have the controls we want, or if the state is maintained in other objects.
Here are three patterns to help reveal internals to tests:
Pattern: Tests as Friends
You want to give a test access to non-public fields or methods.
*
So, modify your code to use the language access control keywords that give tests the access they want (friend, package access, @testable, etc.).
This requires minimal edits, but may expose the access to more than just the tests.
Pattern: Implement a Subclass and Test That
You want to give a test access to non-public fields or methods.
*
So, implement a subclass of the target class, and override or implement a helper method.
This lets you test aspects without editing the original. (However, sometimes you’ll need to edit the original to extract a helper method you can override.) You’re no longer testing the original class, so that divergence creates room for defects.
This doesn’t work if the target class is final.
Pattern: Extensions in the Test’s Package
You want to give a test access to non-public fields or methods.
*
So, create an extension of the target object, accessible only in the test’s package.
For languages that let you extend existing classes, this lets you add to or expose a method in an existing class, without editing the original.
Pattern: Talk to the Neighbors
An object affects the state of the system by changing neighboring objects.
*
So, talk to those neighbors to explore the current state.
Positives
- It gives access to the information we need, provided the neighbors will talk:)
Negatives
- The neighbors may have their own visibility and control issues.
- The target object may not give us access to the neighbors (e.g., if it creates them itself).
- The target object may not let us substitute other neighbors.
Again, we have three patterns:
Pattern: Talk Directly to Existing Neighbors
Neighboring objects have the information you want, and they’re accessible to you.
*
So, get the state from them.
Pattern: Apply Other Patterns to the Neighbors
Neighboring objects have the information you want, but it’s hard to get it from them.
*
So, apply “observe and control” patterns to the neighbors so they reveal it.
It may be easy to modify the neighbors, but this doesn’t work if they don’t know or remember the state you want. (Perhaps they don’t retain it, or they tell yet more neighbors.)
Pattern: Inject Test Doubles
Neighboring objects have the information you want, but it’s hard to get from them, and the target object will let you send in substitute neighbors.
*
So, replace the real neighbors with test doubles – fake or mock objects.
Test doubles make it easier to collect the information you want or monitor patterns of communication between objects, but they are somewhat vulnerable to changes in the original neighboring objects. For better or worse, mocks result in more complete isolation of the object you’re testing.
Pattern: Split Apart the Target Object
The target object is complex enough that we want to test its internal state.
*
So, split it into two parts: a facade or simple class that retains the existing public interface, and a new object that lets you test the tricky state more directly.
Positives
- You get access to the state you want to test, or the part you need to control.
- Clients of the existing object aren’t affected.
Negatives
- It may not be clear how best to split the original object to give you the observability and control you want.
- It’s work to split the object.
- You have more objects to use and understand.
Conclusion
Whether you’re test-driving or just testing, there are times you need to be able to observe or control more than is naturally provided. We’ve looked at a number of patterns that can help. They vary in their effectiveness, the amount of work, and their impact on the system.
Further Reading
“3A – Arrange, Act, Assert“, by Bill Wake. Retrieved 2023-07-05.
“Start Here – TDD: Test-Driven Development“, by Bill Wake. Retrieved 2023-07-05.