Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I implement a series of reusable tests to test an interface's implementation?

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.

like image 299
dlras2 Avatar asked Feb 20 '12 20:02

dlras2


4 Answers

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.

like image 112
avandeursen Avatar answered Nov 12 '22 05:11

avandeursen


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;

    ...
}
like image 33
Antony Scott Avatar answered Nov 12 '22 07:11

Antony Scott


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.

like image 3
dlras2 Avatar answered Nov 12 '22 07:11

dlras2


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.

like image 2
Joe Alfano Avatar answered Nov 12 '22 07:11

Joe Alfano