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:

  1. Beide tests laten staan.
  2. test1 verwijderen.
  3. 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.