Abstract Test Fixture
For the past 12 months I have been making an effort to write all my code test first. It has been a steep learning curve and I am only just coming out the other side. One of the things I want to do with this blog is to talk about what I have come up against and any tricks I have worked out or learnt along the way. I have always found that I gain a deeper understanding when explaining things.
One of the things that I have learnt from Uncle Bob is that your tests are as important if not more so than the production code, and that if you should keep your production code neat and tidy then you should give the tests the same respect. With that in mind I will get to my point.
I use abstract classes to reduce code duplication and enforce a contract on implementation of concrete types. When I refactor out an abstract class in the refactor phase of a “Red Green Refactor” cycle, I also refactor out an abstract test fixture.
As an example I have an abstract fixture that was created to test the common parts of classes that server requests. This is part of an example project I am building.
using Gravyframe.Service.Messages;
using NUnit.Framework;
namespace Gravyframe.Service.Tests
{
[TestFixture]
public abstract class ServiceTests<TRequest, TResponce, TService, TNullRequestException>
where TResponce : Response, new()
where TRequest : Request, new()
where TService : Service<TRequest, TResponce, TNullRequestException>
where TNullRequestException : Service<TRequest, TResponce,TNullRequestException>.NullRequestException, new()
{
public TService Sut;
[SetUp]
protected void SetUp()
{
ServiceSetUp();
}
protected abstract void ServiceSetUp();
[Test]
public void CanCreateTService()
{
// Assert
Assert.IsNotNull(Sut);
}
[Test]
public void IsAssignableFromTService()
{
// Assert
Assert.IsInstanceOf<Service<TRequest, TResponce, TNullRequestException>>(Sut);
}
[Test]
public void WhenRequestIsNullTNullRequestThrown()
{
// Assert
Assert.Throws<TNullRequestException>(() => Sut.Get(null));
}
[Test]
public void
WhenTRequestInNotNullTNullRequestExceptionNotThrown()
{
// Assign
var request = new TRequest();
// Assert
Assert.DoesNotThrow(() => Sut.Get(request));
}
[Test]
public void WhenTRequestIsNotNullTResponceNotNull()
{
// Assign
var request = new TRequest();
// Act
var responce = Sut.Get(request);
// Assert
Assert.IsNotNull(responce);
}
}
}
An example implementation of the abstract fixture
using System.Collections.Generic;
using Gravyframe.Service.Example;
namespace Gravyframe.Service.Tests
{
public class ExampleServiceTests : ServiceTests<ExampleRequest, ExampleResponse, ExampleService, ExampleService.NullExampleRequestException>
{
public IEnumerable<ResponseHydrator<ExampleRequest, ExampleResponse>> ResponseHydratationTasks;
protected override void ServiceSetUp()
{
ResponseHydratationTasks = new List<ResponseHydrator<ExampleRequest, ExampleResponse>>();
Sut = new ExampleService(ResponseHydratationTasks);
}
}
}
After creating the implementation of the fixture, I set the “System under Test” (Sut) in the setup to a news instance of ExampleTest (the type I am testing) supplying it with any implementation dependences. Then all the tests in the abstract fixture can run and any tests for implementation details are added to the new fixture. The abstract tests are made possible with generics and I think an explanation of how I achieved this deserves its own post.
Here is the example concrete types for these tests (I won’t add the abstract service in this blog post)
using System;
using System.Collections.Generic;
namespace Gravyframe.Service.Example
{
public class ExampleService : Service<ExampleRequest, ExampleResponse, ExampleService.NullExampleRequestException>
{
public ExampleService(IEnumerable<ResponseHydrator<ExampleRequest, ExampleResponse>> responseHydratationTasks)
: base(responseHydratationTasks)
{
}
[Serializable]
public class NullExampleRequestException : NullRequestException
{
}
}
}
using Gravyframe.Service.Messages;
namespace Gravyframe.Service.Example
{
public class ExampleRequest : Request
{
}
}
using Gravyframe.Service.Messages;
namespace Gravyframe.Service.Example
{
public class ExampleResponse : Response
{
}
}
It is important to make sure your base functionality stays intact between implementations to conform to the “Liskov substitution principle”. These abstract tests help validate this functionality and gives you free tests.
This is still a work in progress, and I hope to make it openly available on GitHub in the coming months.