afbeelding van man die verwart naar logo's van Karma en Protractor wijst
Romke van der Meulen

Dit is het verhaal van een fout, een reis en voortschrijdend inzicht. Een verhaal van twee web test tools – Protractor en Karma – met een verschillende aanpak. En een verhaal over hoe ik erachter kwam dat je sommige problemen beter met de ene tool op kunt lossen dan met de ander. Hopelijk kun jij er je voordeel mee doen en direct de juiste keuzes maken. Want geloof mij: je kunt je Karma niet ontvluchten 😉

Protractor vs. Karma

Een lange tijd geleden schreef ik een artikel over Protractor, een test-framework waarmee je websites automatisch kunt testen in de browser. Protractor is gebouwd op basis van WebDriver. Bij WebDriver draaien de tests in een apart proces en praten van buitenaf met een browser die jouw website laadt. Er zijn ook tools die de tests en de te testen code beide binnenin de browser draaien. Van deze tools is Karma de meest bekende.

Het voordeel van tools als Protractor die van buitenaf de browser besturen is dat ze het gedrag van eindgebruikers heel natuurgetrouw nabootsen. Een klik in een Protractor test bijvoorbeeld verschilt niet van de klik die de browser ontvangt wanneer een gebruiker op een knop van de muis drukt. Tests die zelf in de browser draaien kunnen een muisklik wel simuleren maar deze simulatie verschilt met wat je in het echt ziet, waardoor je misschien bugs mist. Verder test je met Protractor ook het gehele gedrag van je app zoals die in het echt werkt: van de input van de gebruiker tot de communicatie met de backend.

Het voordeel van tools als Karma is dat ze heel veel sneller werken: de tests en de code draaien samen in de browser en kunnen direct tegen elkaar praten. Hierdoor kun je in je tests ook meer controle uitoefenen in het afzonderen van onderdelen van code voor de test. Dit lijkt minder op hoe de app in zijn geheel zal draaien in productie maar zorgt er wel weer voor dat je uitzonderlijke situaties in detail kunt nabootsen.

Manieren van testen

Er zijn verschillende manieren om code te testen en de verschillen tussen Protractor en Karma maken deze tools geschikt voor verschillende soorten tests. Unit tests bijvoorbeeld testen kleine stukjes code in isolatie. Je geeft een heleboel verschillende waardes als invoer en checkt vervolgens of het resultaat dat je terugkrijgt overeenkomt met wat je verwacht. Het natuurlijk nabootsen van gebruikers is hierbij niet aan de orde terwijl het goed kunnen isoleren van code en het snel kunnen uitvoeren van meerdere tests een eis is. Karma is hier dus uitermate geschikt voor.

Aan het andere einde van het spectrum heb je een aanpak die bekend staat als “end-to-end tests”. Hierbij test je de applicatie in zijn geheel in een situatie die zo dicht mogelijk benadert hoe de code in het echt werkt. In een extreem geval kun je deze tests zelfs direct tegen de productieomgeving uitvoeren, hoewel je hierbij natuurlijk geen controle hebt over de staat van je backend. Voor dit soort tests is isolatie van code niet van belang maar een natuurgetrouwe simulatie van het gedrag van echte gebruikers juist weer wel. Protractor is een zeer geschikte tool voor dit soort tests.

En dan heb je ook nog een niveau van testen tussen beide extremen in. Hierbij test je wel een compleet onderdeel van je code, meer dan in een unit test gebeurt. Alleen test je dit onderdeel in tegenstelling tot end-to-end tests wel in isolatie. Afhankelijk van wie je het vraagt worden tests op dit niveau wel component tests of integratie tests genoemd.

De verkeerde tool

Een lange tijd geleden heb ik voor het ontwikkelen van MyVevida dit soort component tests geïntroduceerd. Ik koos daarbij voor Protractor. Uiteindelijk hadden mijn collega’s en ik een test suite geschreven van een kleine 400 tests. Het uitvoeren van de complete test suite koste echter zo’n tien minuten. Dit is wel heel lang voor een ontwikkelaar om op te wachten. Het resultaat was dan ook dat we meestal alleen de tests uitvoerden van het onderdeel waarmee we bezig waren. En een van de doelen van automatische tests is nu juist dat het je waarschuwt wanneer je onbedoeld het gedrag verandert van onderdelen waar je niet mee bezig bent.

