AI-ondersteund testen: een unit test-poort om de kwaliteit van AI-generatiecode te waarborgen

Deel de volledige workflow van AI-ondersteund testen, van ontwerpdocument tot testcode, gebruik een unit test-poort om de risico’s van AI-generatiecode af te dekken, en verschuif de reviewfocus van code naar testdocumentatie, laat mislukte testcases AI’s luiheid en fouten in de gaten houden, zodat ontwikkelaars vertrouwen hebben in vibe coding.

AI-ondersteund testen lost niet de testdekking op, maar mijn vertrouwen in AI-generatiecode. AI spuwt in drie minuten een groot stuk code uit, terwijl een mens er twee uur over doet om het te reviewen. Drie minuten genereren, twee uur reviewen is geen overdrijving. Mijn oplossing is om het reviewobject te veranderen: niet regel voor regel de code lezen, maar de testdocumentatie lezen, en lokaal in de ontwikkelomgeving een poort bouwen met unit tests, zodat mislukte testcases voor me toezien op AI’s luiheid en fouten.

Kleine bugs zijn het hoofdveld van AI-ondersteund testen

Bugs zijn in twee categorieën verdeeld: goed oplosbare bugs en slecht oplosbare bugs.

Slecht oplosbare bugs stammen meestal van architectuur- of ontwerpgebreken, evolueren geleidelijk tot historische lasten en hebben een kettingreactie; of ze worden veroorzaakt door derden, zoals third-party libraries, besturingssystemen, compilers, en vereisen meer gespecialiseerde kennis en diepgaand onderzoek om op te lossen. Deze complexe problemen vallen buiten het bestek van dit artikel.

Dit artikel richt zich op kleine bugs in de productlevenscyclus, met behulp van AI-ondersteuning om testcases te creëren, en lost de volgende problemen op:

  • Bugs die snel kunnen worden ontdekt
  • Lage bugs die slechts kleine wijzigingen vereisen om te herstellen
  • Nieuwe bugs die worden geïntroduceerd door wijzigingen
  • Fouten door haast bij spoedwijzigingen
  • De afweging tussen kosten en efficiëntie van gerichte tests

De meeste bugs zijn kleine bugs, maar kleine bugs kunnen ook ernstige problemen veroorzaken. Kleine bugs lokaal oplossen, bespaart veel latere inspanning.

Verschuif het reviewobject van code naar testdocumentatie

Ik heb ooit een gedurfd idee geprobeerd: AI volledige vrijheid geven om een vereiste te voltooien, zonder de gegenereerde code te reviewen, vergelijkbaar met de Copilot Agent-modus waarbij je het volledig loslaat. Later gaf ik het op. De hoofdoorzaak is dat ik geen vertrouwen had. Voor een logisch strikt, binair programma met 0 en 1, betekent geen vertrouwen hebben het verliezen van vertrouwen in up- en downstream, en het verliezen van klantvertrouwen. Dus ik leer nadenken over vibe coding: voor AI-generatiecode moet je zekerheid hebben.

Het probleem is de reviewkosten. AI genereert of wijzigt soms te veel code, drie minuten genereren, twee uur reviewen is geen overdrijving. De werklast van testcode is vaak groter dan die van businesscode, en wordt niet altijd meegeteld in de leveringsbeoordeling van ontwikkeling, dit is een van de redenen waarom veel mensen geen testen willen schrijven. Maar AI-assistenten zijn veel slimmer dan ik is een feit, in plaats van te weerstaan, geef het een poort.

Het verschuiven van het reviewobject van code naar testdocumentatie is de sleutel om deze impasse te doorbreken. In tegenstelling tot businesscode, beschrijven AI-generatie testcases meestal een zeer specifiek scenario, wat gemakkelijk te reviewen en te valideren is. Veel testcases hoeven niet eens regel voor regel gelezen te worden, maar alleen nader bekeken te worden wanneer ze falen. Het zeggen dat je de AI-code helemaal niet reviewt is overdreven, maar met volledige testdocumentatie kan worden bewezen dat niet elke regel code handmatig gereviewd hoeft te worden.

