Van chaos naar structuur: zo bouw je onderhoudbare Playwright-tests in Python

Introductie

Playwright is een van de populairdere testframeworks van dit moment. Het stelt je in staat om betrouwbare end-to-end tests te schrijven die werken in alle moderne browsers en platforms — snel en stabiel. Bovendien ondersteunt Playwright meerdere programmeertalen, zoals JavaScript, TypeScript, Python, .NET en Java.

De meeste teams kiezen voor JavaScript of TypeScript, omdat Playwright oorspronkelijk in die talen is ontwikkeld. Zelf ben ik echter een andere kant op gegaan: ik gebruik Playwright met Python. Python is namelijk ideaal voor API-testen en datavalidatie, en dankzij de flexibiliteit van de taal kun je snel je eerste tests opzetten en uitvoeren.

Toch heeft die flexibiliteit ook een keerzijde: Python is minder streng dan TypeScript of Java, waardoor je testcode snel ongestructureerd kan worden. Daarom heb ik een duidelijke gelaagde architectuur opgezet die zorgt voor overzicht, herbruikbaarheid en schaalbaarheid. In dit artikel laat ik zien hoe ik dat heb aangepakt — en waarom deze aanpak zo goed werkt in de praktijk.

Het framework is globaal opgebouwd uit meerdere lagen: een API-laag, een UI-laag, een Auth- en handler-laag die zowel de API- als de UI-laag gebruiken, en een reporting-laag. Elke laag heeft een duidelijke verantwoordelijkheid, waardoor de structuur overzichtelijk en onderhoudbaar blijft.

Van chaos naar structuur: zo bouw je onderhoudbare Playwright-tests in Python

API Structuur

Binnen de API-laag zijn er drie belangrijke componenten:

  • BaseClient: generieke API-logica
  • ApiClient: domeinspecifieke functionaliteit
  • ApiResponseHandler: validatie van responses via statuscode en schema

De BaseClient vormt de basis van alle API-interacties in het testframework. Het is een generieke, herbruikbaar component die ervoor zorgt dat individuele API-clients zich kunnen concentreren op specifieke endpoints, zonder steeds dezelfde basistaken opnieuw te implementeren.

Wat zit er allemaal in de BaseClient?

  • CRUD-operaties via _get, _post, _put, _patch, _delete en _fetch, uitgevoerd via Playwright’s APIRequestContext.
  • Header- en tokenbeheer: automatisch instellen van authenticatietokens en andere headers.
  • Logging: overzicht van requests en responses inclusief status en body, handig voor debugging.
  • Configuratiebeheer: centrale basis-URL en instellingen.

Kortom: de BaseClient bevat alle logica die nodig is om een request te doen, maar zonder specifieke domeinlogica.

 

De ApiClient breidt de BaseClient uit en bevat functionaliteiten die specifiek zijn voor de businesslogica van de applicatie. Waar de BaseClient algemene, herbruikbare taken afhandelt, richt de ApiClient zich op concrete acties die passen bij de domeinlogica van de applicatie.

Bijvoorbeeld: stel je hebt een applicatie voor een autoverhuurbedrijf. In de ApiClient kun je dan methoden toevoegen zoals create_car of get_available_cars — dit zijn business-specifieke acties die direct aansluiten op de applicatie.

Nu heb ik hier één ApiClient, maar deze kan je natuurlijk verdelen in meerdere ApiClients per resource als de applicatie en het testframework groeien. Bijvoorbeeld in dit geval een ApiClient voor autos, en een ApiClient voor klanten etc.

Voor het request heb ik een apart model gemaakt in Pydantic, zodat we de structuur en validatie van de invoer kunnen afdwingen, hergebruiken en duidelijk kunnen vastleggen welke data nodig is om een auto aan te maken.

In de test kun je dan eenvoudig de ApiClient gebruiken om een request te doen.

Bij de laatste regel valideren we de response op basis van de status code, en ook bij het response valideren we aan de hand van Pydantic het schema. Hiervoor hebben we een aparte component: de ApiResponseHandler.

Uiteindelijk zorgt dit voor een duidelijke scheiding van de verantwoordelijkheden:

Component

Verantwoordelijkheid

BaseClient

Generieke API-logica (CRUD, headers, logging)

ApiClient

Domeinspecifieke functionaliteit

ApiResponseHandler

Validatie van responses via statuscode en schema

UI Structuur

De UI-laag van het testframework gaat over alles wat een gebruiker op het scherm doet en ziet. In gewone woorden: we simuleren echte gebruikers die klikken, typen en navigeren in de applicatie.

Om dit overzichtelijk en onderhoudbaar te houden, is de UI-laag opgebouwd volgens een duidelijke structuur. Zo weten testers en ontwikkelaars precies waar acties en controles zitten en hoe ze nieuwe tests kunnen toevoegen om zo een eenduidige structuur te behouden.

