How to Write Unit Tests for Generic Classes

by Zoran Horvat

Introduction

When writing unit tests, we normally have a class, referred to as class under test, and then write separate test methods for each of the usage scenarios. That is what we normally have, and that is what test frameworks are there to help about.

Important trait of this kind of testing is that we have a class and we are varying its usages.

But now, let’s turn the table and ask: What if we have a usage of a class, but then the class implementation varies? How do we test that?

If you’re struggling to figure what this means, let me give two situations in which we meet this requirement. One is testing the generic type, and another is testing the implementation of an interface. In this article, we will demonstrate what it means to test generic classes, and that will require dealing with base and derived test classes.

Let’s start from an example. Suppose that we have a simple generic list class:

public class MyList<T>
{
    public void Add(T value) { /* ... */ }
    public bool Contains(T value) { /* ... */ }
    // ...
}

This simple class only exposes two methods – Add(), which adds new element to the class, and Contains(), which tests whether the list contains the specified value.

And so we come to the question of testing. Let’s say that we only want to test the Contains() method. Here are the test cases we want to check:

  • Value was added using the Add() method, then Contains() returns True when the same value is passed to it as the argument.
  • Value was added using the Add() method, then Contains() returns False when a different value is passed to it as the argument.
  • Value was added using the Add() method, then Contains() returns False when null is passed to it as the argument. This case is only possible when generic type argument is a reference type.
  • null was added using the Add() method, then Contains() returns False when an object is passed as the argument.

These are the test cases we are interested to implement. And that is where our testing troubles will begin.

Recognizing the Need for Varying Tests

Here is the problem. Implementation of the Contains() method in the generic list class depends on qualities of the generic type argument. For example, elements of the list may implement IEquatable<T>. If that is so, Contains() method will use the elements’ Equals() method to test them for equality. Likewise, elements may implement IComparable<T>, in which case Contains() method might rely on its CompareTo() method to test for equality. Additionally, element type may in fact be the value type, which rules out null tests.

Therefore, we can conclude that there are several distinct generic derivations we may be interested in. We must test MyList<int>, for example, to cover (non-nullable) value types. We may have to test MyList<object>, to cover reference types that neither implement IEquatable<T> nor IComparable<T>. Furthermore, we would have to test the class for some type that implements IEquatable<T> and for one more type which only implements IComparable<T>.

As you can see, all the unit tests must remain the same, simply because Contains() method of the generic list must behave consistently over all these generic type derivations. However, we don’t want to have to repeat implementation of all unit tests for each of the separate derived types.

Object-oriented programmers as we are, we wish to write all unit tests exactly once, and then specialize them once for each of the target types. Just like target types are deriving from the base generic type, we want our unit tests to derive from base test class.

An Example Generic Class under Test

Below is the entire implementation of the MyList<T> generic class. Internally, the list keeps its elements in the resizable array. That part is easy.

More complicated part is testing the elements for equality. There is a private delegate named ComparisonStrategy that is responsible for that. Pay close attention to the list’s constructor, which initializes the comparison strategy.

Concrete comparison method depends on the generic type argument. If it implements IEquatable<T>, then comparison strategy will be the Equals() method. Otherwise, if it implements IComparable<T>, then comparison strategy will be the CompareTo() method. Finally, if none of these holds, comparison will fall back to common Equals() method inherited from System.Object.

And that is not the end. If generic type argument is a reference type, then comparison must take null references into account. Previously established comparison strategy must be wrapped into convenient null tests. And this costly complication is avoided entirely for value types.

So, here is the class implementation:

public class MyList<T>
{
    private T[] data = new T[8];
    private int Count { get; set; }

    private Func<T, T, bool> ComparisonStrategy { get; }