---
title: Van code-review naar document-review
---
flowchart TB
    A[AI genereert grote hoeveelheid code] e1@--> B[Regel voor regel review is duur]
    B e2@--> C{Wat reviewen}
    C e3@-->|Code| D[Twee uur reviewen, geen vertrouwen]
    C e4@-->|Testdocumentatie| E[Gemakkelijk te reviewen, snelle validatie]
    E e5@--> F[Unit test-poort]
    F e6@--> G[Mislukte testcases blootleggen problemen]

    classDef risk fill:#FFF3E0,stroke:#E65100,color:#E65100;
    classDef ok fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
    classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
    class D risk;
    class E,F,G ok;
    class e1,e2,e3,e4,e5,e6 animate;

Methodologische basis: Six Sigma en DMAIC

Six Sigma is een datagedreven kwaliteitsverbeteringsmethodologie, met als kerndoel om door procesvariatie te verminderen, het defectpercentage te beheersen op 3,4 per miljoen, oftewel het 6σ-niveau1.

Het biedt een uitvoerbaar proces DMAIC:

  • Define (definiëren): vereistenanalyse, omzetten naar datamodel, ontwerp datastructuren en interfaces
  • Measure (meten): testcase monitoring, dekking, prestaties, beveiliging
  • Analyze (analyseren): analyseren van testcases en businesscode
  • Improve (verbeteren): verbeteren van testcases en businesscode
  • Control (beheersen): geautomatiseerde test-poort

Bijpassende technische tools omvatten linters, statische controle, dynamische controle en testframeworks. Er zijn veel codekwaliteitsindicatoren om in de gaten te houden, zoals complexiteitsindicatoren (cyclomatische complexiteit, cognitieve complexiteit, nestingsdiepte, classkoppeling), kwaliteitsindicatoren (duplicatie, functielengte, magische getallen), en testindicatoren (unit testdekking, integratietest slaagpercentage, testcase-effectiviteit, exceptionafdekking).

Methodologie en tools kunnen helpen problemen te ontdekken en op te lossen, tegen de prijs dat ze de leveringskosten mogelijk verhogen en de efficiëntie verlagen. Onder beperkte middelen moet je een methode kiezen die op korte, middellange en lange termijn blijvend voordeel oplevert. Mijn keuze is om unit tests te gebruiken als hulpmiddel in het ontwikkelproces.

Door gebruik te maken van AI’s vermogen om tussen verschillende rollen/talen te schakelen, kan ontwikkeling de voordelen van TDD, BDD en ATDD combineren, snel testtabellen en testcode genereren, en de leveringskwaliteit verbeteren.

Testgedreven ontwikkeling: TDD/BDD/ATDD en testbare code

De verschillen tussen de drie testgedreven methoden zijn als volgt:

DimensieTDDBDDATDD
Aandrijvende bronUnit testsGebruikersgedragsscenario’sAcceptatiecriteria
DeelnemersOntwikkelaarsBusiness + ontwikkelingBusiness + testen + ontwikkeling
TestniveauUnit testsIntegratie/end-to-end testsAcceptatietests
ToolvoorbeeldenJUnit, pytestCucumber, SpecFlowFitNesse, Robot Framework
Typische outputCode dekkingsrapportUitvoerbare gebruikersscenariodocumentatieAcceptatietest slaagpercentage
Taalassert add(2,3) == 5Natuurlijke taal scenario’s (Given-When-Then)Business taal (acceptatiecriteria tabel)

Testbare code heeft vier kernprincipes: single responsibility (elke functie doet maar één ding), dependency injection (afhankelijkheden doorgeven via parameters), interface isolation (duidelijke invoer/uitvoer typen), observable state (query-interfaces bieden om interne staat te valideren).

In de prompts voor AI raad ik aan om direct uitbreidbaar, single responsibility, dependency injection op te nemen. Ik heb ooit geprobeerd AI te laten voldoen aan functielengte en bestandslengte, maar de naleving was slecht; na overstappen naar single responsibility en dependency injection, voldeed de gegenereerde code veel beter aan de verwachtingen. Premature optimalisatie is de wortel van alle kwaad, bij het genereren van basisdatastructuren en interfaces, geef AI prioriteit aan uitbreidbaarheid.

