Testing Interface Invariants
Today’s article is something a little special. It’s the first article where I use code from my current personal project for examples. You will be getting “real world” examples and not silly, made-up examples like my Scientist and Pen example in my factories article.
My Project
Because of this, I’m going to make a quick introduction to what the project is. I call it the UDice System. It’s a collection of classes and interfaces to make a universal dice roller that supports any and every time of die and die-rolling mechanic out there. Most of the specialness comes from the design of the die hierarchy, but some of the extra cool functionality will require special Roller
classes.
You can read more about it on the project’s site, which is sadly suffering from lack of updates, but it has the basics needed to explain the system. I’d link to the actual github repo, but it’s not ready for public consumption yet :)
The Example
We’ll be looking at an upper level example from the library, since then you won’t have to read about the dice model in order to understand what we’re looking at.
Our top-level interface is Die
(singular of dice; not a murderous suggestions), which contains multiple Face
s, just as a real life die has multiple faces on it. Face
s have multiple FaceValue
s – which aren’t important for you to understand here; you just need to know that they exist – and a weight, which adjusts how frequently the Face
is rolled. Weight serves a double purpose of making “cheater” dice, which makes certain faces come up more often than they should, and to represent having the same face multiple times on the die.
I decided that I wanted Die
s to always combine Face
s into one when there are duplicates (only FaceValue
s are accounted for when determining if Face
s are duplicates of each other), creating a new Face
with the combined weight of the original two. But Die
is an interface, which can’t control that. Interfaces have default methods for doing some stuff like that, but those can be overridden and don’t help in the creation process of an object.
Abstract Tests
The best we can do is to create a set of tests that make sure these invariants are followed. Here’s an outline of some of those tests:
abstract public class AbstractDieTest { /** * Creating a Die combines Faces with the same set of FaceValues into a * single Face with the same set of FaceValues and a combined weight of the * two original Faces */ @Test public void creation1() { fail("test not implemented"); } /** * Creating a Die does not combine Faces that have different sets of * FaceValues into a single Face */ @Test public void creation2() { fail("test not implemented"); } }
Let me note a few things:
- The class is abstract; since the test specifies no specific implementation of
Die
, and it’s meant for testing all implementations, it cannot be directly instantiated. - There isn’t just a test for checking that it combines similar faces, but also that it doesn’t combine dissimilar faces. The biggest reason I split this into two was to make the individual tests simpler.
- The names and comments are probably different than what you’ve seen before. This is a personal preference of mine that I talked about in an old post. It keeps method names easy to read, allows you to fully write out what the test does via comments that allow you to use full punctuation, and can help find tests via their names more easily.
A Small Manufacturing Building
The first step in a test is the assembly step. So, we need to make some Die
s to test. How do we do that? We don’t know how to make the specific implementations Die
; this is just the interface test. The solution is a Factory, specifically a Factory Method. So let’s make that shall we?
abstract protected Die createTestDie(String name, Iterable faces);
More noting of things:
- The method is abstract. I would hope this would be obvious, but I felt I should point it out just in case. Tests that inherit from this class will be the ones providing the implementation.
- The method is protected. There is no reason for any classes to know about this method, other than the subclasses, and they only need to know about it in order to instantiate it for this class’ tests.
- I take an
Iterable
ofFace
s instead of any specific sort of collection (or evenCollection
). The first reason for this is that the only thing we need the “collection” for is to iterate over it, since we may need to change the collection, combiningFace
s together. The second reason is that I have my own set of collections that don’t inherit fromCollection
becauseCollection
forces way too many methods onto you, and I want my collections to be immutable, which means all the optional methods would be unimplemented (which is annoying).
From here, we simply call the factory method from within our tests to build what we need, and write the rest of our tests.
Documentation
The last step is to document the abstract test class and the interface, saying that when someone writes an implementation of the interface with tests, its tests should inherit from your abstract test class.
Outro
I’m a firm believer that you should test these types of invariants if your interfaces (the same idea applies to abstract classes too) have them. There’s plenty of resistance out there against using inheritance within tests, and I totally agree with it. I also happen to think that this is an exception to that. If disagree, then you can use composition over inheritance with some tweaking, but that requires more work from the implementation test, is less automatic, and is more error prone.
Reference: | Testing Interface Invariants from our JCG partner Jacob Zimmerman at the Programming Ideas With Jake blog. |