23 December 2016

Unit Testing Interfaces to Ensure They Don't Get Changed

How to unit test an interface to make certain that it does not get changed


When and why Interface invariance matters

Agile principles teach us that program code should rely on and hold references to abstractions. In C#, this often means declaring a field, a property, an argument or a return type as an interface.

Agile also teaches us when building packages and multi-tier applications to let the client/consumer dictate the interface (logic-to-interface in SOA terms).

If an interface is only consumed within a single application, invariance isn't such a big concern. When interfaces are used by other applications or other packages, however, we must consider them as "published" and treat them as unchanging contracts (see Martin Fowler's article in IEEE Software March/April 2002 for more on this at http://martinfowler.com/ieeeSoftware/published.pdf).

It is important to note that not all interfaces need to be invariant.

Unit Testing for Interface Invariance

Using NUnit's ability to run the same suite of tests on multiple types via its TestFixture with Type and constructor parameters, it is fairly straight-forward to construct a unit test that ensures that an interface only has certain properties and methods.

Our approach will still be the traditional "Arrange/Act/Assert" unit testing pattern, but to eliminate repetitive code, the arrange and act steps will happen in the constructor for the test fixture.

This yields a test fixture ctor with a signature like the following:


There are many ways to approach interface testing. The approach that we favor is to simply test the signatures of properties and methods, optionally ignoring "Special Name" methods, which excludes "get" and "set" methods that properties generate behind the scenes. If you need to test for a read-only property, simply set the constructor parameter ignoreSpecialNames to false.

Arrange & Act


Arrange

We are going to perform the "arrange" part of the unit tests by using NUnit's injection feature via the TestFixture.

To accomplish this, first declare the test fixture class like this:


public class InterfaceContractTests<T> : AssertionHelper where T : class

Next, add test fixture attributes similar to the following (the first argument sets the type T; the remaining are the constructor arguments):

  • Testing for just a method

    [TestFixture(
      typeof(IOutput),
      new string[] { "Void Write(System.String)" },
      new string[] { },
      true,
      null)]

  • Testing for properties, get/set and inheritance

    [TestFixture(
      typeof(IColorOutput),
      new string[]
      {
        "System.String get_Color()",
        "Void set_Color(System.String)",
        "System.String get_BackgroundColor()"
      },
      new string[]
      {
        "System.String Color",
        "System.String BackgroundColor"
      },
      false,
      typeof(IOutput))]

Act

Our code needs to test each property and method to ensure that it is declared by the type we are testing. This is done by checking the DeclaringType property of the MethodInfo and PropertyInfo objects that are returned when our code calls the methods to get the public properties and public methods of the interface.

If our tests are not checking for read-only properties, we can exclude the "get" and "set" methods by testing if the IsSpecialName property of the MethodInfo object is true.

The code for getting the actual method and property signatures is shown below.


Full Constructor Code

Below is the full code for the resulting constructor.


Assert

There are four tests that need to be run for each interface:

  • Verify that we're testing an interface,
  • Verify that the actual method signatures match expectations,
  • Verify that the actual property signatures match expectations, and
  • Verify that the interface does or does not extend another interface.

Below is the code that implements these tests.


Conclusion

When interfaces are used by independent components or clients, they should be considered to be "published" and invariant.

Interfaces that are invariant should have unit tests that ensure that they do not change and break the published contract.

This article shows an easy-to-use and repeatable testing approach that ensures interface invariance. If you add it to your suite of tests and update it as new published interfaces are authored, you will reduce your risk of bugs and broken code.

No comments: