Betere tests met Test Data Builders 🔨

Betere tests met Test Data Builders 🔨

Refactoren en daarna uren je tests aanpassen? Geen idee wat een test doet? Los het op met Test Data Builders!

Tests en onderhoudbaarheid 🧪

De meeste developers snappen het belang van tests: ze geven vertrouwen. Tests stellen je in staat om veilig te refactoren1.

Deze ontwikkelaars weten dat onderhoudbare code enorm belangrijk is. Minder mensen weten dat dit voor testcode net zo cruciaal is. Wanneer onze tests niet onderhoudbaar zijn zal dit negatieve effecten hebben. Refactoren wordt dan lastiger, wat op den duur leidt tot slechtere productiecode1.

what if i told you maintainability of tests matters

Twee mogelijke oorzaken van slecht onderhoudbare tests zijn:

  1. Te veel koppeling door duplicatie in tests.
  2. Tests die niet expressief zijn.

Laten we eens zien waarom deze aspecten invloed hebben op de onderhoudbaarheid van onze testcode.

De pijn van koppeling 🔗

Je hebt het vast meegemaakt: er moet iets aan de code worden aangepast en dat is zo gepiept. Het probleem is dat je daarna uren bezig bent om tests aan te passen.

Hoe kan dat?

Tests zijn een vorm van koppeling. Dat moet ook wel, hoe kunnen we anders onze productiecode aanroepen? Het probleem ontstaat wanneer we signatures in onze code aanpassen, zoals het introduceren van een extra parameter. De koppeling wordt dan op een pijnlijke manier duidelijk: alle tests die dat stuk code gebruikten moet je aanpassen…

In de productiecode houden we ons aan het “Don’t Repeat Yourself” principe, bij tests is dit vaak lastiger. Je hebt nou eenmaal variaties van objecten nodig, waardoor je in tests vaker objecten maakt dan in de productiecode. Dit leidt tot een subtiele vorm van duplicatie, namelijk de aanroep van constructors.

tests gekoppeld aan verschillende objecten
Fig 1. Koppeling vanuit tests door het creëren van objecten

Deze vorm van koppeling heeft een negatief effect op de onderhoudbaarheid van tests, maar dat is niet het enige waar we op moeten letten…

Het belang van expressieve tests 🗣️

Leesbare code is belangrijk. Goede developers begrijpen dat code vaker wordt gelezen dan geschreven, wat voor tests net zo goed waar is.

Tests helpen lezers het gedrag van code te begrijpen. Dit is waarom ze dienen als een effectieve vorm van documentatie.

Helaas kost dit enige moeite. Om tests het gedrag van code effectief uit te laten leggen is het belangrijk dat je hoofd van bijzaak scheidt, maar wat is hoofd- en bijzaak in een test?

Simpel. Het belangrijkste in een test is het wat (het gedrag), niet het hoe (de opzet). In listing 1 zie je een voorbeeld van een niet-expressieve test, doordat het geteste gedrag niet van de testopzet is gescheiden.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
void an_unreadable_test() {
    Country country = new Country("USA", Currency.US_DOLLAR, Language.ENGLISH);
    Author author = new Author("Oscar Wilde", country);
    Novel novel = new Novel(
            "The picture of dorian gray",
            50.00,
            author,
            Language.ENGLISH,
            Lists.newArrayList(Genre.MYSTERY)
    );
    PurchasedBook book = new PurchasedBook(novel, 1);
    Invoice invoice = new Invoice("test", country);

    invoice.addPurchasedBook(book);

    assertEquals(56.35, invoice.computeTotalAmount());
}

Listing 1. Slecht leesbare test, de intentie wordt verborgen door het mechanisme

Als deze zaken niet goed gescheiden zijn, vertelt een test niet wat er gebeurt. Dit zorgt ervoor dat lezers langer bezig zijn het te begrijpen, of erger nog, dat ze het niet durven aan te passen. Gelukkig hoeft het niet zo ver te komen, dankzij Test Data Builders.

Wat zijn Test Data Builders 👷

