Observe and Control Objects – TDD Patterns

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

Make an element public instead of private

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

Make elements have package or friend access instead of private

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

Subclass the original object, modify it, 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

Add an extension to a class, visible to the test

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

Let the test talk to the neighboring classes as well as the class under test

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

If it's hard to talk to a neighbor class, apply observe-and-control patterns to it such as making something public

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

Replace a neighbor with a test double such as a mock object

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

Split the class apart so the parts that are hard to observe or control are now more visible in another class. Test both the original and the new class.

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.