How to test a class hierarchy without repeating yourself

You don’t want to spend your effort on futile tasks. And testing all methods of a base class and then testing them again for every descendant sounds futile.

After all, non virtual methods are not going to change in a descendant, so, really, testing the base should be enough? And what’s the use of testing the virtual methods of an ancestor when they are going to be overridden by the descendants anyway?

As non virtual methods can’t be overridden by descendants, why run the ancestor’s tests for all descendant classes as well?

But, what about non-virtual methods that call out to a virtual method? Shouldn’t they be tested to evaluate the effect of overriding the virtual method? Adhering to the Liskov substitution principle means that a descendant’s behavior should not contradict its ancestor’s behavior contract. So while you can override methods of your base class, you can’t do so willy-nilly.

It is in fact a very good idea to run the ancestor’s tests for all descendants to ensure that none of them violate their base’s behavioral contract. And while you perhaps would only really have to do so for virtual methods and any non-virtual methods that use a virtual one, penny pinching by not testing non-virtual methods for each descendant may come and bite you in the behind pretty quickly.

After all, what’s to keep someone from adding such a call at a later stage? And what are the chances of forgetting to add the unit tests that are now needed for the descendant classes? They after all haven’t been changed, so there is no trigger to add to their tests. Forgetting to add them at that stage means your safety net’s holes just got bigger.

Wouldn’t it be far more preferable to just always test all public and protected methods, regardless of their “virtual” status, so that when such a change is made no manual action is required to maintain your safety net?

Yes, of course, but … YOU DO NOT WANT TO REPEAT YOURSELF.

There is no need to do so.

Test classes are first class citizens too. They too can put inheritance to good use.

To ensure that the tests for the base class are run for all descendants and that you only need to add tests for a descendant that are specific to that descendant, you create a parallel hierarchy of test classes.

Too abstract?

Okay, let’s get specific and have some fun with one of my hobbies.

For 10+ years I was a hobby solid wood furniture maker. Dovetail joints were my nemesis. They are daunting at first and you follow the rules to the T just to keep on top of them. As your skills improve with more practice, you free yourself more and more from the rigidity of the rules and start playing around with the dimensions of the tails and pins. However, even as a master craftsman (which I am anything but) you still have to conform to the basics.

DoveTails HalfBlindDoveTails MarkingOutDoveTails SingleDoveTail

An apprentice and a master dovetail jointer may have very individual approaches to deciding how many tails there will be, how wide they will be and how they will be spaced. Yet both still have to adhere to the basic rules for dovetails though. If that doesn’t sound like a base class with two derived classes to you, it certainly does to me.

Jointer Class Diagram - Actual Classes

To test this tiny hierarchy, you create a parallel hierarchy of test classes.

Jointer Class Diagram - Tests Classes

The BaseJointer_Tests class get the tests for all behavior that any Jointer should provide.
The ApprenticeJointer_Tests and MasterJointer_Tests classes both derive from the BaseJointer_Tests class, thus inheriting all the tests for general jointer behavior, and each add their own tests to verify the behavior specific to themselves.

Still too abstract?

Let’s add some code then. First the BaseJointer class. A pretty simple thing. It has a non-virtual method to make a dovetail joint for joining two pieces of wood. It has a couple of virtual methods that allow derived classes to customize how the dovetail joint will look.

    class BaseJointer
    {
        public DoveTailJoint MakeDoveTailJoint(PieceOfWood woodTails, PieceOfWood woodPins)
        {
            int numberTails = DecideNumberOfTails(woodTails, woodPins);
            int tailWidth = DecideTailWidth(woodTails, numberTails);
            int tailLength = DecideTailLength(woodTails, woodPins);

            return new DoveTailJoint(numberTails, tailWidth, tailLength, woodTails.Width);
        }

        protected virtual int DecideNumberOfTails(PieceOfWood woodTails, PieceOfWood woodPins)
        {
            return 1;
        }

        protected virtual int DecideTailWidth(PieceOfWood woodTails, int numberTails)
        {
            return 1;
        }

        protected virtual int DecideTailLength(PieceOfWood woodTails, PieceOfWood woodPins)
        {
            return 1;
        }
    }

The rules that MakeDoveTailJoint needs to follow, its requirements if you like, are:

  • Should throw ArgumentNullException when woodTails is unassigned
  • Should set ParamName of ArgumentNullException to woodTails when woodTails is unassigned
  • Should throw ArgumentNullException when woodPins is unassigned
  • Should set ParamName of ArgumentNullException to woodPins when woodPins is unassigned
  • Should return non-null reference to a DoveTailJoint when no exceptions are thrown
  • Should return DoveTailJoint with at least 1 tail
  • Should return DoveTailJoint with non-zero TailWidth
  • Should return DoveTailJoint with non-zero TailLength
  • Should return DoveTailJoint with non-zero FullPinWidth
  • Should return DoveTailJoint with non-zero SidePinWidth
  • Should return DoveTailJoint that fits woodTails' width: ((number of tails) * TailWidth) + (((number of tails) – 1) * FullPinWidth) + (2 * SidePinWidth)

