Testing Objects and Relationships


When working with objects, there are different ways you can make them interact. We’ll take a quick look at a variety of connections, what’s needed to test them, and some alternatives you might consider. 

The connections below are in generally increasing order of complexity, but this is not a simple categorization – a class can use multiple approaches.

I find it helpful to think of value objects as the simpler form. These are types are defined (and compared for equality) by their attributes, and includes types such as like money, position, names, …  As you get outside of value types, and into types that represent entities, you find they tend to collaborate more with other types. 

Pure Function of Values

A pure function produces deterministic outputs for a given set of inputs. 

Pure function: input to outputs

This is the easiest to test: Create the object and its input values, and assess that the output matches the expected value.

You might think this is a special case, but functional programming is typically this plus functions of functions. 

History of Interactions

Some objects are driven by the history of prior calls. Container classes are a prime example of this. (See my articles on containers in the References.) Containers are often generic classes, with minimal requirements on the contained object (perhaps Equatable or Hashable). 

History of interactions: input to outputs and new state

These tests look like this:

Create the object
Call a series of mutator methods to set up a particular context

Call the method whose behavior you’re testing

Call “observer” methods to check the resulting state

Interestingly, this sort of object is one of the things that separate functional and object-oriented programming. Designing functional data types that are both immutable and efficient is a research challenge. (See Okasaki’s book in the References.)

Objects with Collaborators 

When an object collaborates with others, the collaborator could be established through a constructor or setter, or passed into a method. (It could also be a global; we’ll look at that later.)

Collaborators: input to outputs, new state, and potentially new state of collaborators

For basic testing of this, there are two main approaches:

  1. Collaborate with the real object
Set up collaborators
Create the object

Call the method whose behavior you’re testing

Check the result, on the tested object and/or its collaborators

2. Collaborate with test doubles (fake or mock)

Set up collaborator test doubles
Create the object

Call the method whose behavior you’re testing

Check the result, on the tested object and/or its collaborators (test doubles)

Fake vs Mock

When the tested object uses collaborators to provide (or absorb) values, it is a good place for a simple fake object.

In other cases, the behavior we’re testing is the pattern of how the object interacts with its collaborator. In this case a mock is typically used to enforce that calls occur in the expected order. 

Changing the Coupling

This approach to coupling is not the only possible way; it’s a design decision. You could:

  • Replace a supplier with supplied data. (See the article by Mike Hill in the References.) If the collaborator gives you a stream of data, you might be able to pass in the data instead of the collaborator. The data could be in the form of an Array or Stream (etc.); this removes our dependency on the source itself. 
  • Pass in a closure. Instead of an object to talk to, you may be able to pass in a function reference (closure) and call through that. The closure may be easier to replace for testing, and you’ve simplified the dependency.
  • Apply other patterns: James Shore (see References) has a whole pattern language on working without mocks. 

Objects with Global Values

If a class depends on global variables (including static variables), it becomes trickier to understand and test. To test these, we set up the global object as well as the object to be tested:

Set up the global value
Create the object
Call any methods needed to get it into the right state

Call the method whose behavior you’re testing

Call “observer” methods to verify the object’s state is as expected
If needed, call observer methods on the global object you used
Global value: input to output, new global value state, new state of object

The problem with global state isn’t so much “Does it act as we expect when everything is set up right?” but rather “Is the global state set up right when we want to use it?”

Reducing Complexity

We can restructure to reduce the use of globals. One way is to make it a parameter, passing it in to anything that uses it. This turns an implicit dependency into an explicit one. 

Objects with Global Collaborators

Much like working with global or static values, the test will need to set up the global object that it collaborates with.

Global collaborators: input to new global state, new state of object, and new state of collaborators

As with the earlier global value type, the challenge is that a global could change “behind our back.”

[Added 2021-06-24] It’s not hard to reach this level of complexity. For example, you might have a purchasing application – the global is the system log used for audits (directly accessed by your class); the output is a receipt and tracking code; and the collaborators are a price sheet (to look up the cost of items), and a third-party service that requires the conversation to follow a certain protocol.

To address the design issue, you can either pass in the global, or use one of the other mock-removal techniques. 

Conclusion

The connections we make between objects affect how they’re tested and how complex they are. We looked at a few varieties of connection types, and some ways to simplify the more complex ones. 

Notice how many side effects we trigger if we use the more complex forms – and side effects are not our friend!

We aren’t necessarily trying to make every object of the simplest form, but rather to balance out our considerations, to lower our complexity and increase our ability to change code. 

References

Hill, Mike. “Use Supplier or Supplied or Both?” https://www.geepawhill.org/2018/03/31/use-supplier-or-supplied-or-both/ – Retrieved 2021-06-21.

Okasaki, Chris. Purely Functional Data Structures. Cambridge University Press, 1999. 

Shore, James. “Testing Without Mocks.” https://www.jamesshore.com/v2/blog/2018/testing-without-mocks – Retrieved 2021-06-21.

Wake, Bill. “List-Like Objects – TDD Patterns.” https://xp123.com/articles/list-like-objects-tdd-patterns/ – Retrieved 2021-06-21.

Wake, Bill. “Set-Like Objects – TDD Patterns.” https://xp123.com/articles/set-like-objects-tdd-patterns/ – Retrieved 2021-06-21.

Wake, Bill. “Tree-Like Objects – TDD Patterns.” https://xp123.com/articles/tree-like-objects-tdd-patterns/ – Retrieved 2021-06-21.