Het artikel bespreekt twee essentiële eigenschappen van softwaretests op basis van de Test Desiderata: isolatie en compositie.
Isolatie zorgt ervoor dat tests volledig onafhankelijk van elkaar zijn, wat vaak wordt bereikt door per test een nieuwe test fixture aan te maken. Compositie richt zich op het voorkomen van redundantie. In plaats van bestaande tests te kopiëren en uit te breiden, stelt de auteur voor om tests zo te 'snoeien' dat ze complementair zijn aan elkaar.
De voordelen van deze benadering worden toegelicht aan de hand van een 'N x M' scenario, waarbij compositie het aantal benodigde tests aanzienlijk kan verminderen (bijvoorbeeld van 20 naar 10) zonder dat dit ten koste gaat van de betrouwbaarheid. Volgens de auteur leidt dit tot tests die:
- Sneller en leesbaarder zijn
- Makkelijker aan te passen zijn
- Specifieker zijn en minder gevoelig voor structurele wijzigingen
De auteur concludeert dat het bekijken van de testsuite als een samenhangend geheel superieur is aan het simpelweg maximaliseren van assertions in elke individuele test.
Componeerbare Tests
- Isolatie — het resultaat van het uitvoeren van één test moet volledig onafhankelijk zijn van de resultaten van andere tests.
- Compositie — ??? tests zouden samen moeten draaien ??? Is dat niet hetzelfde als isolatie?
Nee, en dit is waarom (ik heb eindelijk een voorbeeld gevonden — voorbeelden zijn altijd het moeilijkste deel).
Isolatie
Als een test begint met het opzetten van zijn eigen test fixture en alle data die als input wordt gebruikt vanaf nul creëert, dan is gegarandeerd dat die test geïsoleerd is. Het maakt niet uit in welke volgorde je de tests uitvoert; de resultaten zullen exact hetzelfde zijn. (Dit is dezelfde eigenschap als referentiële transparantie in functioneel programmeren.)
Isolatie wordt in de xUnit-testframeworks (althans in de meeste) gestimuleerd door voor elke test een nieuwe instantie van een testobject aan te maken en de setUp()-functie uit te voeren voordat de test start. (Sommige frameworks, met name NUnit, hergebruiken testinstanties, wat de deur openzet voor het doorbreken van isolatie.)
Compositie
Stel dat we een suite van geïsoleerde tests hebben en we voeren deze allemaal samen uit. Het succes van de suite zou ons vertrouwen moeten geven (in de termen van de Desiderata: het zou voorspellend moeten zijn), ook al is elke individuele test op zichzelf niet uitputtend.
Voorbeeld — stel dat we de volgende test hebben:
test1()
object := new Whatever()
actual := object.doSomething()
assertEquals(expected, actual)
We krijgen dit werkend en willen vervolgens de volgende functionaliteit implementeren. We kopiëren, plakken en breiden uit:
test2()
object := new Whatever()
actual := object.doSomething()
assertEquals(expected, actual)
actual2 := object.nowSomethingElse()
assertEquals(expected2, actual2)
Ik heb tests gezien die op deze manier zes of zeven keer zijn gekopieerd, geplakt en uitgebreid. Die laatste test is erg moeilijk leesbaar.
Merk op dat test2 niet kan slagen als test1 faalt. Alle niet-conforme programma's die door test1 worden opgemerkt, zullen ook door test2 worden opgemerkt. We hebben minstens drie opties die dezelfde dekking en dezelfde voorspelbaarheid behouden:
- Beide tests laten staan.
test1 verwijderen.
test2 vereenvoudigen.
Snoeien
Vanuit een puur esthetisch standpunt (en onderschat esthetiek niet), stoot het me af om beide tests zo te laten staan. Ze zijn redundant! Er moet iets mis zijn.
Het verwijderen van test1 doet ons echter een andere eigenschap uit de Test Desiderata verliezen: tests moeten specifiek zijn. Dat is de eigenschap waarbij je, wanneer een test faalt, precies weet waar het probleem zit.
Dit leidt tot mijn voorkeursoplossing: compositie. Ik trim test2 om de puur redundante delen te vermijden:
test2()
object := new Whatever()
object.doSomething()
actual := object.nowSomethingElse()
assertEquals(expected, actual)
De compositie van test1 + test2 is niet verloren gegaan in voorspellende waarde. De specifieke eigenschap is ook behouden. Sterker nog, de compositie kan specifieker zijn, aangezien het mogelijk is dat test1 faalt terwijl test2 slaagt (hoewel ze beide kunnen falen vanwege een gemeenschappelijke reden).
N x M
Stel dat we vier manieren hebben om rente te berekenen en vijf manieren om die rente te rapporteren. De brute force benadering om dit te testen is 20 tests. Met behulp van compositie kunnen we echter hetzelfde vertrouwen in ons systeem bereiken met 10 tests. Als de varianten van het berekenen van de rente (in functionele programmeersin) gescheiden zijn van de varianten van het rapporteren, dan hebben we nodig:
- 4 tests voor de berekening
- 5 tests voor de rapportage
- 1 test die berekening en rapportage combineert, om aan te tonen dat ze correct met elkaar verbonden zijn.
Vertrouwen putten uit gecomponeerde tests vereist enige nadenken, deductie en ontwerp (om de orthogonale dimensies aantoonbaar orthogonaal te maken), maar de investering in het schrijven hiervan betaalt zich terug door tests die:
- Sneller zijn
- Leesbaarder zijn
- Makkelijker aan te passen zijn
- Specifieker zijn
- Minder gevoelig zijn voor structurele wijzigingen
Kritiek
Wanneer ik uitleg wat ik bedoel met componeerbare tests, krijg ik vaak geschokte reacties van ervaren test-ontwikkelaars. "Ik zou nooit de assertions in een test verminderen." Dit lijkt me een reactie gebaseerd op angst, niet op principe. We hebben zo hard gewerkt om überhaupt tests te kunnen schrijven; we kunnen ze niet slechter maken.
Compositie maakt tests niet slechter. Compositie is het bekijken van de tests als een geheel, in een poging het geheel te verbeteren, getoetst aan verschillende waardevolle eigenschappen van tests.
Componeerbare Tests
- Isolatie — het resultaat van het uitvoeren van één test moet volledig onafhankelijk zijn van de resultaten van andere tests.
- Compositie — ??? tests zouden samen moeten draaien ??? Is dat niet hetzelfde als isolatie?
Nee, en dit is waarom (ik heb eindelijk een voorbeeld gevonden — voorbeelden zijn altijd het moeilijkste deel).
Isolatie
Als een test begint met het opzetten van zijn eigen test fixture en alle data die als input wordt gebruikt vanaf nul creëert, dan is gegarandeerd dat die test geïsoleerd is. Het maakt niet uit in welke volgorde je de tests uitvoert; de resultaten zullen exact hetzelfde zijn. (Dit is dezelfde eigenschap als referentiële transparantie in functioneel programmeren.)
Isolatie wordt in de xUnit-testframeworks (althans in de meeste) gestimuleerd door voor elke test een nieuwe instantie van een testobject aan te maken en de setUp()-functie uit te voeren voordat de test start. (Sommige frameworks, met name NUnit, hergebruiken testinstanties, wat de deur openzet voor het doorbreken van isolatie.)
Compositie
Stel dat we een suite van geïsoleerde tests hebben en we voeren deze allemaal samen uit. Het succes van de suite zou ons vertrouwen moeten geven (in de termen van de Desiderata: het zou voorspellend moeten zijn), ook al is elke individuele test op zichzelf niet uitputtend.
Voorbeeld — stel dat we de volgende test hebben:
test1()
object := new Whatever()
actual := object.doSomething()
assertEquals(expected, actual)
We krijgen dit werkend en willen vervolgens de volgende functionaliteit implementeren. We kopiëren, plakken en breiden uit:
test2()
object := new Whatever()
actual := object.doSomething()
assertEquals(expected, actual)
actual2 := object.nowSomethingElse()
assertEquals(expected2, actual2)
Ik heb tests gezien die op deze manier zes of zeven keer zijn gekopieerd, geplakt en uitgebreid. Die laatste test is erg moeilijk leesbaar.
Merk op dat test2 niet kan slagen als test1 faalt. Alle niet-conforme programma's die door test1 worden opgemerkt, zullen ook door test2 worden opgemerkt. We hebben minstens drie opties die dezelfde dekking en dezelfde voorspelbaarheid behouden:
- Beide tests laten staan.
test1 verwijderen.
test2 vereenvoudigen.
Snoeien
Vanuit een puur esthetisch standpunt (en onderschat esthetiek niet), stoot het me af om beide tests zo te laten staan. Ze zijn redundant! Er moet iets mis zijn.
Het verwijderen van test1 doet ons echter een andere eigenschap uit de Test Desiderata verliezen: tests moeten specifiek zijn. Dat is de eigenschap waarbij je, wanneer een test faalt, precies weet waar het probleem zit.
Dit leidt tot mijn voorkeursoplossing: compositie. Ik trim test2 om de puur redundante delen te vermijden:
test2()
object := new Whatever()
object.doSomething()
actual := object.nowSomethingElse()
assertEquals(expected, actual)
De compositie van test1 + test2 is niet verloren gegaan in voorspellende waarde. De specifieke eigenschap is ook behouden. Sterker nog, de compositie kan specifieker zijn, aangezien het mogelijk is dat test1 faalt terwijl test2 slaagt (hoewel ze beide kunnen falen vanwege een gemeenschappelijke reden).
N x M
Stel dat we vier manieren hebben om rente te berekenen en vijf manieren om die rente te rapporteren. De brute force benadering om dit te testen is 20 tests. Met behulp van compositie kunnen we echter hetzelfde vertrouwen in ons systeem bereiken met 10 tests. Als de varianten van het berekenen van de rente (in functionele programmeersin) gescheiden zijn van de varianten van het rapporteren, dan hebben we nodig:
- 4 tests voor de berekening
- 5 tests voor de rapportage
- 1 test die berekening en rapportage combineert, om aan te tonen dat ze correct met elkaar verbonden zijn.
Vertrouwen putten uit gecomponeerde tests vereist enige nadenken, deductie en ontwerp (om de orthogonale dimensies aantoonbaar orthogonaal te maken), maar de investering in het schrijven hiervan betaalt zich terug door tests die:
- Sneller zijn
- Leesbaarder zijn
- Makkelijker aan te passen zijn
- Specifieker zijn
- Minder gevoelig zijn voor structurele wijzigingen
Kritiek
Wanneer ik uitleg wat ik bedoel met componeerbare tests, krijg ik vaak geschokte reacties van ervaren test-ontwikkelaars. "Ik zou nooit de assertions in een test verminderen." Dit lijkt me een reactie gebaseerd op angst, niet op principe. We hebben zo hard gewerkt om überhaupt tests te kunnen schrijven; we kunnen ze niet slechter maken.
Compositie maakt tests niet slechter. Compositie is het bekijken van de tests als een geheel, in een poging het geheel te verbeteren, getoetst aan verschillende waardevolle eigenschappen van tests.