Shared behaviours in PHPUnit with Traits

One of my favourite features from RSpec is Shared Behaviours - these allow you to include a standard set of tests against a bunch of different Classes.

If you consider an Interface declaration in PHP, it ensures that the implementing class matches the method signatures defined. However, an Interface also comes with some implicitly expected behaviours associated with these methods. We can document what this behaviour is supposed to be - but using a shared-behaviour-like approach we can assert it programmatically in our test suite.

This same logic can also be applied to Abstract classes and Traits. I'm personally not a fan of testing an abstract class by having a Mock object inherit from it, because this isn't how it's actually used in the real system. The fact that a particular class inherits from an Abstract is actually an implementation detail, the only part we care about is that it exhibits the behaviour required.

You can think of a Shared Behaviour as a runtime Interface declaration. An Interface statically states "This class implements this set of functionality", a Shared Behaviour proves "This class implements this set of functionality".

Example

The example we're using is some standard behaviour that we'll use across all of our collection classes. These collections receive their data as an associative array via a JSON API. The API also provides an array of IDs in the order that the collection should be iterated over - this is required because JSON doesn't guarantee the order of elements in its collection type. The collection classes handle converting to appropriate model classes, in addition to iterating in the correct order.

If this was an interface, it would look something like this:

The Plan

  • Write tests for a single concrete implementation of this interface
  • Implement a class which implements the interface and passes the tests
  • Move the standard behaviour specified in the test into a shared example group using a trait
  • Implement a second class which implements the interface by creating a default implementation using an abstract base class

Before abstraction...

We wrap up some of the instance creation boilerplate with some protected methods, but otherwise a pretty ordinary unit test.

If you like, you can read the implementation of this class, but there's not much to it.

Nothing particularly unusual so far, now we'll look at how we can reduce code and test duplication when we have more than one collection class.

Traits to the rescue!

To give you a taster, here's the two test classes after we've moved their shared behaviour into the trait. The second list class works a little differently, but still exposes the same interface. It also includes a higher-level iteration method specific to its own use-cases.

And now for the meat, the trait itself that makes this all work.

Note that this is almost identical to the single test version, but we've extracted the implementor-specific bits into method that the real test class can fill in later.

You can see the full code example, including the abstracted implementations of AppleList and AddressList in this gist.

In Summary

Hopefully the example I've chosen was suitable for conveying the idea. Rather than artificially testing an abstract class, we can structure tests to test the behaviour exhibited. The fact we're using an abstract class to share code becomes an implementation detail, rather than part of the interface contract. In my book, decoupling behaviour from implementation is always a good thing!

This approach should be applicable in any scenario where you have a number of classes exhibiting the same behaviour, regardless of whether they achieve this through inheritance, composition, or even copy-paste!

Thanks to Craig for running into the problem in the first place, as well as doing most of the implementation while I yelled suggestions over his shoulder.

Further Reading