De UI-laag bestaat uit drie belangrijke componenten:

  • Page Objects: Definiëren de locators voor UI-elementen, zoals velden, knoppen, etc.
  • Steppers: Acties die een gebruiker kan uitvoeren, bijvoorbeeld inloggen of een formulier invullen.
  • Asserters: Controleren of de uitkomsten van acties overeenkomen met de verwachtingen.

Door deze drie lagen te combineren, ontstaat een duidelijke scheiding tussen wat een gebruiker doet en wat we verwachten dat er gebeurt. In de volgende secties duiken we dieper in elk van deze componenten en laten ik zien hoe ze samenwerken om robuuste UI-tests te schrijven.

Als eerst hebben we een Page Object die de locators voor de UI-elementen bevat, dit is de basis voor alle andere lagen. Deze maken we aan zodat we alle locators in één plek hebben en deze dus niet dubbel hoeven te registeren.

Dit Page Object gebruiken we vervolgens in een stepper. Alle acties worden gedefinieerd een _ prefix, vervolgens komen deze acties samen in een stap. Omdat Playwright automatisch het Page Object meegeeft, hebben we een native Page Object en een Page Object waarin alle locators zitten.

Bij een test wil je controleren of een actie correct is uitgevoerd. Hiervoor maken we een apart component: de asserter. Het is belangrijk dat we deze apart houden van de stepper, zodat we een duidelijke scheiding hebben tussen wat een gebruiker doet en wat we verwachten dat er gebeurt.

In de test zelf wordt dit duidelijk: eerst voer je de stappen uit, daarna controleer je het resultaat.

Samengevat ziet de UI-laag er als volgt uit:

Component

Verantwoordelijkheid

Page Object

Locators voor UI-elementen

Stepper

Acties die gebruikers uitvoeren

Asserter

Validaties die we uitvoeren op de stappen

Auth module

Dit is een grove basis van de gelaagdheid van het framework. Verder zijn er in veel applicaties verschillende rollen waarmee je bepaalde acties wel of juist niet mag uitvoeren. Hiervoor heb ik een auth module die alle gebruikers bevat met hun respectievelijke rollen. Deze gebruikers kun je vervolgens gebruiken in je testen om ze op basis van hun rol aan te roepen.

Hier definiëren we de rollen die we in de applicatie hebben in een enum.

Deze oplossing zorgt ervoor dat je gestructureerd flows kunt testen op basis van de verschillende beschikbare rollen in de applicatie. Deze functie kun je vervolgens zowel in je ui gebruiken als in je api testen.

En in je test:

Handlers

De ApiClient is verantwoordelijk voor het ophalen of versturen van data via de API. Hij weet hoe en waar requests gedaan moeten worden, maar bevat geen logica over wat er met die data gebeurt. Dat is de taak van de Handlers.

Handlers bouwen voort op de ApiClient en bevatten de businesslogica. Ze gebruiken de ApiClient om data op te halen en passen daarop regels of filters toe, interpreteren of combineren de data zodat deze bruikbaar is voor de tests of applicatielogica.

Een duidelijk voorbeeld is het ophalen van beschikbare auto’s. De ApiClient haalt simpelweg alle auto’s op, zonder te weten welke beschikbaar zijn. De Handler daarentegen filtert de auto’s en geeft alleen de beschikbare auto’s terug. Deze auto’s kun je dan gebruiken in je testen.

Handlers kunnen op zowel API als UI niveau worden gebruikt

Reporting

Er is heel eenvoudig een integratie met allure. Met pip install allure-pytest kun je allure installeren. Vervolgens kun je bij het starten van je testen de parameter --alluredir=allure-results meegeven. Dit zorgt ervoor dat allure de resultaten van de testen opslaat in de map allure-results. Vervolgens kun je met allure serve allure-results lokaal een report starten.

In je test kun je dan allure tags gebruiken om je testen te structureren en leesbaar te maken.

Hieronder een screenshot van een allure report en een link naar de documentatie. https://allurereport.org/

Test afhankelijkheden

Tests kunnen soms erg lang duren, vooral UI-tests. Ze zijn vaak minder robuust: kleine wijzigingen in de interface kunnen al leiden tot een mislukte test, zelfs als de kernfunctionaliteit gewoon werkt.

Daarom kan het handig zijn om sommige testen afhankelijk te maken van elkaar. Een API-test is vaak sneller en robuuster dan een UI-test en als een API-test faalt, weet je meteen dat er een probleem is met de status code of het response model, en hoef je de bijbehorende UI-test niet eens uit te voeren. Dit bespaart veel tijd en maakt het testproces efficiënter.

Om dit te organiseren, bouwen we afhankelijkheden tussen tests in. Met Pytest kun je bijvoorbeeld aangeven dat een UI-test afhankelijk is van een API-test. Als de API-test faalt, zal de UI-test automatisch worden overgeslagen.

#replace title#