Angular & Cypress end-to-end testing
Auteur: Danny Holstein
Enige tijd geleden heb ik mij verdiept in Cypress dat een Testing framework is voor JavaScript. Dit betreft een framework voor end-to-end (e2e) testing. De term end-to-end wil zeggen dat er methoden worden gebruikt om de complete workflow van een applicatie te testen vanuit het perspectief van de eindgebruiker. In dit nieuwe blogbericht wordt Cypress voor Angular applicaties nader behandeld.
Theorie: unit- en integration-tests versus end-to-end testing
Met unit tests worden de kleinste onafhankelijke delen van een applicatie getest. Het woord unit verwijst naar functies en methoden. Unit tests verzekeren dat delen van een applicatie correct werken in isolatie. Dit is van belang voor bijvoorbeeld componenten met veel business logic. Een integration test daarentegen gaat na in hoeverre verschillende delen van een applicatie met elkaar kunnen samenwerken. Het doel van deze tests is om bugs te verhelpen voordat de eindgebruiker deze opmerkt. Het testen van componenten gaat de diepte in en er worden waardes getest in de componenten, alsook waardes in de formulieren, bepaalde events en alle andere business logic die er is.
Echter testen end-to-end tests de werking van de applicatie vanuit het perspectief van de eindgebruiker. Dit zijn volledig geautomatiseerde tests. De centrale vraag hierbij is in hoeverre de bouwblokken werken zoals de gebruiker verwacht. Vaak zijn de belangrijkste delen die getest worden de login/authenticatie pagina en betaalpagina. In tegenstelling tot unit- en integration-tests worden geen waardes in componenten getest, maar end-to-end tests testen het gedrag van de eindgebruiker. En end-to-end tests die ‘spiegelen’ niet de code, dat wil zeggen dat niet alles getest wordt.
Er zijn een aantal zaken waarvoor end-to-end tests niet dienen, namelijk: randzaken die buiten de normale werking van de applicatie vallen (bijvoorbeeld bugs voor specifieke browsers en of apparaten), er worden geen componenten getest en er wordt geen CSS-opmaak getest. Waar end-to-end tests wel voor dienen is om te valideren of de applicatie correct werkt en niet crasht. Een andere naam voor een end-to-end test is ‘smoke test’ en dit is een term die afkomstig is uit de wereld die zich bezighoudt met het testen van elektronische hardware. Een ‘smoke test’ houdt in dat wanneer er stroom wordt gezet op een bepaald apparaat en er vervolgens rook vanaf komt, dat er niet meer getest hoeft te worden.
In tegenstelling tot unit- of integration-tests zijn end-to-end tests trager en kunnen deze mislukken. Maar end-to-end tests taxeren wel de gezondheid van de gehele applicatie. Het is vaak zo wanneer alle delen van een applicatie samen komen, dat er een nieuw type bug verschijnt wat op zijn beurt te maken heeft met de timing of de volgorde van bepaalde events. Waar unit- of integration tests in isolatie worden uitgevoerd, daar hebben end-to-end tests een omgeving nodig die lijkt op de productie-omgeving, uiteraard wel zodat de productie-data niet wordt gecompromitteerd. Met Cypress testing is het mogelijk om zowel de een ‘real-API’ als ‘fake-API’ te gebruiken waarbij een bepaald request beantwoord zal worden met een vaste response.
Voordelen van end-to-end testing
Wanneer er de behoefte bestaat om een applicatie te testen, dan is het aanbevolen om eerst met end-to-end te beginnen. Een reden hiervan is dat end-to-end testing goedkoop is. End-to-end testing kan ook nuttig zijn wanneer er meerdere pagina's met formulieren ingevuld moeten worden. Het percentage bugs dat verholpen wordt kan snel oplopen (in bepaalde situaties zelfs 80%). Met end-to-end testing moet het zeker zijn dat de applicatie ten alle tijden werkt voor de eindgebruiker.
Een voordeel van end-to-end testing is dat er in een vroeg stadium bugs uit een applicatie worden gehaald en er verder gegaan kan worden met de ontwikkeling van een applicatie. Met end-to-end testing is er na te bootsen hoe een gebruiker interacties heeft met de applicatie. In end-to-end testing wordt gecontroleerd op de juiste content, veranderingen in de URL, bepaalde features en worden de UI’s (User Interfaces) onderzocht.
Een ander voordeel is dat Cypress goed wordt onderhouden en eveneens goed is gedocumenteerd. Cypress excelleert in developer experience en kosteneffectiviteit, waarbij bepaalde features van een applicatie uit te testen zijn onder realistische omstandigheden. Een 100% code coverage – dit wil zeggen dat alle code wordt getest - is onnodig. Zaken die wel getest moeten worden zijn features die uniek zijn voor de applicatie. Het testen van HTTP-requests levert niet veel voordelen op, behalve wanneer er retry-requests worden uitgetest. Over het algemeen zijn de API-calls/HTTP-requests niet de plek waar problemen zullen optreden. Hetzelfde geldt voor veel populaire libraries, want deze zijn vaak al uitvoerig getest.
Cypress installeren in een bestaande Angular applicatie
Tot en met Angular 12 werd er gebruik gemaakt van Protractor (dat nu deprecated is) als het standaard end-to-end testing framework. Er is dus na Angular 12 geen standaard end-to-end testing framework geconfigureerd. Protractor was gebaseerd op WebDriver. Het WebDriver protocol maakt het mogelijk om op afstand een browser te controleren met een set commando’s. Dit heeft als voordeel dat tests in verschillende browsers uitgevoerd kunnen worden.
Cypress maakt geen gebruik van Webdriver en heeft een unieke architectuur. Cypress is erop gericht om de developer experience te verbeteren, alsook de prestaties en betrouwbaarheid van end-to-end tests. Wanneer Cypress wordt gestart dan zal een Node.js applicatie de browser starten. De tests worden direct uitgevoerd binnen de browser door middel van een ondersteunde browser plugin. De test runner biedt een krachtige User Interface voor het inspecteren en debuggen van de tests in de browser. Een nadeel is dat niet alle browsers zijn ondersteund, maar wel: Firefox, Chrome en Edge. Cypress heeft experimentele ondersteuning voor Webkit – dat is de engine die gebruikt wordt door Safari.
Voor Angular is er een schematic om Cypress te installeren. Een ander voordeel is dat Cypress officieel wordt ondersteund door Angular. Cypress is in een Angular applicatie te installeren en in te stellen met het commando: ng add @cypress/schematic
Na de installatie is er niet alleen een Cypress directory aangemaakt, maar ook zijn er een aantal nieuwe mappen en bestanden aangemaakt. Zoals: een tsconfig.json bestand met de configuratie voor alle TypeScript bestanden in de directory. En directories zoals: e2e (voor alle end-to-end tests), support (voor aangepaste commando’s en andere helpers), fixtures (testbestanden zoals: json-bestanden, afbeeldingen, etc. die nodig zijn voor de tests). Iedere test in Cypress is een TypeScript bestand dat te herkennen is aan de extensie: cy.ts
De praktijk: Cypress end-to-end testing in een Angular applicatie
Cypress test code wordt onafhankelijk uitgevoerd in de browser, maakt gebruik van de talen Mocha en Chai en dient voor end-to-end testing. Dit in tegenstelling tot Angular test code, dat alleen binnen de Angular environment wordt uitgevoerd, gebruikt maakt van het TestBed en Jasmine, en alleen dient voor unit- en integration-tests.
De tests in Cypress zijn gestructureerd met het test-framework Mocha. Maar de assertions (ook wel expectations genoemd) zijn geschreven in Chai. De combinatie van Mocha en Chai is populair en doet in grote lijnen hetzelfde als Jasmine maar is meer flexibel en rijk in features. De structuur van Mocha is vergelijkbaar met die van Jasmine. Ieder bestand bevat gewoonlijk 1 describe-block met eventuele andere geneste it-blocks. Andere overeenkomst zijn de functies: beforeEach, afterEach, beforeAll en afterAll.
Veel gebruikte Cypress commando’s zijn:
| cy.get() | Een HTML-element vinden op basis van een CSS-selector. |
| cy.title() | Deze functie geeft de titel van de pagina terug. |
| cy.visit() | Cypress naar een bepaalde pagina laten navigeren. |
| cy.intercept() | Een HTTP-request onderscheppen en dummy-data teruggeven. |
| cy.fixture() | Dummy data laden, zowel vanuit een JSON-bestand als andere bestanden. |
| cy.log() | Code wegschrijven naar de terminal voor nadere inspectie. |
De bovenstaande commando’s worden toegevoegd aan de queue (wachtrij). De queue wordt asynchroon en commando-voor-commando verwerkt. Een feature van Cypress is dat het bepaalde commando’s opnieuw worden uitgevoerd wanneer deze een eerste keer niet slagen. De standaardtijd hiervan is 4 seconden en is zowel globaal in te stellen als op het niveau van de tests.
Om code overzichtelijker te houden met Cypress, zodat er bijvoorbeeld niet te veel herhalende code in het beforeEach-block komt te staan, kan er code verplaatst worden naar zowel de Commands als Page-objects. Page objects richten zich meer op UI-interactions, herhalende cy.visit() functies en of wachten totdat data geladen is. Een voorbeeld van herhalende code is om HTML elementen te verkrijgen door middel van cy.get(). Het is bovendien aanbevolen om elementen te markeren met test-ids. Om veel herhalende code te voorkomen is dit in de Commands te plaatsen – zie de afbeelding hieronder:
In de praktijk bleek er een gemene adder onder het gras te zijn, want wanneer er gebruik wordt gemaakt van Commands dan moet de eerste import regel worden ingeschakeld.
Een tweede issue was wanneer er gebruik wordt gemaakt van Commands dat Cypress afweet van het bestaan van een Custom Command (zoals byTestId hierboven) door middel van een namespace en interface.
De opmerkzame lezer zal opmerken dat er een (asynchrone) Chainable wordt teruggeven met een jQuery -object. Waar de Cypress commands (ook de aangepaste commands) asynchroon zijn (in tests kan er dan gebruik gemaakt worden van de should functie), daar zijn de jQuery-objecten synchroon. De jQuery -objecten zijn in de tests toegankelijk door gebruik te maken van de forEach functie. De waardes van HTML-elementen en zijn attributen zijn dus uit te lezen door middel van jQuery functies (en niet door de should functie want deze is asynchroon). Cypress heeft voor jQuery gekozen, omdat er veel developers hiermee bekend zijn (hoewel dat voor mij al meer dan 12 jaar geleden is).
In veel gevallen geven Cypress commands een asynchrone wrapper – dit is een Chainer – terug rond een willekeurige waarde of DOM-element. De Chainer heeft een should functie om een assertion/expectation te maken en deze functie wordt gewoonlijk in Cypress gebruikt. Dit in tegenstelling tot Jasmine-expectations, die gebruik maken van expect().toBe(), etc.
Een ander voorbeeld om herhalende code te voorkomen in Cypress is om de combinaties van commando’s cy.fixture() en cy.intercept() in de Commands te plaatsen. Met de functie cy.fixture() wordt er dummy-data geladen vanuit een JSON-bestand. In de then functie wordt er iets gedaan met deze data, namelijk opnieuw gebruikt in een cy.intercept() dat een HTTP-request onderschept en simuleert. Een asterisk (*) wordt gebruikt als wildcard dat overeenkomt met iedere andere query parameter. Tenslotte wordt er gebruik gemaakt van as(‘allPaintings’) en dit dient om dit HTTP-request een alias te geven dat op zijn beurt in de tests is te gebruiken.
In het algemeen is Cypress onder de knie te krijgen wanneer er in de praktijk mee wordt gewerkt. Een probleem dat zich voordeed was onder andere het testen van objecten. De oplossing was om in de expectations gebruik te maken van de syntax: that.deep.equal. Iets anders waar ik tegenaan ben gelopen dat was het uittesten van bestanden uploaden. Hiervoor was de oplossing om gebruik te maken van de cypress-file-upload plugin. Deze plugin heeft kant-en-klare functies om het openen van een bestand te simuleren, zoals de functie attachFile. Een test-bestand kan geplaats worden in de directory /cypress/fixtures.
Testresultaten
Wanneer Cypress correct is geïnstalleerd dan is de Cypress test runner te starten met het commando ng e2e in de terminal. Er kan een keuze gemaakt worden tussen de verschillende browsers waaronder Electron – dat is Cypress’ zijn eigen User Interface en is gebaseerd op Chromium (dat is de open source stichting van de Chrome browser). Ik heb zelf gekozen voor Chrome.
De User Interface met de testresultaten wijst zich zelf uit. Om zaken uit te testen met Cypress zijn er meer end-to-end tests geschreven dan noodzakelijk is. In de afbeelding hieronder zijn alle tests geslaagd en hebben deze een groene kleur. Wanneer een test mislukt dan is deze rood. In dit voorbeeld is er gebruik gemaakt van een ‘fake-API’.
Tot slot
Ik ben positief over het testen van Angular applicaties door middel van Cypress testing voor end-to-end tests. Zoals met alle nieuwe vaardigheden, moet men eerst de weg zien te vinden, maar veel zaken wijzen zich uiteindelijk vanzelf uit als er in de praktijk mee wordt gewerkt. Geautomatiseerde tests zijn tevens een vorm van verdere professionalisering en deze zijn goedkoop. Dit in tegenstelling tot handmatige (manual) tests die arbeidsintensief en duur zijn. Tijdens het uitproberen van Cypress testing voor een van mijn eigen Angular projecten zijn er een aantal bugs verholpen die anders over het hoofd zouden zijn gezien.