Test Data Builders zijn een vorm van het Builder Pattern, maar dan toegepast op het maken van objecten voor tests2. Deze builders maken objecten aan met veilige, logische default waarden. Ze bieden, zoals de gewone builder, publieke chainable methodes aan waarmee de objecten aangepast kunnen worden.

Dit pattern is niet altijd toepasbaar. Het is effectief bij complexe objectstructuren, vooral voor Value Objects of Entities.

Naast de voordelen zijn er toch nadelen. Je moet deze builders zelf schrijven en onderhouden. Er kunnen dus ook bugs in je builders ontstaan. Ga niet gelijk overal builders voor maken, maar maak een afweging!

Hoe Test Data Builders helpen ✨

Test Data Builders helpen je testcode meer te ontkoppelen van je productiecode en tegelijkertijd de expressiviteit ervan te verhogen. Dat klinkt bijna als magie, maar zoals je straks zult zien, is het in essentie eenvoudig.

Minder koppeling 🎊

Test Data Builders lossen het koppelingprobleem op door constructie van objecten te encapsuleren. Je gebruikt de builder om objecten voor je test te maken, waardoor het aantal plekken waar dit gebeurt teruggebracht wordt. Stel dat je een argument toevoegt, dan hoef je nu alleen nog maar code in de builder aan te passen!

ontkoppel tests van productiecode met Test Data Builders
Fig 2. Test Data Builders ontkoppelen tests van de creatie van objecten

Verhoogde Expressiviteit 🎉

Het expressiviteitsprobleem wordt ook opgelost door Test Data Builders. Doordat ze objecten aanmaken met veilige defaults hoef je alleen relevante waardes voor een test aan te passen. Stel dat je één property wilt valideren, dan is dat de enige die je overschrijft bij het maken van het object, de rest doet er niet toe. Op deze manier verminder je technische “clutter” en vertelt de test meer! Vergelijk listing 1 maar eens met listing 2.

1
2
3
4
5
6
7
8
9
10
11
@Test
void increased_readability_by_test_data_builder() {
    Invoice invoice =
        anInvoice()
            .from(USA)
            .with(
                aPurchasedBook().of(aNovel().costing(50.0))
            ).build();

    assertEquals(56.35, invoice.computeTotalAmount());
}

Listing 2. Test Data Builders maken de intentie van een test duidelijker

Bonus: Test DSL 🎁

Een bijkomend voordeel is dat je met builder methodes meer kunt spreken in de termen van je domein. Als je listing 1 met listing 2 vergelijkt dan zie je dat de builder methodes meer zeggen dan een constructoraanroep of het zetten van een property. Benoem je de methodes van de builders goed, dan eindig je met een Domain Specific Language voor je tests!

Conclusie 📝

Tests zijn belangrijk voor een goede codebase, maar als ze niet onderhoudbaar zijn kunnen ze aanpassingen juist lastig maken. Dit kan komen door koppeling in tests, of door tests die niet expressief zijn.

Test Data Builders bieden een oplossing voor beide problemen. Ze maken het mogelijk om testdata grotendeels op één plek op te bouwen en voorkomen daarmee onnodige koppeling. Daarnaast zorgen ze voor tests die duidelijker laten zien welk gedrag getest wordt.

Zoals met elk pattern moet je ook de nadelen meenemen in de overweging om ze toe te passen. Doe je dit goed dan zullen ze je tests flexibeler en expressiever maken!

Denk de volgende keer bij het schrijven van een test dus aan deze effectieve oplossing. Maak het jezelf en je team gemakkelijk!

Wat zijn jouw ervaring met Test Data Builders? Zie jij nog andere voor- of nadelen? Deel het!

Referenties 🌐

  1. Martin, F. (2018). Refactoring: Improving the Design of Existing Code (Addison-Wesley Signature Series (Fowler)) (2nd ed.). Addison-Wesley Professional.  2

  2. Freeman, S., Pryce, N. (2009). Growing Object-Oriented Software, Guided by Tests (1st ed.). Addison-Wesley Professional.