Voorbeeld van code die gemakkelijk te testen is:

// user.h
typedef struct {
    int id;
    char name[50];
    int age;
} User;

// Geabstracteerde opslaginterface
typedef struct {
    User* (*get_user)(int id);
    int (*save_user)(const User* user);
} UserStorage;

// Gebruiker maken (dependency injection van opslaginterface)
User* create_user(const char* name, int age, UserStorage* storage) {
    if(strlen(name) >= 50 || age <= 0) return NULL;

    User* new_user = malloc(sizeof(User));
    new_user->id = generate_user_id();
    strncpy(new_user->name, name, 49);
    new_user->name[49] = '\0';
    new_user->age = age;

    if(storage->save_user(new_user) != 0) {
        free(new_user);
        return NULL;
    }
    return new_user;
}

Voorbeeld van code die moeilijk te testen is:

// Strak gekoppelde databasebewerkingen
void save_user_to_db(User* user) {
    MYSQL conn;
    mysql_init(&conn);
    if(!mysql_real_connect(&conn, "localhost", "root", "pass", "test", 0, NULL, 0)) {
        log_error("DB connection failed"); // Moeilijk te testen log-afhankelijkheid
        exit(1); // Directe afsluiting beïnvloedt testuitvoering
    }
    // SQL samenstellen en naar database schrijven
}

Goede code werkt onopvallend, slechte code valt op. Functies die gemakkelijk te testen zijn, hebben duidelijke returnwaarden, standaard foutafhandeling, duidelijke parameters, geïsoleerde neveneffecten en ondersteunen dependency injection.

AI-code is soms geen best practice en heeft stijlproblemen. Het lost vaak het probleem op, maar een echt project is geen eenmalige klus, je moet rekening houden met toekomstig onderhoud. Bij het prompten van AI om code te genereren, benadruk ook leesbaarheid voorop, uitbreidbaar, single responsibility, dependency injection. Code stijlproblemen worden niet verder besproken, zie de Google code style guide2.

Valkuilen en afwegingen in de praktijk

Vereisten ondergaan bij overdracht tussen rollen meerdere taalconversies, en elke ronde kan uitdrukkingsafwijkingen veroorzaken. Bijvoorbeeld, de klant zegt “centraal bekijken” en de backend begrijpt dit als “één interface retourneert” zonder paginering. Door AI te gebruiken om rol-specifieke taal sjablonen te genereren, kan informatieverlies bij handmatige conversie worden verminderd.

Onvoldoende doordenken is de wortel van veel bugs. Door AI te gebruiken om testscenario’s te ontwerpen, kan het niet alleen de verwachte taak voltooien, maar ook onverwachte inzichten geven en details aanvullen die de releaseverantwoordelijke niet had overwogen. Bij elke taalconversie tussen rollen kun je met AI in gesprek gaan om lekken aan te vullen. Markdown is de taal die AI het beste begrijpt, invoer en uitvoer zouden zoveel mogelijk in Markdown-formaat moeten zijn.

Er zijn een paar punten waar je op moet letten bij testcode: testisolatie, nauwkeurigheid van gesimuleerde afhankelijkheden, geldigheid van assertions, grenswaarden, exceptionafhandeling, prestaties, concurrentie. AI zal lui zijn en niet in één keer alle scenario’s genereren. Het in één keer genereren van alle testcode stelt hoge eisen aan het model, je kunt het in meerdere keren genereren.

Neem Claude model in Cursor als voorbeeld, het presteert goed op sommige aandachtspunten voor testcode, maar vergeet vaak testisolatie en exception scenario’s. Het toevoegen van een user rule kan luiheid enigszins verminderen: bij het genereren van testcases let op testdocumentatie, testisolatie. Toch genereert het niet in één keer alle scenario’s, normaal pad, exception pad, grenswaarden, prestaties, concurrentie etc. moeten via meerdere dialoogrondes worden aangevuld.

Een haalbare AI-ondersteunde testworkflow

Samen met ontwerpdocumenten kun je een pijplijn creëren van scenario naar testcase:

---
title: AI-ondersteunde testworkflow
---
flowchart TB
    A[Ontwerpdocument] e1@--> B[Genereer acceptatietestscenario's]
    B e2@--> C[Genereer datastructuren en interfaces]
    C e3@--> D[Genereer testtabel aan clientzijde]
    D e4@--> E[Genereer testcode]
    D e5@--> F[Genereer businesscode]
    E e6@--> G[Lokale poort: testcases geslaagd]
    F e7@--> G
    G e8@--> H{Er zijn mislukte testcases}
    H e9@-->|Ja| E
    H e10@-->|Nee| I[Indienen voor test]

    classDef input fill:#E3F2FD,stroke:#1565C0,color:#0D47A1;
    classDef gen fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
    classDef gate fill:#FFF3E0,stroke:#E65100,color:#E65100;
    classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
    class A input;
    class B,C,D,E,F gen;
    class G,H,I gate;
    class e1,e2,e3,e4,e5,e6,e7,e8,e9,e10 animate;

De prompts voor elke stap kunnen als volgt worden georganiseerd: genereer acceptabele testscenario’s op basis van het productontwerpdocument; genereer datastructuren en interfaces op basis van testscenario’s; genereer testtabel aan clientzijde op basis van testscenario’s en interfaces; genereer testcode op basis van interfaces en testtabel; genereer businesscode op basis van testscenario’s en zorg ervoor dat testcases slagen.

Er is hier een verborgen risico: AI kan zowel businesscode wijzigen op basis van testcode, als omgekeerd testcode wijzigen op basis van businesscode. Als beide fouten bevatten, raakt AI in de war, en naarmate het aantal dialoogrondes toeneemt, worden testcode en businesscode steeds rommeliger, op dat moment moet er handmatig worden ingegrepen.

Een te groot prompt is een tegenvoorbeeld: vereis dat AI in één keer normale, exception, grenswaarden en concurrentie dekt en code genereert. Dit soort verzoeken worden vaak onderbroken. De juiste aanpak is om in Cursor scenario’s op te splitsen en in batches te genereren via meerdere verzoeken. Performancetestcases vereisen veel data, plaats deze in de systeemtestfase, laat AI eerst een random datagenerator schrijven, en gebruik de gegenereerde data om testcases op te bouwen.

Integratietests vereisen het opbouwen van een testomgeving. Sommige testcases zijn afhankelijk van externe tools, bijvoorbeeld micro-isolatie is afhankelijk van iptables en ipset, je kunt Docker gebruiken om testcontainers te bouwen. Het principe is: hoe een mens test, zo stuur je AI aan om dezelfde automatiseringsstappen uit te voeren. Over beveiligingstesten wordt momenteel vooral vertrouwd op ervaring en analyse, er is nog geen rijpe AI-ondersteunde ervaring.

Samenvatting

Deze workflow produceert vergeleken met niet-AI-gebruik meer documentatie en testcode, en het is moeilijk om elk woord te reviewen. Maar documentatie is gemakkelijker te reviewen dan code, vooral de testtabel, die nauwkeurige begeleiding biedt voor testcasegeneratie. In de praktijk review ik soms alleen mislukte testcases, en bevestig problemen door mislukte testcases te debuggen.

Verbeterpunten zijn drie: het ontwerpfase-subproces heeft overlappingen, acceptatietestscenario’s kunnen afhankelijk zijn van datastructuren en interfaces; de testtabel aan clientzijde is zelf een aandachtspunt voor ontwikkeling; voor het indienen voor test moet opnieuw gedetailleerde testscenario’s worden gegenereerd met businesscode details.

Het uiteindelijke voordeel zit niet in het testdekkingscijfer, maar in vertrouwen: met behulp van de lokale unit test-poort kan ik eindelijk vertrouwen hebben in AI-generatiecode, in plaats van vibe coding-code ondoordacht naar productie te sturen.


  1. Six Sigma definitie en DMAIC methode verwijzen naar het Wikipedia-lemma. ↩︎

  2. Google Style Guide is Google’s openbare code style guide. ↩︎