Test op test op test

Deze week liet ik aan een collega (Rob Vens) een aantal testen zien voor een recent project van mijn hand. Hij vond ze wel mooi om te zien en ik bedacht me dat ik deze nog niet gedeeld heb met mijn collega’s en andere geïnteresseerden. Dus dat leek me mooi om hier ook even uit de doeken te doen. Het is gebaseerd op werk van Greg Young en Mark Nijhof. Wat ik er met name aan heb toegevoegd is het afleiden van specifiekere testcases van generiekere testcases, waardoor complexere scenarios op simpelere scenarios bouwen, zonder dat er dubbeling optreedt.

Het leukste is om even een concreet voorbeeld te pakken. Dit gaat om een service die rond een rfid kaart geboden zou kunnen worden door ons systeem. Het brengt gegevens voor een rfid kaart bij elkaar vanuit verschillende bronnen, onder andere de id provider en biedt deze aan aan de klant (organisatie). De klant-organisatie dient zich te registreren op updates (events) op deze identifier. Het eerste scenario gaat meteen om de meest complexe in de serie: de klant doet nogmaals een registratie op een identifier waarop deze zich al eerder succesvol geregistreerd heeft.

[TestClass]
public class And_the_client_does__the_same_registration_again : When_registering_an_identifier_succeeded
{
    protected Guid NewRegistrationKey { get; set; }

    protected override void Given()
    {
        base.Given();
        base.When();
    }

    protected override void When()
    {         
        NewRegistrationKey = Service.RegisterIdentifier(AccountKey, Identifier);
    }

    [TestMethod]
    public void The_previous_registration_should_have_been_returned()
    {
        Assert.AreEqual(RegistrationKey, NewRegistrationKey);
    }
}

Het leuke is dus dat je ziet dat de bovenstaande class afleid van de de class When_registering_an_identifier_succeeded, waarin een eerste registratie gebeurd is. De bovenstaande test leest dus voluit als: When_registering_an_identifier_succeeded.
And_the_client_does__the_same_registration_again.
The_previous_registration_should_have_been_returned.

Als je dan kijkt naar de class When_registering_an_identifier_succeeded, zie je het volgende beeld:

[TestClass]
public class When_registering_an_identifier_succeeded : When_registering_an_identifier
{
    protected override void When()
    {
        RegistrationKey = Service.RegisterIdentifier(AccountKey, Identifier);
    }

    [TestMethod]
    public void A_registration_with_the_given_key_should_have_been_made()
    {
        var registration = Service.GetRegistration(AccountKey, RegistrationKey);
        Assert.AreEqual(RegistrationKey, registration.RegistrationKey);
    }

    [TestMethod]
    public void The_registration_will_be_found_in_the_registrations()
    {
        var registrations = Service.GetRegistrations(AccountKey);
        Assert.AreEqual(1, registrations.Count(r => r.RegistrationKey == RegistrationKey));
    }
}

De bovenstaande class When_registering_an_identifier_succeeded is dus de base class voor alle scenarios waarin de (eerste) registratie gelukt is.
Deze leidt af van de generieke class die geldt voor alle scenarios waarin een identifier geregistreerd wordt. Hierin wordt ook de nodige basic setup gedaan.

public abstract class When_registering_an_identifier : ServiceTestFixture
{
    protected const string ClientName = "KlantOrganisatie";

    protected IManagementService _mservice;

    protected ClientReport Client;
    protected Guid AccountKey;
    protected readonly IdentifierEngravedId Identifier = new IdentifierEngravedId(128452875481724);
    protected readonly IdentifierChipId ChipId = new IdentifierChipId(146124, 0);

    protected Guid RegistrationKey;

    protected override void SetupDependencies()
    {
        // Ja, we gebruiken Moq
        Mock serviceMock = new Mock();
        serviceMock.Setup(x => x.ProcessRegistration(It.IsAny()))
            .Returns(new IdentifierProviderServiceResponse
                         {
                             EngravedId = Identifier, 
                             ChipId = ChipId, 
                             From = SystemDateTime.Now(), 
                             Until = SystemDateTime.Now().AddYears(4)
                         }).AtMostOnce().Verifiable();
        // En StructureMap
        ObjectFactory.Inject(serviceMock.Object);
    }

    protected override void Given()
    {
        _mservice = ObjectFactory.GetInstance();
        _mservice.CreateNewClient(ClientName);
        Client = _mservice.GetAllClients().Single(m => m.ClientName == ClientName);
        AccountKey = _mservice.GenerateNewAccountKeyForClient(Client.Key);
    }

    // Hier zouden nog testen geplaatst kunnen worden die de setup testen.
    // Dan moet de class een [TestClass] worden, niet abstract zijn en
    // When() zal dan nog (leeg) geïmplementeerd moeten worden
}

Deze leidt tenslotte weer af van de generieke ServiceTestFixture class, die een service test. Tevens zet deze applicatie op voor zover nodig voor een integratietest en doorloopt deze het standaard Given-When-Then scenario van een BDD test.

[TestClass]
public abstract class ServiceTestFixture
{
    protected TService Service;
    protected IReportingRepository ReportingRepository;
    protected Exception CaughtException;
    protected virtual void SetupDependencies() { }
    protected virtual void Given() { }
    protected abstract void When();
    protected virtual void Finally() { }

    [TestInitialize]
    public void Setup()
    {
        CaughtException = new ThereWasNoExceptionButOneWasExpectedException();

        ApplicationBootStrapper.BootStrapWithCleanDatabases();

        Service = ObjectFactory.GetInstance();
        ReportingRepository = ObjectFactory.GetInstance();

        SetupDependencies();

        Given();
        try
        {
            When();
        }
        catch (Exception exception)
        {
            CaughtException = exception;
        }
        finally
        {
            Finally();
        }
    }

    [TestCleanup]
    public void TearDown()
    {
        ObjectFactory.ResetDefaults();
        SystemDateTime.Reset();
    }
}

Het mooie van de bovenstaande test setup is dat elk van de test classes goed leesbaar zijn en de testen erop onafhankelijk van elkaar getest worden.
Bovendien gebeurt er heel veel in de taal van het domein op een hoog niveau, zodat deze testen ook bij flinke implementatiewijzigingen bijna niet aangepast hoeven te worden.

Inmiddels heb ik goede ervaringen met de bovenstaande vorm van testen, die ik met mijn lezers wilde delen. Veel plezier ermee en als je vragen hebt, laat het weten.