Bovendien bleek Protractor bijzonder gevoelig voor timing-problemen. Protractor communiceert immers van buitenaf met de browser. Dat kost natuurlijk tijd. En in de tussentijd kan de state van je pagina onverwacht alweer veranderd zijn waardoor je tests mislukken. Daar zijn wel oplossingen voor te verzinnen: zie bijvoorbeeld browser.wait en ExpectedConditions uit mijn eerdere artikel over Protractor. Maar het is moeilijk te voorspellen waar zulke timing-problemen optreden: als er net even op het verkeerde moment een CPU-spike is tijdens een test run zie je een test die al honderd keer geslaagd is misschien opeens falen. En om de browser.wait workarounds op elke test toe te passen is ook geen schaalbare oplossing.

Kortom, ik had de verkeerde tool gekozen om deze tests mee te bouwen. Mijn bedoelingen waren goed, en zelfs een onbetrouwbare test-suite is beter dan helemaal niets. Maar wat ik voor ogen had was een test suite die echt behulpzaam is in het voorkomen van bugs en bovendien betrouwbaar en snel genoeg om regelmatig helemaal uit te voeren. Toen ik nieuwe tests begon te bouwen voor ons bestelproces ging ik op zoek naar een betere oplossing.

aurelia-testing

We hebben bij Vevida ervoor gekozen om onze frontend code te bouwen op basis van het Aurelia framework (zie onze JavaScript framework vergelijking). Nu heeft Aurelia een plugin genaamd aurelia-testing die het mogelijk maakt om tests voor Aurelia code te maken die in Karma gedraaid kunnen worden. Deze plugin zorgt ervoor dat aan het begin van elke test het Aurelia framework gestart wordt en het te testen component wordt ingeladen. Daarna kun je muisklikken en tekstinvoer simuleren en checken dat het component zich gedraagt zoals je wilt.

Daar komt het voordeel ook nog eens bij dat, omdat je tests nu naast Aurelia in de browser draaien, je directe toegang hebt tot de Aurelia Dependency Injection (DI) container. Deze container linkt al je Aurelia code aan elkaar en verbindt het met de code die tegen de backend praat. Je kunt de code voor backend-communicatie dus vervangen door een mock-implementatie: hierdoor kun je het gedrag van de backend in je tests simuleren en allerlei bijzondere situaties nadoen. De Mock Backend die we voor Protractor hadden gemaakt ondersteunt dit ook maar was een stuk moeilijker te bouwen en biedt veel minder controle.

Ik koos er dan ook voor de component tests voor het bestelproces te bouwen op basis van aurelia-testing en Karma. Het verschil met de Protractor tests was verbluffend: de tests draaiden vele malen sneller en de te testen componenten werden veel beter afgescheiden van de rest van de code. Voor het bestelproces hebben we inmiddels een kleine 300 component tests. Om al die tests uit te voeren heeft onze CI server maar 20 seconden nodig. Bovendien verzamelen we code coverage informatie via IstanbulJS om ervoor te zorgen dat elke stukje code bij de tests tenminste een keer uitgevoerd wordt. Dat garandeert natuurlijk niet dat de tests elk mogelijk probleem kunnen detecteren maar het is een goed begin.

Doordat deze tests zoveel sneller waren werd het veel makkelijker de complete test suite te draaien voordat je een wijziging doorvoert. Meestal slagen de tests gewoon maar het is me ook al een paar keer overkomen dat de tests een probleem vonden dat ik over het hoofd gezien had. Dit was precies hoe ik me een nuttige test suite had voorgesteld.

Het voordeel van net gescheiden code

Mijn ervaringen bij het bouwen van de tests voor het bestelproces waren dus heel plezierig. Daarom ben ik begonnen enkele van de component tests van MyVevida om te zetten van Protractor naar Karma. Hierbij bleek opnieuw hoe handig het is onderdelen van je test code netjes te scheiden. Wat met name belangrijk bleek was dat de te testen specificaties apart stonden van de “PO-laag” die daadwerkelijk Protractor aanroept. Want de specificaties die je wilt testen veranderen natuurlijk niet als je van test-tool wisselt: wat verandert is dat je in plaats van de Protractor API nu de Karma en aurelia-testing APIs aan moet roepen. Ik kan de specificaties dus met minimale wijzigingen overnemen en hoef alleen de code in de PO-laag te veranderen. Wat natuurlijk nog steeds wel wat tijd kost, maar je kunt niet alles hebben.