    public MyList()
    {

        Func<T, T, bool> coreComparisonStrategy;

        if (typeof(IEquatable<T>).IsAssignableFrom(typeof(T)))
            coreComparisonStrategy = (a, b) => ((IEquatable<T>)a).Equals(b);
        else if (typeof (IComparable<T>).IsAssignableFrom(typeof (T)))
            coreComparisonStrategy = (a, b) => ((IComparable<T>) a).CompareTo(b) == 0;
        else
            coreComparisonStrategy = (a, b) => a.Equals(b);

        if (typeof (T).IsValueType)
            this.ComparisonStrategy = coreComparisonStrategy;
        else
            this.ComparisonStrategy = (a, b) =>
                (object.ReferenceEquals(a, null) && object.ReferenceEquals(b, null)) ||
                (!object.ReferenceEquals(a, null) && coreComparisonStrategy(a, b));
    }

    public void Add(T value)
    {
        if (this.data.Length == this.Count)
            Array.Resize<T>(ref this.data, this.data.Length*2);
        this.data[this.Count++] = value;
    }

    public bool Contains(T value)
    {
        for (int i = 0; i < this.Count; i++)
            if (this.ComparisonStrategy(this.data[i], value))
                return true;
        return false;
    }
}

Writing Generic Unit Tests

Now that we have entire generic class at our disposal, we may step to the problem of writing corresponding unit tests. And, as the class itself is generic, we conclude that unit tests should be generic as well. The point here is that behavior of the generic class should not change with changing the generic type argument. Otherwise, the class would become unpredictable, and that is not what we want to have.

In terms of testing the Contains() method of the generic list, we can expect that it should return True if the list contains an element which claims to be the same as the argument to the Contains() method. That is the expected behavior of the generic list, and that behavior must remain the same whatever concrete type might be used as the type argument.

Therefore, we will start with a generic test class. In this article I am using MSTest framework, but the idea would be the same with any other framework. We will take a look at the test class declaration first, and then provide test methods.

public abstract class MyListTests<T>
{
}

There are a couple of details to notice here. First, the class is not decorated with the TestClass attribute, which is not usual. When working with the MSTest framework, we normally have to decorate test classes with TestClass attribute. Nevertheless, this class is not the real test class. It is only a base to the real test class and therefore it is not relevant whether it has the TestClass attribute or not. On a related note, the compiler will not complain if it finds the attribute there anyway.

But then, the class is declared abstract. Now, this is something we normally don’t do. The fact is that test runner will ignore abstract classes, because it must instantiate each test class in order to run test methods declared on it. Therefore, this test class will not be visible to the test runner. Is that the way we want it? Yes – because it doesn’t make sense to test the generic class alone – we have to know concrete generic type argument first.

Further on, we see that the test class is generic. That is another obstacle to the test runner, because it cannot instantiate generic test classes.

So, this class will definitely be invisible to the test runner. But it will still be able to contain proper test methods, methods that will become visible to the test runner once the generic type argument is fixed, and the class becomes concrete, rather than abstract.

Implementing Test Methods for the Generic Type

Finally, we are ready to add some test methods. Tests will run on the generic list type, but they will not know concrete generic type argument. That is what will eventually make entire test class reusable.

Here is the test method which verifies that generic list will find an element that is contained in it:

public abstract class MyListTests<T>
{
    [TestMethod]
    public void ValueAddedToEmptyList_ContainsInvokedWithSameValue_ReturnsTrue()
    {
        T valueToAdd = this.CreateSampleValue();

        MyList<T> list = new MyList<T>();
        list.Add(valueToAdd);

        T valueToSeek = this.CreateSampleValue();
        bool actualContains = list.Contains(valueToSeek);

        Assert.IsTrue(actualContains);
    }

    protected abstract T CreateSampleValue();

}

Once again, we find an unusual detail here. The test class is now equipped with an abstract CreateSampleValue() method. This method is expected to return an object that can be added to the list. It must be implemented in the derived test class, simply because we don’t know what type T is, and we obviously cannot instantiate it. Therefore, creation of a sample object must be delegated to the derived test class.

