Unit testen is een methodologie waarbij eenheden van code worden getest in isolatie van de rest van de applicatie. Een unit test kan een bepaalde functie, object, klasse, of module testen. Unit tests zijn zeer geschikt om te leren of individuele onderdelen van een applicatie wel of niet werken. NASA kan maar beter weten of een hitteschild wel of niet werkt voordat ze de raket de ruimte in sturen.
Maar unit tests testen niet of units wel of niet samenwerken wanneer ze worden samengevoegd tot een hele applicatie. Daarvoor heb je integratietesten nodig, dat kunnen samenwerkingstesten tussen twee of meer units zijn, of volledige end-to-end functionele testen van de hele draaiende applicatie (aka systeemtesten). Uiteindelijk moet je de raket lanceren en kijken wat er gebeurt als alle onderdelen worden samengevoegd.
Er zijn meerdere denkrichtingen als het gaat om systeemtesten, waaronder Behavior Driven Development (BDD), en functioneel testen.
Behavior Driven Development (BDD) is een tak van Test Driven Development (TDD). BDD gebruikt menselijk leesbare beschrijvingen van gebruikerseisen voor software als basis voor softwaretests. Net als Domain Driven Design (DDD), is een vroege stap in BDD het definiëren van een gedeelde woordenschat tussen belanghebbenden, domeinexperts en ingenieurs. Dit proces omvat de definitie van entiteiten, gebeurtenissen, en outputs die de gebruikers belangrijk vinden, en het geven van namen waar iedereen het over eens kan zijn.
DBDD beoefenaars gebruiken dat vocabulaire vervolgens om een domeinspecifieke taal te creëren die zij kunnen gebruiken om systeemtesten te coderen, zoals User Acceptance Tests (UAT).
Elke test is gebaseerd op een user story, geschreven in de formeel gespecificeerde alomtegenwoordige taal, gebaseerd op het Engels. (Een alomtegenwoordige taal is een vocabulaire dat door alle belanghebbenden wordt gedeeld.)
Een test voor een overboeking in een cryptocurrency wallet zou er als volgt uit kunnen zien:
Story: Transfers change balancesAs a wallet user
In order to send money
I want wallet balances to updateGiven that I have $40 in my balance
And my friend has $10 is their balance
When I transfer $20 to my friend
Then I should have $20 in my balance
And my friend should have $30 in their balance.
Merk op dat deze taal zich uitsluitend richt op de zakelijke waarde die een klant uit de software zou moeten halen, in plaats van de gebruikersinterface van de software te beschrijven, of hoe de software de doelen zou moeten bereiken. Dit is het soort taal dat je zou kunnen gebruiken als input voor het UX design proces. Het ontwerpen van dit soort gebruikerseisen kan veel werk besparen later in het proces door het team en de klanten te helpen op één lijn te komen over welk product je aan het bouwen bent.
Vanuit dit stadium zijn er twee paden die je kunt bewandelen:
- Geef de test een concrete technische betekenis door de beschrijving om te zetten in een domeinspecifieke taal (DSL), zodat de door mensen leesbare beschrijving ook als machineleesbare code kan worden gebruikt, (ga door op het BDD-pad) of
- Verttaal de user stories naar geautomatiseerde tests in een algemene taal, zoals JavaScript, Rust, of Haskell.
Hoe dan ook, het is over het algemeen een goed idee om je tests als black box tests te behandelen, wat betekent dat de testcode zich niets moet aantrekken van de implementatiedetails van de functie die je aan het testen bent. Black box tests zijn minder broos dan white box tests omdat, in tegenstelling tot white box tests, black box tests niet worden gekoppeld aan de implementatie details, die waarschijnlijk zullen veranderen als requirements worden toegevoegd of aangepast, of code wordt gerefactored.
Proponenten van BDD gebruiken aangepaste tools zoals Cucumber om hun aangepaste DSL’s te maken en te onderhouden.
Voorstanders van functionele tests daarentegen testen over het algemeen functionaliteit door gebruikersinteracties met de interface te simuleren en de werkelijke output te vergelijken met de verwachte output. In websoftware betekent dat meestal het gebruik van een testframework dat een interface heeft met de webbrowser om typen, drukken op knoppen, scrollen, zoomen, slepen, enzovoort te simuleren, en vervolgens de output van de view te selecteren.
Ik vertaal gebruikerseisen meestal naar functionele tests in plaats van BDD-tests bij te houden, vooral vanwege de complexiteit van het integreren van BDD-frameworks met moderne applicaties, en de kosten van het onderhouden van aangepaste, Engels-achtige DSL waarvan de definities zich uiteindelijk over meerdere systemen kunnen uitstrekken, en zelfs meerdere implementatietalen.
Ik vind de voor leken leesbare DSL nuttig voor specificaties op zeer hoog niveau als communicatiemiddel tussen belanghebbenden, maar een typisch softwaresysteem vereist ordes van grootte meer tests op laag niveau om voldoende code- en case coverage te produceren om te voorkomen dat flagrante bugs de productie bereiken.
In de praktijk moet je “Ik maak 20 dollar over naar mijn vriend” vertalen in iets als:
- Open wallet
- Klik op overboeking
- Vul het bedrag in
- Vul het adres van de receiver wallet in
- Klik op
- Wacht op een bevestigingsdialoog
- Klik op “Bevestig transactie”
Een laag daaronder, onderhoud je de status voor de “geld overmaken” workflow, en je wilt unit tests die ervoor zorgen dat het juiste bedrag wordt overgemaakt naar het juiste portemonnee adres, en een laag daaronder, wil je de blockchain API’s te raken om ervoor te zorgen dat de portemonnee saldo’s daadwerkelijk werden aangepast op de juiste wijze (iets dat de client misschien niet eens een beeld voor).
Deze verschillende testbehoeften worden het best gediend door verschillende lagen van tests:
- Unit tests kunnen testen of de lokale client state correct wordt bijgewerkt en correct wordt gepresenteerd in de client view.
- Functionele tests kunnen UI interacties testen en ervoor zorgen dat aan de gebruikerseisen wordt voldaan in de UI laag. Dit zorgt er ook voor dat UI-elementen op de juiste manier worden bedraad.
- Integratietesten kunnen testen dat API-communicatie op de juiste manier gebeurt en dat de portefeuillebedragen van de gebruiker daadwerkelijk correct werden bijgewerkt op de blockchain.
Ik heb nog nooit een belanghebbende leek ontmoet die zich in de verste verte bewust is van alle functionele tests die zelfs het UI-gedrag op het hoogste niveau verifiëren, laat staan een die zich bekommert om alle gedragingen op lagere niveaus. Aangezien leken niet geïnteresseerd zijn, waarom dan de kosten betalen van het onderhouden van een DSL om voor hen te vertalen?
Of je het volledige BDD proces nu wel of niet in de praktijk brengt, er zitten een heleboel goede ideeën en praktijken in die we niet uit het oog moeten verliezen. Specifiek:
- De vorming van een gedeelde woordenschat die ingenieurs en belanghebbenden kunnen gebruiken om effectief te communiceren over de behoeften van de gebruiker en software-oplossingen.
- De creatie van user stories en scenario’s die helpen bij het formuleren van acceptatiecriteria en een definitie van gedaan voor een bepaalde functie van de software.
- De praktijk van samenwerking tussen gebruikers, het kwaliteitsteam, productteam, en ingenieurs om consensus te bereiken over wat het team aan het bouwen is.
Een andere benadering van systeemtesten is functioneel testen.
Wat is Functioneel Testen?
De term “functioneel testen” kan verwarrend zijn omdat het in de softwareliteratuur verschillende betekenissen heeft gehad.
IEEE 24765 geeft twee definities:
1. testen die het interne mechanisme van een systeem of component negeert en zich uitsluitend richt op de outputs die worden gegenereerd in reactie op geselecteerde inputs en uitvoeringscondities
2. testen die worden uitgevoerd om te evalueren of een systeem of component voldoet aan gespecificeerde functionele eisen
De eerste definitie is algemeen genoeg om op vrijwel alle populaire vormen van testen van toepassing te zijn, en heeft al een perfect passende naam die door softwaretesters goed wordt begrepen: “black box testing”. Als ik het over black box testing heb, zal ik in plaats daarvan die term gebruiken.
De tweede definitie wordt meestal gebruikt in tegenstelling tot testen die niet direct betrekking hebben op de kenmerken en functionaliteit van de app, maar zich in plaats daarvan concentreren op andere kenmerken van de app, zoals laadtijden, UI-reactietijden, serverbelasting testen, beveiligingspenetratietesten, enzovoort. Nogmaals, deze definitie is te vaag om op zichzelf erg nuttig te zijn. Meestal willen we specifieker zijn over wat voor soort tests we doen, bijvoorbeeld unit testing, smoke testing, user acceptance testing?
Om die redenen geef ik de voorkeur aan een andere definitie die de laatste tijd populair is. Developer Works van IBM zegt:
Functionele tests worden geschreven vanuit het perspectief van de gebruiker en richten zich op systeemgedrag waarin gebruikers geïnteresseerd zijn.
Dat is een stuk dichter bij de waarheid, maar als we tests gaan automatiseren, en die tests gaan testen vanuit het perspectief van de gebruiker, betekent dit dat we tests moeten schrijven die interactief zijn met de UI.
Dergelijke tests kunnen ook de namen “UI testing” of “E2E testing” hebben, maar die namen vervangen niet de noodzaak van de term “functionele tests”, omdat er een klasse van UI tests is die zaken als stijlen en kleuren test, die niet direct gerelateerd zijn aan gebruikerseisen zoals “Ik moet geld kunnen overmaken naar mijn vriend”.
Het gebruik van “functioneel testen” om te verwijzen naar het testen van de gebruikersinterface om er zeker van te zijn dat deze voldoet aan de gespecificeerde gebruikerseisen wordt meestal gebruikt in tegenstelling tot unit testen, dat wordt gedefinieerd als:
het testen van afzonderlijke eenheden code (zoals functies of modules) in isolatie van de rest van de applicatie
Met andere woorden, terwijl een unit test bedoeld is voor het testen van afzonderlijke eenheden code (functies, objecten, klassen, modules) geïsoleerd van de applicatie, terwijl een functionele test dient voor het testen van de eenheden in integratie met de rest van de app, vanuit het perspectief van de gebruiker die interactie heeft met de UI.
Ik hou van de classificatie van “unit tests” voor ontwikkelaar-perspectief code-eenheden, en “functionele tests” voor gebruiker-perspectief UI-tests.
Unit Tests vs Functionele Tests
Unit tests worden meestal geschreven door de uitvoerende programmeur, en testen vanuit het perspectief van de programmeur.
Functionele tests worden geïnformeerd door de gebruiker acceptatiecriteria en moeten de applicatie testen vanuit het perspectief van de gebruiker om ervoor te zorgen dat de eisen van de gebruiker worden voldaan. In veel teams worden functionele testen geschreven of uitgebreid door quality engineers, maar elke software engineer moet zich bewust zijn van hoe functionele testen worden geschreven voor het project, en welke functionele testen nodig zijn om de “definition of done” voor een bepaalde feature set te voltooien.
Unit testen worden geschreven om individuele units te testen, geïsoleerd van de rest van de code. Er zijn twee grote voordelen aan deze aanpak:
- Unit tests lopen erg snel omdat ze niet afhankelijk zijn van andere delen van het systeem, en als zodanig, meestal geen asynchrone I/O hebben om op te wachten. Het is veel sneller en goedkoper om een fout te vinden en op te lossen met unit tests dan te wachten tot een complete integratie suite is uitgevoerd. Unit tests zijn meestal in milliseconden klaar, in plaats van minuten of uren.
- Units moeten modulair zijn, zodat het makkelijk is om ze geïsoleerd van andere units te testen. Dit heeft als bijkomend voordeel dat het zeer goed is voor de architectuur van de applicatie. Modulaire code is gemakkelijker uit te breiden, te onderhouden, of te vervangen omdat de effecten van het veranderen ervan over het algemeen beperkt blijven tot de module unit die getest wordt. Modulaire applicaties zijn flexibeler en voor ontwikkelaars gemakkelijker om mee te werken in de loop der tijd.
Functionele tests daarentegen:
- Het duurt langer om ze uit te voeren, omdat ze het systeem end-to-end moeten testen, waarbij ze moeten integreren met alle verschillende onderdelen en subsystemen waar de applicatie op vertrouwt om de geteste gebruikersworkflow mogelijk te maken. Grote integratiesuites nemen soms uren in beslag. Ik heb verhalen gehoord van integratiesuites die dagen in beslag namen. Ik raad aan om je integratie pijplijn te hyper-optimaliseren om parallel te draaien, zodat hij in minder dan 10 minuten klaar is – maar dat is nog steeds te lang voor ontwikkelaars om op elke verandering te wachten.
- Zorg ervoor dat de units samenwerken als een geheel systeem. Zelfs als je een uitstekende unit test code dekking hebt, moet je nog steeds je units geïntegreerd testen met de rest van de applicatie. Het maakt niet uit of NASA’s hitteschilden werken als ze niet vast blijven zitten aan de raket bij de terugkeer. Functionele testen zijn een vorm van systeemtesten die ervoor zorgen dat het systeem als geheel zich gedraagt zoals verwacht wanneer het volledig is geïntegreerd.
Functionele testen zonder unit testen kunnen nooit diep genoeg code dekking bieden om er zeker van te zijn dat je een adequaat regressie vangnet hebt voor continue oplevering. Unit tests bieden diepte van de code dekking. Functionele tests bieden dekking voor de testcases van gebruikersvereisten.
Functionele tests helpen ons het juiste product te bouwen. (Validatie)
Unitests helpen ons het juiste product te bouwen. (Verificatie)
Je hebt beide nodig.
Note: Zie Validatie vs Verificatie. Build the right product vs build the product right onderscheid werd kernachtig beschreven door Barry Boehm.
Hoe schrijf je functionele tests voor webapplicaties
Er zijn veel frameworks waarmee je functionele tests voor webapplicaties kunt maken. Velen van hen gebruiken een interface genaamd Selenium. Selenium is een cross-platform, cross-browser automatiseringsoplossing gemaakt in 2004, waarmee u interacties met de webbrowser kunt automatiseren. Het probleem met Selenium is dat het een engine is die buiten de browsers staat en op Java is gebaseerd, en het kan moeilijker zijn dan nodig om het samen te laten werken met uw browsers.
Meer recentelijk is er een nieuwe familie van producten opgedoken die veel soepeler integreert met browsers met minder onderdelen om zorgen over te installeren en te configureren. Een van die oplossingen heet TestCafe. Het is degene die ik momenteel gebruik en aanbeveel.
Laten we eens een functionele test schrijven voor de TDD Day website. Eerst maak je er een project voor aan. In een terminal:
mkdir tddday
cd tddday
npm init -y # initialize a package.json
npm install --save-dev testcafe
Nu moeten we een "testui"
script toevoegen aan onze package.json
in het scripts
blok:
{
"scripts": {
"testui": "testcafe chrome src/functional-tests/"
}
// other stuff...
}
U kunt de tests uitvoeren door npm run testui
te typen, maar er zijn nog geen tests om uit te voeren.
Maak een nieuw bestand aan op src/functional-tests/index-test.js
:
import { Selector } from 'testcafe';
TestCafe maakt automatisch de fixture
en test
functies beschikbaar. U kunt fixture
met de tagged template literal syntax gebruiken om titels voor groepen tests te maken:
fixture `TDD Day Homepage`
.page('https://tddday.com');
Nu kunt u uit de pagina selecteren en beweringen maken met behulp van de test
en Select
functies. Als je het allemaal samenvoegt, ziet het er als volgt uit:
TestCafe start de Chrome-browser, laadt de pagina, wacht tot de pagina is geladen en wacht tot je selector met een selectie overeenkomt. Als de selector nergens mee overeenkomt, zal de test uiteindelijk mislukken.
Als de selector wel ergens mee overeenkomt, wordt de daadwerkelijk geselecteerde waarde vergeleken met de verwachte waarde en mislukt de test als deze niet overeenkomt.
TestCafe biedt methoden om allerlei soorten UI-interacties te testen, waaronder klikken, slepen, tekst typen, enzovoort.
TestCafe levert ook een rijke selector API om DOM-selecties pijnloos te maken.
Laten we de registratieknop eens testen om er zeker van te zijn dat deze naar de juiste pagina navigeert als je erop klikt. Eerst hebben we een manier nodig om de huidige pagina locatie te controleren. Onze TestCafe code draait in Node, maar we hebben het nodig om in de client te draaien. TestCafe levert een manier voor ons om code in de client te draaien. Eerst moeten we ClientFunction
toevoegen aan onze import regel:
import { Selector, ClientFunction } from 'testcafe';
Nu kunnen we het gebruiken om de venster locatie te testen:
Als je niet zeker weet hoe je moet doen wat je probeert te doen, TestCafe Studio laat je tests opnemen en opnieuw afspelen. TestCafe Studio is een visuele IDE voor het interactief opnemen en bewerken van functionele tests. Het is zo ontworpen dat een test engineer die misschien geen JavaScript kent een suite van functionele tests kan bouwen. De tests die het genereert wachten automatisch op asynchrone taken zoals het laden van pagina’s. Net als de TestCafe engine, kan TestCafe Studio tests produceren die gelijktijdig kunnen worden uitgevoerd over vele browsers, en zelfs externe apparaten.
TestCafe Studio is een commercieel product met een gratis proefversie. U hoeft TestCafe Studio niet aan te schaffen om de open source TestCafe engine te gebruiken, maar de visuele editor met ingebouwde opnamefuncties is zeker de moeite waard om te onderzoeken of het geschikt is voor uw team.
TestCafe heeft een nieuwe lat gelegd voor cross-browser functioneel testen. Na vele jaren van pogingen om cross-platform tests te automatiseren, ben ik blij te kunnen zeggen dat er eindelijk een redelijk pijnloze manier is om functionele tests te maken, en er is nu geen goed excuus meer om je functionele tests te verwaarlozen, zelfs als je geen toegewijde kwaliteitsingenieurs hebt om je te helpen je functionele testsuite te bouwen.
Do’s en Don’ts van functionele tests
- Verander niets aan het DOM. Als je dat wel doet, kan je test runner (bijv. TestCafe) niet begrijpen hoe het DOM is veranderd, en die DOM verandering kan invloed hebben op de andere asserties die afhankelijk zijn van de DOM output.
- Deel geen mutable state tussen tests. Omdat ze zo langzaam zijn, is het ongelooflijk belangrijk dat functionele tests parallel kunnen worden uitgevoerd, en ze kunnen dat niet deterministisch doen als ze strijden om dezelfde gedeelde mutable state, wat nondeterminisme kan veroorzaken door race conditions. Omdat je systeemtests uitvoert, moet je in gedachten houden dat als je gebruikersdata wijzigt, je verschillende test gebruikersdata in de database moet hebben voor verschillende tests, zodat ze niet willekeurig falen door race condities.
- Meng functionele tests niet met unit tests. Unit tests en functionele tests moeten vanuit verschillende perspectieven worden geschreven, en op verschillende momenten worden uitgevoerd. Unit tests moeten worden geschreven vanuit het perspectief van de ontwikkelaar en worden uitgevoerd elke keer als de ontwikkelaar een wijziging aanbrengt, en moeten worden voltooid in minder dan 3 seconden. Functionele testen worden geschreven vanuit het perspectief van de gebruiker, en hebben te maken met asynchrone I/O, waardoor het uitvoeren van de test te langzaam kan zijn voor de ontwikkelaar om direct feedback te krijgen op elke verandering in de code. Het moet makkelijk zijn om de unit tests te draaien zonder functionele test runs te triggeren.
- Draai tests in headless mode, als je kunt, wat betekent dat de browser UI niet echt hoeft te worden gestart, en tests sneller kunnen draaien. Headless mode is een geweldige manier om de meeste functionele tests te versnellen, maar er is een kleine subset van tests die niet in headless mode kunnen worden uitgevoerd, simpelweg omdat de functionaliteit waar ze op vertrouwen niet werkt in headless mode. Sommige CI/CD pijplijnen vereisen dat je functionele testen in headless mode uitvoert, dus als je sommige testen hebt die niet in headless mode kunnen draaien, kan het zijn dat je ze moet uitsluiten van de CI/CD run. Zorg ervoor dat het kwaliteitsteam op de uitkijk staat voor dat scenario.
- Voer tests uit op meerdere apparaten. Slagen je tests nog steeds op mobiele apparaten? TestCafe kan draaien op externe browsers zonder TestCafe te installeren op de externe apparaten. De screenshot-functionaliteit werkt echter niet op externe browsers.
- Maak screenshots bij testfouten. Het kan nuttig zijn om een screenshot te maken als uw tests mislukken om te helpen diagnosticeren wat er mis ging. TestCafe studio heeft daar een run configuratie optie voor.
- Houd je functionele test runs onder de 10 minuten. Langer dan dat zorgt voor te veel vertraging tussen de ontwikkelaar die aan een functie werkt en het oplossen van iets dat fout ging. 10 minuten is lang genoeg voor een ontwikkelaar om bezig te zijn met de volgende functie, en als de test mislukt na meer dan 10 minuten, zal het waarschijnlijk de ontwikkelaar onderbreken die is overgegaan op de volgende taak. Een onderbroken taak duurt gemiddeld twee keer zo lang om te voltooien en bevat ruwweg twee keer zo veel fouten. TestCafe staat u toe om veel tests gelijktijdig uit te voeren, en de remote browser optie kan dit doen over een vloot van test servers. Ik raad je aan om van deze mogelijkheden gebruik te maken om je testruns zo kort mogelijk te houden.
- Zet de continue leveringspijplijn wel stil als tests falen. Een van de grote voordelen van geautomatiseerde tests is de mogelijkheid om je klanten te beschermen tegen regressies – bugs in functies die vroeger werkten. Dit vangnetproces kan worden geautomatiseerd, zodat je er goed op kunt vertrouwen dat je release relatief vrij is van bugs. Tests in de CI/CD-pijplijn elimineren effectief de angst van een ontwikkelteam voor verandering, wat een serieuze aderlating kan zijn voor de productiviteit van ontwikkelaars.
Volgende stappen
Neem deel aan TDD Day.com – een TDD-curriculum van een hele dag met 5 uur opgenomen video-inhoud, projecten om unit testen en functioneel testen te leren, hoe je React-componenten test, en een interactieve quiz om ervoor te zorgen dat je de stof onder de knie hebt.