Blog

Wat is niet de unit in een unittest? (deel 1)

In mijn carrière als softwareontwikkelaar heb ik vaak liggen worstelen met de vraag wat een unittest is. Vers uit school gaf ik het tekstboek antwoord met de regels waar een unittest aan moest voldoen. Later realiseerde ik hoe vaag het tekstboek antwoord voor mezelf en andere kan zijn en stapte ik over op een concreter antwoord. Ik antwoordde stellig dat een class de harde grens was. Tussendoor heb ik ook nog wel eens uitstapjes gemaakt naar de arrogante ontwikkelaar die geen testen maakt voor zijn projecten. Ik werd steeds meer met de neus op de feiten gedrukt, en zag dat de kwaliteit van het product met testen en zonder testen niet veel verschilde. Beide hadden net zoveel bugs en door het maken van testen duurde het langer om iets op te leveren. Het werd me steeds duidelijker dat de kwaliteitswaarborging die ik nastreefde totaal geen kwaliteit was. Ik werd steeds onzekerder van mijn antwoord en uiteindelijk moest ik erkennen dat ik alleen maar het antwoord geen idee kon geven. Tenminste totdat ik deze blog ging schrijven.

#Table Of Contents

Telkens als ik een softwareontwikkelaar hardop hoor roepen dat hij/zij aan Test Driven Developement doet, dan word ik al ongemakkelijk. Als hij/zij vervolgt met de mantra’s van waarom je aan TDD moet doen en met een grijns de groep in kijkt, dan gaan mijn nekharen recht overeind staan. Op dat moment vraag ik me af of deze persoon daadwerkelijk in de discipline van TDD gelooft of dat hij alleen de stappen volgt. Er is een groot verschil. En dat verschil is het maken van de geautomatiseerde testen.

Ik wil in deze blog het niet hebben over of TDD een goede discipline is of niet, maar ik merk dat het maken van goede geautomatiseerde testen een vaardigheid op zich is. De discipline van TDD gaat niet om het maken van testen. Het doel is om goed na te denken over je software en de architectuur; Het opbouwen van een vertrouwen dat nieuwe functionaliteiten niet oude functionaliteiten breken. Dat laatste wil zeggen dat wanneer je op de knop drukt om alle testen te draaien en ze slagen allemaal, dat je dan met een gerust hart je code kan inchecken. Je hebt geen nieuwe bug geïntroduceerd. Faalt een test dan heb je iets kapot gemaakt en moet je de software repareren, en niet de falende test.

In dit deel van de blog is het doel te omschrijven wat mijn idee van een unittest niet is. Hieraan kan je afleiden of een ontwikkelaar in de TDD-discipline gelooft of gewoon de stappen doorloopt. Als een ontwikkelaar niet volgens TDD werkt, maar wel testen maakt dan toont dit ook dat hij/zij dit doet omdat het gezien wordt als een good practice binnen softwareontwikkeling of dat hij/zij goed nadenkt over wat de kwaliteit is, die gewaarborgd wordt. Als je je aangesproken voelt, dan heeft de tekst zijn werk gedaan. Begrijp wel dat ik je niet wil beledigen, maar dat ik hoop je aan het denken te zetten, waardoor je jezelf evalueert en als een betere ontwikkelaar uit deze blog komt. Ongeacht of je het met mij eens bent of niet.

Waarom de focus op unittesten?

Ik had iedere laag van geautomatiseerde testen kunnen kiezen. Maar voor wie de testpiramide kent weet dat unittesten de laagste en grootste laag is van alle testen. Wanneer we beginnen met het maken van geautomatiseerde testen, dan zijn unittesten de eerste die gemaakt worden. Unittesten zijn ook de testen die het vaakst gedraaid worden door ontwikkelaars. Iedere keer dat je ‘build’ zouden ook alle unittesten moeten draaien. Unittesten zijn klein en wij maken er een groot aantal van. Hierdoor leveren unittesten de grootste return of investment op.

Aan unittesten lezen ontwikkelaars af of zij wel of niet iets kapot hebben gemaakt en vormt de grootste basis van het vertrouwen. Om een goede kwaliteit aan geautomatiseerde testen neer te zetten, begint het bij een goede basis. Als de basis van de piramide niet goed is, dan stort de rest ook in.

Lange tijd heb ik mezelf afgevraagd: “Wat zijn de grenzen die de unit van een unittest definiëren?”. Als ik dit aan mijn collega’s vraag dan krijg ik drie mogelijke antwoorden terug. Het eerste antwoord is geen idee. Deze persoon was net als ik nog altijd op zoek naar een correct antwoord. Het tweede antwoord is altijd een heel vaag tekstboek antwoord. Dit antwoord is volgens de definitie correct maar het helpt niet om kwalitatief goede unittesten te maken. Het laatste antwoord is gerelateerd aan de syntax van de programmeertaal. In het geval van C# en Java wordt een harde grens getrokken bij een ‘class’. Ik ben momenteel van mening dat dit antwoord verkeerd is. Maar later meer hierover. Wat ik wel als een correct antwoord zie zal ik in de volgende blog uitleggen.

Een unit is niet vaag.

Laten we beginnen met het tweede antwoord. Dit antwoord is het opsommen van de tekstboek regels van wat het unittest concept is en uitgebreid met de vergelijking ten opzichte van een integratie test.

  • Het individueel testen van iedere module/component in je systeem en het aantonen dat deze correct werken.
  • Een unittest is geïsoleerd.
  • Het is een “soort van White box Testen”.
  • Wordt gemaakt door de softwareontwikkelaar.
  • Een unittest moet snel en op ieder moment uitgevoerd kunnen worden.
  • Een unittest test alleen de unit zelf en niet de interactie met andere units.
  • Maakt het vinden van fouten gemakkelijk.
  • Het onderhoud van unittesten is goedkoop.
  • Een unittest test het gedrag van de unit.

Dit is een lijst met regels die ik vaak te horen krijg. Deze regels zijn voor een unittest correct. Het nadeel is dat deze zijn geschreven in een natuurlijke taal. Iedereen die volgens het Scrum framework te werk gaat, weet dat niets zo ambigu kan zijn als natuurlijke taal. Dat is ook de reden waarom we de rituelen hebben in Scrum. Door deze ambiguïteit komt het voor dat er misvattingen ontstaan en dat we eigenlijk testen aan het maken zijn die niets bijdragen aan de kwaliteitsgarantie van het systeem.

Wat is er dan zo vaag aan. Laten we beginnen met “soort van White box Testen”. Het ‘soort van’ maakt het verwarrend, want is het wel of niet White box testing. Sommige ontwikkelaars zijn hier stellig in en zeggen dat unittesten wel white box testen zijn, wat naar mijn mening niet correct is. Het antwoord is dat de ontwikkelaar die de code schrijft ook de unittest schrijft en in die vorm een white box is. De test zelf gaat niet kijken hoe de code dat doet en of dat wel volgens de voorgeschreven standaarden doet. In een unittest kijk je naar resultaten die gedaan zijn en kan niet uitsluiten dat er geen andere dingen gedaan worden. In die zin is de unit gewoon een black box.

Daarnaast heb je termen als module en component, wat eigenlijk termen uit software design/architectuur zijn. Het helpt ook niet dat modules en component kunnen bestaan uit kleinere modules en componenten. Als dit dan naar een programmeertaal vertaald wordt, door applicaties zoals Enterprise Architect en Visio, dan kom je al snel op termen in C# als namespaceclassfunction/method en variabele. Maar wat is de module die je test?