Completing the Base Test Class

There are other test cases we want to implement in the base test class as well. As per the list of test cases given above, we can add one more test method to the class:

public abstract class MyListTests<T>
{
    [TestMethod]
    public void ValueAddedToEmptyList_ContainsInvokedWithSameValue_ReturnsTrue()
    {
        T valueToAdd = this.CreateSampleValue();

        MyList<T> list = new MyList<T>();
        list.Add(valueToAdd);

        T valueToSeek = this.CreateSampleValue();
        bool actualContains = list.Contains(valueToSeek);

        Assert.IsTrue(actualContains);
    }

    [TestMethod]
    public void ValueAddedToEmptyList_ContainsInvokedWithAnotherValue_ReturnsFalse()
    {
        T valueToAdd = this.CreateSampleValue();

        MyList<T> list = new MyList<T>();
        list.Add(valueToAdd);

        T valueToSeek = this.CreateDifferentValue();
        bool actualContains = list.Contains(valueToSeek);

        Assert.IsFalse(actualContains);
    }

    protected abstract T CreateSampleValue();

    protected abstract T CreateDifferentValue();

}

This is all we can do in the test class which knows nothing about the generic type argument. Remaining two test cases that were identified before deal with null references. That means that we would have to add constraints to the abstract test class, or otherwise we won’t be able to pass null reference in place of the T argument.

Adding Abstract Derived Test Class

In order to provide unit tests for null argument value passed to either Add() or Contains() method of the generic list, we would have to introduce another test class which adds generic constraint:

public abstract class MyListReferenceTests<T> : MyListTests<T>
    where T : class
{
}

This is important moment in implementing unit tests for the generic list type. This test class is once again abstract, and it is still generic, which means that the test runner won’t be able to access it. Anyway, the quality this test class is adding to the table is the generic constraint.

This class will only contain unit tests that are valid when list element is of a reference type. That will let us cover cases when list contains null or when null is passed to the Contains() method. Without further ado, here are the two additional test methods:

public abstract class MyListReferenceTests<T> : MyListTests<T>
    where T : class
{
    [TestMethod]
    public void ValueAddedToEmptyList_ContainsInvokedWithNull_ReturnsFalse()
    {
        T valueToAdd = this.CreateSampleValue();

        MyList<T> list = new MyList<T>();
        list.Add(valueToAdd);

        bool actualContains = list.Contains(null);

        Assert.IsFalse(actualContains);
    }

    [TestMethod]
    public void NullAddedToEmptyList_ContainsInvokedWithNonNull_ReturnsFalse()
    {
        MyList<T> list = new MyList<T>();
        list.Add(null);

        T valueToSeek = this.CreateSampleValue();
        bool actualContains = list.Contains(valueToSeek);

        Assert.IsFalse(actualContains);
    }
}

This completes the four abstract test methods we had on mind for the Contains() method of the generic list class. Basic quality of these test methods is that they are absolutely independent from concrete type of list elements. This means that we have successfully defined behavior of the generic list without telling anything about what concrete list class it is.

Later on, when concrete list classes finally start appearing, we will be able to rely on these abstract tests. We will be able to tell with certainty that passing tests are proving that concrete list class is behaving correctly.

Implementing Concrete Derived Test Classes

The time has come to define concrete test classes. By this time, we have established unit tests that are independent on concrete generic argument type. This means that we can now vary the type and keep tests the same – that is completely inverse situation compared to what we are usually doing. Normally, we are working on one type under test and then adding more and more test cases. This time, we have a fixed set of test cases and we are varying the type on which they will run.

In that respect, we recognize these concrete types we might want to put under the test:

  • System.Int32 to cover the value types
  • Mock class which only implements IEquatable<T>
  • Mock class which only implements IComparable<T>
  • System.Object to cover reference types that do not implement neither of the two interfaces.

And, guess what. Thanks to the fact that all the tests are completed, the only real work remaining is to just define the concrete generic type argument and to provide sample values:

[TestClass]
public class MyListOfValuesTests : MyListTests<int>
{
    protected override int CreateSampleValue() => 7;
    protected override int CreateDifferentValue() => 9;
}

This is all it takes to define concrete unit tests over a list of integers. Let’s examine this short class. It comes with the TestClass attribute, marking it as a potential target to the test runner. Note that TestClass attribute is not inherited. Even if we had it added to the base test class, this concrete class would have to be decorated with its own TestClass attribute.

Further on, the class is not abstract anymore. And it is not generic. Therefore, all conditions are met for this class to be visible to the test runner – it is a concrete, non-abstract, non-generic class, with TestClass attribute added.

And in order to compile, this class had to provide concrete implementations for the two methods expected by its base.

Let’s see what it takes to test classes that implement IEquatable<T> interface:

[TestClass]
public class MyListOfEquatableTests
    : MyListReferenceTests<MyListOfEquatableTests.EquatableMock>
{
    public class EquatableMock : IEquatable<EquatableMock>
    {
        private int Id { get; }

        public EquatableMock(int id)
        {
            this.Id = id;
        }

        public bool Equals(EquatableMock other) =>
            other != null && this.Id.Equals(other.Id);
    }

    protected override EquatableMock CreateSampleValue() =>
        new EquatableMock(1);

    protected override EquatableMock CreateDifferentValue() =>
        new EquatableMock(2);
}

This time, concrete test class has become a bit more engaged. It comes with a mock class which only provides proper implementation of the IEquatable<T> interface. Beyond that complication, it only remains to provide two sample objects from the two overridden methods and that is all.

Note, however, that this test class is deriving from MyListReferenceTests. That is the proper base for test classes on reference types, and that will add two more tests compared with the test class on System.Int32.

The next concrete test class we will see here is the one which tries objects implementing IComparable<T> interface:

[TestClass]
public class MyListOfComparableTests
    : MyListReferenceTests<MyListOfComparableTests.ComparableElement>
{
    public class ComparableElement : IComparable<ComparableElement>
    {
        private int Id { get; }

        public ComparableElement(int id)
        {
            this.Id = id;
        }

        public int CompareTo(ComparableElement other)
        {
            if (other == null)
                return 1;
            return this.Id.CompareTo(other.Id);
        }
    }

    protected override ComparableElement CreateSampleValue() =>
        new ComparableElement(1);

    protected override ComparableElement CreateDifferentValue() =>
        new ComparableElement(2);
}

This implementation is quite similar to the previous one, only the elements are now implementing IComparable<T> interface.

Finally, we wish to test the list class on a type which doesn’t implement any of these equality-related interfaces. We will simply rely on the System.Object class for that:

[TestClass]
public class MyListOfObjectTests
    : MyListReferenceTests<object>
{
    private static object Sample { get; } =
        new object();

    protected override object CreateSampleValue() =>
        Sample;

    protected override object CreateDifferentValue() =>
        new object();
}

This was more than easy. With all four concrete test classes in place, we have effectively created total of fourteen unit tests. And all that without ever having to repeat a single line of testing code.

We can run the tests and all of them will be green, as shown in the figure below.

Test Explorer

Conclusion

In this article we have tackled one situation which commonly occurs in unit testing – how to implement tests for a generic class.

We have seen that tests can be coded even without knowing concrete generic type argument. Even more, it is recommended to code unit tests in terms of a generic type because that will give us the opportunity to fully describe expected behavior of the generic class.

Once concrete classes are derived from the generic base, we want all the tests to keep passing on that type as well. That will ensure that generic derivation itself did not cause an unexpected change in behavior of the class.

We have seen that unit test classes can be derived in the same line of derivation as the classes under test. That gives the opportunity to implement all unit tests in the base class only. The only responsibility of the concrete derived test classes then remains to specify generic type argument and all the tests will be inherited and specialized for that type automatically.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub