Build your Unit Tests against ADTs

Veröffentlicht von

I’m actually working through Zoran Horvats Writing Highly Maintainable Unit Tests – Pluralsight Course. Throughout this course, Zoran showed the idea to build unit tests around Abstract Data Types (=ADT), which I liked a lot. The core idea: You build a base set of unit tests around a given interface. Since your tests are always pointing to an interface you’re able to test derived classes for base functionality with the same test suite.

As short node from Wikipedia about ADTs:

In computer science, an abstract data type (ADT) is a mathematical model for data types. An abstract data type is defined by its behavior (semantics) from the point of view of a user, of the data, specifically in terms of possible values, possible operations on data of this type, and the behavior of these operations. This mathematical model contrasts with data structures, which are concrete representations of data, and are the point of view of an implementer, not a user.

So the key point is that an ADT defines the requested behavior of a structure WITHOUT defining how it is implemented.

Let us take a look at an example:

 

Here we define an IList ADT which offers the following operations:

  • Count: Returns the number of items in the list.
  • Append: Allows to insert a new element to the list.
  • Clear: Removes all elements from the list.
  • GetElementAtIndex: Returns the element at a given index.

In addition to the interface definition, we should refine the behavior of the given methods. This kind of analysis will help us later on when designing the unit tests for the ADT.

ADT Behaviour

Below points are going to define the very base behavior for the ADT. These descriptions are afterward used to define your base test-cases for the ADT.

Count:

  • Returns 0 for a newly created List.
  • Returns value > 0 for a List with added items.

Append:

  • If called n x times, Count returns n.

Clear:

  • Count returns 0 if n items have been on the list.

GetElementAtIndex:

  • Index doesn’t exist, Exception is thrown.
  • Index is negative, Exception is thrown.
  • Index = n, Append was called n x times. Element at index n is returned.

 

ADT Unit Tests

Class setup of the unit tests:

Test cases checking the behavior of the IMyList ADT are defined in the IMyListTests class. Since IMyListTests class is generic it has to define some abstract methods for

  • System Under Test (=SUT) creation
  • Equality comparison
  • Retrieving elements to be added during the running unit test.

One of the biggest benefits of this strategy is that we’re able to add new derived classes implementing the IMyList interface. Unit tests are highly maintainable because we only need to add a new test class (e.g. MyNewCollectionTest) implementing the abstract protected members of IMyListTests.

I’ve created a Github repository hosting the corresponding code.

The below code shows the implementation of the base test class. The implementation follows the Arrange-Act-Assert flow. The ADT behavior descriptions are mapped as comments to the corresponding unit tests.

using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace MyCollections.Test
{
    public abstract class IMyListTest<T>
    {
        [Fact]
        // Returns 0 for a newly created List.
        public void Count_ZeroElementsInList_ReturnZero()
        {
            var lst = this.CreateSut();

            Assert.Equal(0, lst.Count());
        }

        [Fact]
        // Count: Returns value > 0 for a List with added items.
        // Append: If called n x times, Count returns n.
        public void Count_NElementsInList_ReturnN()
        {
            // Arrange
            var lst = this.CreateSut();
            var elements = this.GetElements().ToList();

            // Act 
            elements.ForEach(e => lst.Append(e));

            // Assert
            Assert.Equal(elements.Count, lst.Count());
        }

        [Fact]
        // Clear: Count returns 0 if n items have been in the list.
        public void Clear_NonEmptyList_CountReturnsZero()
        {
            // Arrange
            var lst = this.CreateSut();
            var elements = this.GetElements().ToList();

            elements.ForEach(e => lst.Append(e));

            // Act 
            lst.Clear();

            // Assert
            Assert.Equal(0, lst.Count());
        }

        [Fact]
        // Index doesn’t exist, Exception is thrown.
        public void GetElementAtIndex_ListIsEmpty_ThrowException()
        {
            // Arrange
            var lst = this.CreateSut();

            // Act 
            // Assert
            Assert.ThrowsAny<Exception>(() => lst.GetElementAt(1));
        }

        [Fact]
        // Index is negative, Exception is thrown.
        public void GetElementAtIndex_IndexIsNegative_ThrowException()
        {
            // Arrange
            var lst = this.CreateSut();

            // Act 
            // Assert
            Assert.ThrowsAny<Exception>(() => lst.GetElementAt(-1));
        }

        [Fact]
        // Index = n, Append was called n x times. Element at index n is returned.
        public void GetElementAtIndex_SingleElementInList_ElementAtIndexIsReturned()
        {
            // Arrange
            var lst = this.CreateSut();
            var elementToAppend = this.GetElement();
            lst.Append(elementToAppend);

            // Act 
            var fetchedElement = lst.GetElementAt(0);

            // Assert
            AssertAreEqual(elementToAppend, fetchedElement);
        }

        [Fact]
        public void GetElementAtIndex_MultipleElementInList_ElementAtIndexIsReturned()
        {
            // Arrange
            var lst = this.CreateSut();
            var elements = this.GetElements().ToList();

            elements.ForEach(e => lst.Append(e));
            var expectedElement = elements.Last();

            // Act 
            var fetchedElement = lst.GetElementAt(elements.Count -1);

            // Assert
            AssertAreEqual(expectedElement, fetchedElement);
        }

        protected abstract IMyList<T> CreateSut();
        protected abstract IEnumerable<T> GetElements();
        protected abstract T GetElement();
        protected abstract void AssertAreEqual(T val1, T val2);
    }
}

The implementation of the derived test classes is very simple:

using System.Collections.Generic;
using Xunit;

namespace MyCollections.Test
{
    public class MyArrayTest : IMyListTest<int>
    {
        protected override void AssertAreEqual(int val1, int val2)
            => Assert.Equal(val1, val2);

        protected override IMyList<int> CreateSut() 
            => new MyArray<int>();

        protected override int GetElement() 
            => 42;

        protected override IEnumerable<int> GetElements() 
            => new int[] { 1, 2, 3, 4 };
    }
}

 

Hope this gives you a new perspective when thinking about/creating new unit test suites.

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

*

code