8

I am writing a series of collection classes in C#, each of which implement similar custom interfaces. Is it possible to write a single collection of unit tests for an interface, and automatically run them all on several different implementations? I would like to avoid any duplicated testing code for each implementation.

I'm willing to look into any framework (NUnit, etc.) or Visual Studio extension to accomplish this.


For those looking to do the same, I posted my concrete solution, based off of avandeursen's accepted solution, as an answer.

Community
  • 1
  • 1
dlras2
  • 8,416
  • 7
  • 51
  • 90
  • 1
    Added [lsp](http://stackoverflow.com/questions/tagged/lsp) as tag, since the question & answer apply to any class hierarchy adhering to LSP. – avandeursen Feb 21 '12 at 06:52

4 Answers4

7

Yes, that is possible. The trick is to let your unit class test hierarchy follow the class hierarchy of your code.

Let's assume you have an interface Itf with implementing classes C1 and C2.

You first create a test class for Itf (ItfTest). To actually exercise the test, you need to create a mock implementation of your Itf interface.

All tests in this ItfTest should pass on any implementation of Itf (!). If not, your implementation does not conform to the Liskov Substitution Principle (the "L" in Martin's SOLID principles of OO design)

Thus, to create a test case for C1, your C1Test class can extend ItfTest. Your extension should replace the mock object creation with the creation of a C1 object (injecting it in, or using a GoF factory method). In this way, all ItfTest cases are applied to instances of type C1. Furthermore, your C1Test class can contain additional test cases specific to C1.

Likewise for C2. And you can repeat the trick for deeper nested classes and interfaces.

References: Binder's Polymorphic Server Test pattern, and McGregor's PACT -- Parallel Architecture for Component Testing.

Community
  • 1
  • 1
avandeursen
  • 8,458
  • 3
  • 41
  • 51
  • 1
    To avoid mock implementations, I simply declared my `ItfTest` class as abstract and declared a `protected abstract Itf CreateInstance();` function stub. (Note that both `ItfTest` and `C1Test` need to have the `[TestClass]` attribute.) – dlras2 Feb 20 '12 at 22:37
  • 1
    Yes, I also used the factory method for that since it is simpler. I used this test approach in JUnit, where there is no `[TestClass]` attribute, but where `@Test` annotations in superclasses are inherited causing superclass test cases to be re-run in subclasses. And: thanks for the edit. – avandeursen Feb 21 '12 at 06:47
3

This is my concrete implementation based off of avandeursen's answer:

[TestClass]
public abstract class IMyInterfaceTests
{
    protected abstract IMyInterface CreateInstance();

    [TestMethod]
    public void SomeTest()
    {
        IMyInterface instance = CreateInstance();
        // Run the test
    }
}

Each interface implementation then defines the following test class:

[TestClass]
public class MyImplementationTests : IMyInterfaceTests
{
    protected override IMyInterface CreateInstance()
    {
        return new MyImplementation();
    }
}

SomeTest is run once for each concrete TestClass derived from IMyInterfaceTests. By using an abstract base class, I avoid the need for any mock implementations. Be sure to add TestClassAttribute to both classes or this won't work. Lastly, you can add any implementation-specific tests (such as constructors) to the child class if desired.

Community
  • 1
  • 1
dlras2
  • 8,416
  • 7
  • 51
  • 90
  • are you using a particular test framework? I'm using the included unit tests in VS2012, and inherited tests from another "shared" test project (namespace) don't get picked up. – drzaus Apr 30 '13 at 17:06
  • weird -- inherited tests within the same project, different namespace show up fine. it's only when they're in a different project that they aren't registering. what am i missing? – drzaus Apr 30 '13 at 19:50
2

You can use the [RowTest] attributes in MBUnit to do this. The example below shows where you pass the method a string to indicate which interface implementation class you want to instantiate, and then creates this class via reflection:

[RowTest]
[Row("Class1")]
[Row("Class2")]
[Row("Class3")]
public void TestMethod(string type)
{
   IMyInterface foo = Activator.CreateInstance(Type.GetType(type)) as IMyInterface;

   //Do tests on foo:
}

In the [Row] attributes, you can pass any arbitrary number of input parameters, such as input values for testing or expected values to be returned by method invocations. You will need to add arguments of the corresponding types as test method input arguments.

Joe Alfano
  • 10,149
  • 6
  • 29
  • 40
  • Can you edit your answer to explain exactly how I would test implementations this way? You seem to be saying that each unit test needs a switch statement for all implementations, which is not what I want. – dlras2 Feb 20 '12 at 20:21
  • You don't need to use a switch statement if you don't want to. I mentioned that you can also use reflection. See Anthony's answer for an example of this. I also updated my post to include an example of using reflection to instantiate the various concrete implementations of the interface. – Joe Alfano Feb 20 '12 at 20:51
  • I would probably write this to take instances of `IMyInterface` instead of just the name, but still +1. – dlras2 Feb 20 '12 at 22:34
  • Thanks Dan for the idea. I've never been able to get MbUnit row tests attributes to work with anything other than constants. For instance, something like this does not compile: [Row(new Class1())]. I think this is a limitation of .net attribute arguments. I don't know if it is possible to do something like this with other testing frameworks. – Joe Alfano Feb 20 '12 at 22:56
  • Good point about the constants constraint. The activator is definitely a good alternative. – dlras2 Feb 20 '12 at 23:01
  • 1
    @JoeAlfano: .NET attributes parameters can indeed pretty much be constants only ([more](http://stackoverflow.com/questions/2763981/an-attribute-argument-must-be-a-constant-expression)). – k.m Feb 20 '12 at 23:14
2

Expanding on Joe's answer, you can use the [TestCaseSource] attribute in NUnit in a similar way to MBUnit's RowTest. You could create a test case source with your class names in there. You could then decorate every test which the TestCaseSource attribute. Then using Activator.CreateInstance you could cast to the interface and you'd be set.

Something like this (note - head compiled)

string[] MyClassNameList = { "Class1", "Class2" };

[TestCaseSource(MyClassNameList)]
public void Test1(string className)
{
    var instance = Activator.CreateInstance(Type.FromName(className)) as IMyInterface;

    ...
}
Community
  • 1
  • 1
Antony Scott
  • 21,690
  • 12
  • 62
  • 94