Echte end-to-end tests

Voor het bestelproces heb ik ook tests geschreven op de manier waar Protractor echt goed in is. Deze end-to-end tests bestaan uit een aantal scenario’s die overeenkomen met de use-cases die we bij het ontwikkelen van de app voor ogen hadden. Voor deze scenario’s maakte ik gebruik van CucumberJS. Dit is weer een andere geweldige test tool waar ik een andere keer meer over zal vertellen. Een scenario ziet er bijvoorbeeld zo uit:

Scenario: zoeken met een zoekterm die een domeinextensie bevat

  Gegeven dat de aanbevolen domeinextensies ingesteld staan op "nl", "be", en "com"
    En ik op de pagina "Kies je domeinnaam" ben

  Als ik de tekst "hallowereld.be" typ in invoerveld "domeinzoeker"
    En ik op de knop "check" klik

  Dan moeten als aanbevolen domeinkeuzes "hallowereld.be", "hallowereld.nl" en "hallowereld.com" getoond worden

Als het scenario eenmaal klaar is schrijf ik code die de tekst vertaalt naar aanroepen van Protractor. Deze worden uitgevoerd en praten van buitenaf met de browser. Waar in het scenario dus “typ in invoerveld” staat worden door de browser echte toetsaanslagen nagebootst.

Timing problemen kunnen hierbij natuurlijk nog steeds ontstaan. Maar er zijn veel minder end-to-end tests nodig dan component tests, waardoor deze problemen beter beheersbaar zijn. Bovendien was ik nu ook beter voorbereid. Door van tevoren goed na te denken over waar timing problemen zouden kunnen optreden zijn deze Protractor tests nu zo geschreven dat ze vrijwel nooit mislukken tenzij er echt iets mis is.

Er zijn nog veel meer tools

End-to-end tests, unit tests en component tests zijn waardevolle technieken in het bewaken van de kwaliteit van je code. Het is dan ook niet verrassend dat Karma en Protractor lang niet de enige tools zijn die deze technieken ondersteunen, al zijn ze wel het meest populair. Inmiddels heb ik aardig wat ervaring met deze twee tools en zal ik niet gauw overstappen op een andere tool. Maar als je nog moet beginnen met het schrijven van je tests dan zijn deze alternatieven ook zeker het overwegen waard:

  • Jest is een alternatief voor Karma, geschreven door Facebook en zo’n beetje de standaard binnen de React community.
  • Nightwatch en WebDriver.io zijn alternatieven voor Protractor die ook op WebDriver gebaseerd zijn.
  • TestCafe en Cypress zijn end-to-end testtools die echter niet van buitenaf met de browser praten maar in de browser draaien.

Onderzoek je tools

Je kunt een boel tijd verliezen als je een tool kiest, veel tijd steekt in het ontwikkelen van code voor deze tool, en er pas dán achter komt dat de tool niet de meest geschikte is voor het probleem dat je op wilt lossen. Wanneer je jouw toolchain aan het samenstellen bent is het dus een goed idee om ruim de tijd te nemen om elke tool goed te onderzoeken voordat je er hevig in investeert. Probeer elke tool uit, bouw wat experimenten, kijk naar de alternatieven. Zorg dat je goed in de gaten hebt wat de sterke en zwakke punten van de tool zijn (want de zwakke plekken zijn er altijd, wat superfans van de tool je ook vertellen). Vraag advies: gegarandeerd heeft een van je collega ontwikkelaars wel eens voor deze keuze gestaan. Bedwing je enthousiasme om direct te beginnen met code schrijven. Geloof mij, ik spreek uit ervaring.

Romke van der Meulen Code-goochelaar

“Ik ben developer bij Vevida. Elke dag leer ik nieuwe dingen over het ontwikkelen van webapps, en ik houd ervan die kennis weer te delen met anderen.”