Software Development

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 Faces, just as a real life die has multiple faces on it. Faces have multiple FaceValues – 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 Dies to always combine Faces into one when there are duplicates (only FaceValues are accounted for when determining if Faces 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:

  1. 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.
  2. 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.
  3. 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 Dies 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:

  1. 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.
  2. 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.
  3. I take an Iterable of Faces instead of any specific sort of collection (or even Collection). 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, combining Faces together. The second reason is that I have my own set of collections that don’t inherit from Collection because Collection 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.

Jacob Zimmerman

Jacob is a certified Java programmer (level 1) and Python enthusiast. He loves to solve large problems with programming and considers himself pretty good at design.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button