The current implementation of BaseJointer is not meeting these rules of course, but that is beside the point of this post. These rules, requirements, become the tests in the BaseJointer_Tests class.

    [TestClass]
    public class BaseJointer_Tests
    {
        [TestMethod]
        public void MakeDoveTailJoint_woodTails_Unassigned_Should_Throw_ArgumentNullException() { }

        [TestMethod]
        public void MakeDoveTailJoint_woodTails_Unassigned_Should_Set_ParamName_Of_ArgumentNullException() { }

        [TestMethod]
        public void MakeDoveTailJoint_woodPins_Unassigned_Should_Throw_ArgumentNullException() { }

        [TestMethod]
        public void MakeDoveTailJoint_woodPins_Unassigned_Should_Set_ParamName_Of_ArgumentNullException() { }

        [TestMethod]
        public void MakeDoveTailJoint_No_Exceptions_Raised_Should_Return_Non_Null_DoveTailJoint() { }

        [TestMethod]
        public void MakeDoveTailJoint_Should_Return_DoveTailJoint_With_At_Least_1_Tail() { }

        [TestMethod]
        public void MakeDoveTailJoint_Should_Return_DoveTailJoint_With_Non_Zero_TailWidth() { }

        [TestMethod]
        public void MakeDoveTailJoint_Should_Return_DoveTailJoint_With_Non_Zero_TailLength() { }

        [TestMethod]
        public void MakeDoveTailJoint_Should_Return_DoveTailJoint_With_Non_Zero_FullPinWidth() { }

        [TestMethod]
        public void MakeDoveTailJoint_Should_Return_DoveTailJoint_With_Non_Zero_SidePinWidth() { }

        [TestMethod]
        public void MakeDoveTailJoint_Should_Return_DoveTailJoint_That_Fits_woodTails_Width() { }
    }

According to TDD you should write one test, make it pass, refactor and then write another test, but I just can’t work that way. It makes me lose sight of the overall picture. I like to use what is known as a “test list”. TDD’ist would probably keep that list outside of the code and there are certainly arguments to do so. Me, I just keep ’em in my code as empty test methods.

Currently with most test frameworks, I am forced to use `[Ignore]` attributes or `Assert.Inconclusive` calls to have them not show as failing, while still serving as “To Do”‘s. I do wish frameworks would mark any empty test method as “Ignore – to be implemented” automatically. In fact I would love if they did so automatically as well for test methods that are not empty but do not call a single `Assert` member.

Have any suggestions for me on how to achieve what I want easily? Please let me know!

Every descendant of BaseJointer should be tested against these same rules, without repeating them in each test class companion of every descendant. As mentioned above, the solution is to derive these test classes from the BaseJointer_Tests class:

    [TestClass]
    public class ApprenticeJointer_Tests : BaseJointer_Tests
    {
        [TestMethod]
        public void Extra_Test_Specifically_For_ApprenticeJointer()
        {
            // ... snip ...
        }
    }

When you do that for the MasterJointer_Tests class as well, then running the tests would show that all tests in the BaseJointer_Tests class are run also for both the ApprenticeJointer_Tests and MasterJointer_Tests. Also notice the extra test defined for each derived test class.

JointerTestExplorer
Note: I commented out most of the tests in the BaseJointer_Tests class for brevity.

Nice ha? You are testing everything without repeating yourself!

Now, to the tiny matter of implementing a test in the BaseJointer_Tests class. The naive way would be to do something like:

    [TestMethod]
    public void MakeDoveTailJoint_woodTails_Unassigned_Should_Throw_ArgumentNullException()
    {
        var jointer = new BaseJointer();
        jointer.MakeDoveTailJoint(null, new PieceOfWood());
    }

But that foils your goal of testing descendants of BaseJointer against the same set of rules. The inheritance we set up isn’t going to help you if the test instance you create is always an instance of BaseJointer. You need it to be a BaseJointer only when running the tests for the BaseJointer class. When you run the tests for ApprenticeJointer or MasterJointer you need the object under test to be an instance of that class, even when running the tests in BaseJointer_Tests.

The answer is to use a virtual “factory” method:

    protected virtual BaseJointer MakeJointer()
    {
        return new BaseJointer();
    }

and use that in your tests when you need a Jointer instance:

    [TestMethod]
    public void MakeDoveTailJoint_No_Exceptions_Raised_Should_Return_Non_Null_DoveTailJoint()
    {
        var jointer = MakeJointer();
        var DoveTailJoint = jointer.MakeDoveTailJoint(new PieceOfWood(), new PieceOfWood());
        Assert.IsNotNull(DoveTailJoint);
    }

Now when you run the tests, the object under test always matches the class being tested.

And every descendant of BaseJointer_Tests can use the instance MakeJointer() even for tests added specifically to exercise their own class under test. Thus putting the ability to substitute the base for any derived class to good use.

    [TestMethod]
    public void Extra_Test_Specifically_For_MasterJointer()
    {
        var jointer = MakeJointer();
        jointer.MakeDoveTailJoint(new PieceOfWood(), new PieceOfWood());
        // ... any asserts you need
    }

The only time you would need to cast the instance to the specific class is when testing methods that only appear in a derived class. And you don’t even have to do that. You could also add another factory method to the derived test class to return a reference to the exact class:

    protected ApprenticeJointer MakeApprenticeJointer()
    {
        return new ApprenticeJointer();
    }

You can make this one virtual as well if there are classes deriving from ApprenticeJointer

    [TestMethod]
    public void Extra_Test_For_Method_Specific_To_ApprenticeJointer()
    {
        var jointer = MakeApprenticeJointer();
        jointer.DoSomethingSpecificToApprentices();
        // ... any asserts you need
    }

That’s it. Enjoy!

What piece of code had you wondering how to get it under test? Please do feel free to let me know! I’d love hearing from you by email or in the comments below. I read everything and will try to help where and as best I can.

Posted in Unit Testing
Tags: , , , , , , ,

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Show Buttons
Hide Buttons