Er zijn ook nog een aantal regels die gezamenlijk een versterkende werking hebben op de misvattingen over, van, voor wat een unit is. “Een unittest test een module” en ook alleen de module, “niet de interactie met andere units”. Een unittest is “snel” uit te voeren en moet ook op ieder moment uitgevoerd kunnen worden. Deze regels versterken elkaar dat ontwikkelaars vaak zeggen dat een module “zo klein mogelijk” moet zijn en een test “zo snel mogelijk” uitgevoerd moet kunnen worden. Dit leidt ertoe dat een ontwikkelaar een verkeerde keuze maakt en dat een class met zijn publieke methoden gezien wordt als de “unit”. Door het Dependency Inversion principe, samen met de trend die IoC containers zoals Autofac meebrengen, wordt het gemakkelijk om de afhankelijkheden van een class te mocken en de interacties tussen classes los te koppelen. Hiermee denken ontwikkelaars te voldoen aan de regel dat een “unittest geïsoleerd is”.

Dit zijn misvattingen en je zal je wel afvragen waarom. Kleiner dan een class en de publieke methode kan je in C# en Java niet gaan. De compiler staat het niet toe dat je vanuit een unittest een private methode op een class aanroept. Of dat je een specifiek statement test in een functie en de resterende statements in dezelfde functie niet uitgevoerd worden. Of je moet je testcode tussen je productiecode zetten. Maar dit wordt gezien als bad practice in softwareontwikkeling en breekt dit ook vele SOLID principes. Het is een goed begin om stukken code naar hun eigen classes en methodes te herstructureren, maar bedenk ook dat een unittest maken voor iedere class en methode die je herstructureert niet je testen sneller zullen maken. Op een gegeven moment wordt de overhead van je testframework je beperkende factor. Denk aan de tijd die nodig is om je test situatie op te zetten met alle mocks en het uitvoeren van de code in de mocks ten opzichte van de tijd voor het uitvoeren van het aantal statements in je productiecode. En des te meer unittesten je maakt des te meer overhead je ook creëert. Het kan voorkomen dat je test omgeving niet langer meer snel is maar traag wordt door de vele overhead. Er bestaat iets als TE klein en TE snel willen zijn. Een unittest moet snel zijn om ervoor te zorgen dat ontwikkelaars snel feedback krijgen of hun nieuwe ontwikkelingen iets breken. Als ontwikkelaars het gedrag van de unit aan kunnen tonen met een of twee test items, dan is het niet de bedoeling dat ze de test gaan overladen met 10000 items. Het is ook niet goed als ontwikkelaars I/O gaan toevoegen als het gedrag goed geëvalueerd kan worden in-memory. Het uitvoeren van alle unittesten voor de module moet niet langer duren dan dat er mogelijk een ‘context-switch’ kan plaats vinden.

Hierboven heb ik de regels een unittest is geïsoleerd en een unittest test het gedrag van een unit een paar keer genoemd. Dit zijn de twee regels voor unittesten die vaak verkeerd begrepen worden. En ik kan dit het beste aantonen met een voorbeeld van waarom een class niet een goede maatstaaf is voor een unit in unittesten.

Een unit is niet beperkt tot een class!

Een derde antwoord dat ik te horen krijg, zijn van personen die zeker van hun zaak zijn en aangeven dat een class de maatstaaf is voor een unit. Iedere productiecode class wordt dan vergezeld door een unittest class en alle dependencies die de class heeft worden gemockt. Wat je je misschien niet realiseert is dat je hiermee een sterk gekoppeld systeem maakt. Als je een class verandert dan gaat er met zekerheid de daarbij behorende unittest class ook omvallen. En als je interne interfaces hebt binnen je module die wijzigen, dan heb je al helemaal de poppen aan het dansen. Unittest classes die gebruik maken van de interface moeten ook wijzigen. Als je systeem zo sterk gekoppeld is dan wordt je code rigide. Iedere verandering brengt extra werk met zich mee, omdat je alle afhankelijkheden van je code moet gaan repareren. Dus unittesten zijn dan niet langer meer goedkoop om te onderhouden, maar juist extra duur.

Laat ik dan nu ook maar eens een voorbeeld maken. Een veel voorkomende implementatie van een class die ik zie is als volgt.

public FooController : IFooController
{
    private readonly IFooServiceOne _serviceOne;
    private readonly IFooServiceTwo _serviceTwo;

    public FooController(
        IFooServiceOne serviceOne,
        IFooServiceTwo serviceTwo)
    {
        _serviceOne = serviceOne;
        _serviceTwo = serviceTwo;
    }

    public FooResult DoFoo(int fooInput)
    {
        var something = 
            _serviceOne.GetSomething(fooInput);
        return _serviceTwo.CompleteFoo(something);
    }
}

Wanneer we kijken naar hoe de unittesten uitzien dan krijgen we het volgende. Mijn voorbeeld maakt gebruik van NUnit en Moq, omdat ik op dit moment deze veel gebruik.

[TestFixture]
public class FooControllerTests
{
    private static Random _random = new Random();
    private Mock<IFooServiceOne> _serviceOneMock;
    private Mock<IFooServiceTwo> _serviceTwoMock;
    private FooController _sut;

    [SetUp]
    public void SetUp()
    {
        _serviceOneMock = new Mock<IFooServiceOne>();
        _serviceTwoMock = new Mock<IFooServiceTwo>();
        _sut = new FooController(
            _serviceOneMock.Object,
             _serviceTwoMock.Object);
    }	

    [Test]
    public void DoFoo_Should_CallFooServiceOneWithTheInput()
    {
        // Arrange
        var expected = _random.Next();

        // Act
        var _ = _sut.DoFoo(expected);

        // Assert
        _serviceOne.Verify(
            mock => mock.GetSomething(
                It.Is<int>(actual => actual == expected)),
            Times.Once);
    } 

    [Test]
    public void DoFoo_Should_CallFooServiceTwoWithResultFromFooServiceOne()
    {
        // Arrange
        var expected = new Something()
        _serviceOneMock
            .Setup(mock => mock.GetSomething(It.IsAny<int>()))
            .Returns(expected);

        // Act
        var _ = _sut.DoFoo(_random.Next())

        // Assert
        _serviceTwoMock.Verify(
            mock => mock.CompleteFoo(
                It.Is<Something>(actual => actual == expected)),
            Times.Once);
    }

    [Test]
    public void DoFoo_Should_ReturnTheResultFromFooServiceTwo()
    {
        // Arrange
        var expected = new FooResult();
        _serviceTwoMock
            .Setup(mock => mock.CompleteFoo(It.IsAny<Something>()))
            .Returns(expected);

        // Act
        var actual = _sut.DoFoo(_random.Next());

        // Assert
        Assert.AreEqual(expected, actual); 
    }
}

Het bovenstaande zal menig ontwikkelaar bekend voorkomen. Maar als we beter gaan kijken naar wat deze testen doen en welke kwaliteitsgarantie de testen met zich meebrengen. Dan zullen we verbaasd zijn.

Als eerste zien wij dat de class het gedrag DoFoo heeft. En om dit gedrag te realiseren besteedt FooController gedeeltes van de stappen uit aan onderliggende services. Ik moet hierbij benadrukken dat deze services geen I/O doen en dat we van I/O nog wel een aantal lagen verwijderd zijn. De testen “lijken” goed het gedrag van DoFoo te testen. De eerste test checkt of de input van DoFoo aan de eerste service doorgegeven wordt. De tweede test of het resultaat van de eerste service aan de tweede service doorgegeven wordt. En de laatste test of het resultaat van de tweede service ook het resultaat van DoFoo is. Dit lijkt allemaal correct. De unittesten zijn “geïsoleerd”, want de dependencies zijn gemockt. En de unittesten testen het “gedrag” dat data van het ene object naar het daaropvolgende object doorgegeven wordt. Deze conclusie is fout!

Ten eerste wil het mocken van je dependencies niet zeggen dat je unittest geïsoleerd is. Het belang van een geïsoleerde unittest is dat deze de productiecode test in combinatie met de input van de unittest en dat andere factoren geen invloed hebben op het resultaat. Het resultaat van andere unittesten mogen niet deze test beïnvloeden, ongeacht of de andere testen falen en ongeacht of de andere testen wel of niet goed geschreven zijn en wel of niet artefacten achterlaten. Een geïsoleerde test wil niet zeggen dat je system-under-test uit een enkele class moet bestaan. Als het gedrag van je systeem uitgevoerd wordt door meerdere classes dan is dit ook goed. Bij een unittest is het belangrijk dat alles in memory draait en geen I/O doet (schrijven naar disk of een aanroep naar een netwerkconnectie); en geen geforceerde wachttijden heeft voor hardware stabilisatie of iets dergelijks. Als een unittest in-memory draait zonder I/O dan is deze al zeer snel en is er geen kans op artefacten die andere testen beïnvloeden.

Ten tweede zijn implementatie details niet een gedrag. Neem bijvoorbeeld dat ik de functie heb dat ik van een locatie in Zwolle moet reizen naar een locatie in Den Helder. Mijn test is dat ik de functie uitvoer en controleer of ik ook daadwerkelijk op de locatie in Den Helder ben aangekomen. De test gaat niet controleren of ik gereisd ben via Amsterdam of via de Afsluitdijk. En ook niet of ik de auto, fiets of openbaar vervoer heb genomen. Dit zijn allemaal implementatie details en niet van belang voor het test resultaat. Het resultaat is of ik op de locatie in Den Helder arriveer. Test dus geen implementatie details!

Je zal nu wel zeggen, maar we hebben het hier over software en niet over het reizen van Zwolle naar Den Helder. Dat is juist de reden dat ik in mijn voorbeeld FooController en DoFoo heb gebruikt. Dit zijn op zich niets zeggende namen. Maar wat als ik DoFoo vervang met ExecuteFinancialBackgroundCheck(int person)HasAccess(int person) of DeleteLibraryBook(int book)? Als je die functie namen ziet ga je er dan direct van uit dat het gedrag is dat er onderliggende services aangeroepen worden. Nee, als ik HasAccess(int person) zie ga ik ervan uit dat het gedrag van de functie is, dat deze een validatie uitvoert op de persoon en mij als resultaat teruggeeft of die persoon toegang heeft, mits de persoon ook bestaat. Het is voor het aanroepen van die functie niet van belang dat dit via onderliggende services verloopt. Als dat wel van belang had moeten zijn, dan was de functie naam HasAccessByCallingServiceOneAndPassingTheResultToServiceTwoAndReturningThatResult(int person) geweest. Ja, dat is een functie naam die niemand graag ziet. Goed dat we IntelliSense hebben.

Als derde probleem met deze aanpak is dat je geen ruimte over laat voor toekomstig herstructureren en verbeteren van je code. Bedenk goed dat je automatische testen de kwaliteit van je product moeten waarborgen en als een test faalt dan heb je een fout in je systeem geïntroduceerd. Als in de toekomst meerdere functionaliteiten erbij komen die maar voor een gedeelte de GetSomething van IFooServiceOne gebruiken, de code uit GetSomething op meerdere plaatsen gedupliceerd is, of bij het opschonen van je code erachter komt dat GetSomething en IFooServiceOne niet voldoen aan de SOLID principes, dan ga je je code herstructureren. Het gedrag van je code verandert niet, de implementatie wel. Herstructureren is ook overbodige code-fluff weghalen waardoor je uiteindelijk met de volgende implementatie van IFooController overblijft.

public FooController : IFooController
{
    private readonly IFooServiceOneOne _serviceOneOne;
    private readonly IFooServiceOneTwo _serviceOneTwo;
    private readonly IFooServiceTwo _serviceTwo;

    public FooController(
        IFooServiceOneOne serviceOneOne,
        IFooServiceOneTwo serviceOneTwo,
        IFooServiceTwo serviceTwo)
    {
        _serviceOneOne = serviceOneOne;
        _serviceOneTwo = serviceOneTwo;
        _serviceTwo = serviceTwo;
    }

    public FooResult DoFoo(int fooInput)
    {
        var something = 
            _serviceOneOne.GetSomething(fooInput);
        var anotherThing = 
            _serviceOneTwo.GetAnotherThing(fooInput, something.Part);
        return _serviceTwo.CompleteFoo(something, anotherThing);
    }
}

Je begrijpt nu wel dat alle unittesten die je voor FooController had gemaakt, nu kapot zijn. Sterker nog, het compileert niet eens meer. Als het goed is heb je nu een seintje dat er een bug in je systeem zit en dat je de productiecode moet gaan repareren. Jammer genoeg, kan dit alleen door de productiecode terug te brengen naar de originele staat. Je kan dan zeggen dat je IFooServiceOne behoudt en alleen daar de veranderingen in aan brengt. Maar voor IFooServiceOne heb je ook soortgelijke unittesten gemaakt, die dan gaan breken. Sterker nog, in de bovenstaande verandering wordt IFooServiceOne geheel overbodig en kan verwijderd worden. Dus ga je de code dan in zijn geheel terugbrengen naar de originele staat? Nee! Het doel was juist om de code op te schonen en alle rotzooi in de code, die de ontwikkelingen van nieuwe functies vertragen, eruit te halen.

Er zit nog maar één ding op. Om de code op te kunnen schonen en toch weer alle unittesten te laten slagen gaan we de unittesten aanpassen. Je krijgt dan de volgende unittesten:

[TestFixture]
public class FooControllerTests
{
    private static Random _random = new Random();
    private Mock<IFooServiceOneOne> _serviceOneOneMock;
    private Mock<IFooServiceOneTwo> _serviceOneTwoMock;
    private Mock<IFooServiceTwo> _serviceTwoMock;
    private FooController _sut;

    [SetUp]
    public void SetUp()
    {
        _serviceOneOneMock = new Mock<IFooServiceOneOne>();
        _serviceOneTwoMock = new Mock<IFooServiceOneTwo>();
        _serviceTwoMock = new Mock<IFooServiceTwo>();
        _sut = new FooController(
            _serviceOneOneMock.Object,
            _serviceOneTwoMock.Object,
             _serviceTwoMock.Object);
    }	

    [Test]
    public void DoFoo_Should_CallFooServiceOneOneWithTheInput()
    {
        // Arrange
        var expected = _random.Next();

        // Act
        var _ = _sut.DoFoo(expected);

        // Assert
        _serviceOneOne.Verify(
            mock => mock.GetSomething(
                It.Is<int>(actual => actual == expected)),
            Times.Once);
    } 

[Test]
    public void DoFoo_Should_CallFooServiceOneTwoWithTheInputAndResultFromFooServiceOneOne()
    {
        // Arrange
        var expectedInput = _random.Next();
        var expectedPart = new Part();
        _serviceOneOneMock
            .Setup(mock => mock.GetSomething(It.Is<int>(actual => actual == expectedInput)))
            .Returns(new Something() { Part = expectedPart });

        // Act
        var _ = _sut.DoFoo(expectedPart);

        // Assert
        _serviceOneTwoMock.Verify(
            mock => mock.GetAnotherThing(
                It.Is<int>(actual => actual == expectedInput),
                It.Is<Part>(actual => actual == expectedPart),
            Times.Once);
    }

    [Test]
    public void DoFoo_Should_CallFooServiceTwoWithResultsFromFooServiceOneOneAndSerivceOneTwo()
    {
        // Arrange
        var expectedSomething = new Something();
        var expectedAnotherThing = new AnotherThing();
        _serviceOneOneMock
            .Setup(mock => mock.GetSomething(It.IsAny<int>()))
            .Returns(expectedSomething);
        _serviceOneTwoMock
            .Setup(mock => mock.GetAnotherThing(It.IsAny<int>(), It.IsAny<Part>()))
            .Returns(expectedAnotherThing);

        // Act
        var _ = _sut.DoFoo(_random.Next())

        // Assert
        _serviceTwoMock.Verify(
            mock => mock.CompleteFoo(
                It.Is<Something>(actual => actual == expectedSomething),
                It.Is<AnotherThing>(actual => actual == expectedAnotherThing)),
            Times.Once);
    }

    [Test]
    public void DoFoo_Should_ReturnTheResultFromFooServiceTwo()
    {
        // Arrange
        var expected = new FooResult();
        _serviceTwoMock
            .Setup(mock => mock.CompleteFoo(It.IsAny<Something>()))
            .Returns(expected);

        // Act
        var actual = _sut.DoFoo(_random.Next());

        // Assert
        Assert.AreEqual(expected, actual); 
    }
}

De reden is omdat de classes niet meer zo geïmplementeerd zijn als dat waar de unittesten op testen. Dit is de eerste indicatie dat je unittesten eigenlijk helemaal niet het gedrag van je units testen. Maar erger nog, je begaat nu een grote zonde wat betreft geautomatiseerde testen. In plaats van dat je testen aangeven dat je een bug hebt gemaakt in je productiecode, zeg je nu dat de testen verkeerd zijn en gerepareerd moeten worden en dat jouw code correct en bug vrij is. En des te vaker een dergelijke situatie voorkomt, des te sneller je de conclusie gaat trekken dat je testen verkeerd zijn en jouw code correct. Hiermee bega je de grootste fout die je kunt maken, want je schendt nu volledig het vertrouwen in de geautomatiseerde testen.

Je kan niet meer vertrouwen op je geautomatiseerde testen dat als ze falen dat er dan ook werkelijk een bug in zit, want het kan net zo goed een herstructurering zijn. Een falende test die daadwerkelijk een bug aantoont kan ook heel snel aangezien worden als een veranderde implementatie, waardoor er eigenlijk bugs insluipen. Hierdoor geldt de regel dat unittesten snel bugs opsporen ook niet meer. Als je een class als je harde limiet van een unit ziet, dan worden je unittesten eigenlijk een reflectie of een tweede implementatie van je productiecode. We zeggen wel dat een ontwikkelaar die geen testen maakt en de mentaliteit heeft van “It works on my machine” heel erg arrogant is. Maar wat zegt het over een ontwikkelaar die wel testen maakt, maar bewust of onbewust dezelfde mentaliteit heeft. Moeten wij zijn werk accepteren, want hij heeft geautomatiseerde testen die slagen, maar eigenlijk een reflectie van zijn productiecode is? Inclusief alle bugs?

Afrondend

Ik begon deze blog met dat ik de kriebels krijg als iemand hardop roept dat hij volgens TDD werkt. Dit is niet zo zeer om het feit dat het TDD is, maar omdat de meeste ontwikkelaars voor unittesten de class als harde grens stellen. Wat ik vroeger zelf ook deed was enthousiast te beginnen met het maken van een unittest voor de class en functie die ik ging maken, maar niet zo zeer over het probleem nadacht. Ik ging een testen schrijven voor iedere statement in een functie. Ik had de code dat moest worden geschreven al in mijn hoofd zitten. De onderdelen waarvan ik niet wist hoe het algoritme van input naar output ging werden weggezet naar andere classes die ik dan ging mocken.

Hierdoor had ik 100% code coverage. Volgde ik de stappen van TDD, want dat was “good practice”. Maar het product bevatte net zoveel bugs als dat ik het niet gedaan zou hebben. Ik ben van mening dat een unittest meer bevat dan de code van een enkele class. Als het nodig is dan is het goed om meerdere classes erbij te betrekken. Wat een unit in een unittest maakt is niet gedefinieerd door de programmeertaal. Naar mijn mening wordt een unit meer gedefinieerd door de business rules en de functies die je software waarde geven dan de implementatie. Maar hier ga ik in het volgende deel van deze blog dieper op in.

Referenties:

Boek

  • Clean Architecture: A Craftsman’s Guide to Software Structure and Design geschreven door Robert C. Martin

Video’s

Blogs