Test op test op test

 
06 februari 2011

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<IClientService>
{
    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<IProviderServiceService> serviceMock = new Mock<IProviderServiceService>();
        serviceMock.Setup(x => x.ProcessRegistration(It.IsAny<IdentifierProviderServiceRequest>()))
            .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<IManagementService>();
        _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<TService>
{
    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<TService>();
        ReportingRepository = ObjectFactory.GetInstance<IReportingRepository>();
 
        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.


Werken met ?
Kijk dan bij onze mogelijkheden voor starters en/of ervaren IT'ers.


Categorieën: Architectuur, Development, Professionele vaardigheden

Tags: , , , ,


Reacties (4)

  • Heb je SpecFlow wel eens gebruikt? Je kunt daar ook best leuke dingen mee, maar het vereist wel het in synch houden van je in Gherkin geschreven testen met achterliggende testcode (die je nog steeds zelf zult moeten schrijven). Je kunt wel test (tussen-)stappen hergebruiken inderdaad. Maar het idee achter SpecFlow is dat de testen leesbaar zijn voor ‘business’ mensen, het heeft eigenlijk niet zoveel te maken met het schrijven van de testen zelf. (voor die leesbaarheid heb ik in dit project trouwens Fitnesse gebruikt).

    Geplaatst op 08 februari 2011 om 15:21 Permalink

  • Ziet er goed uit, maar dit kan niet met standaard BDD frameworks zoals SpecFlow?

    Geplaatst op 08 februari 2011 om 14:52 Permalink

  • Bedankt! Eventueel kan ik natuurlijk nog wat in detail laten zien, maar dit laat volgens mij al wel redelijk goed zien hoe je dit kunt inzetten.

    Geplaatst op 07 februari 2011 om 12:18 Permalink

  • Sjors Miltenburg schreef:

    Leuke post! was nog niet op dit idee gekomen

    Geplaatst op 07 februari 2011 om 11:59 Permalink

Trackback & Pingback (1)

  1. Van Tweets die vermelden Test op test op test | Software Innovators -- Topsy.com op 08 februari 2011 at 14:17

    […] Dit blogartikel was vermeld op Twitter door sogyo, Rick van der Arend. Rick van der Arend heeft gezegd: @seagile maybe this is useful for you? http://goo.gl/mDmIC (inheritance used to link test cases) […]