E2exe: Hoe we Windows desktop-automatisering voor enterprise-schaal hebben opgelost

Er is een goede reden waarom het testen van Windows desktop-apps een van de meest gevraagde functies van klanten was: niemand ter wereld beschikt over de infrastructuur om duizenden runs per maand parallel uit te voeren.

Infrastructuur voor webtesten is relatief eenvoudig: een headless browser start binnen enkele seconden in een Linux-container, kost een fractie van een cent om te draaien en wordt weggegooid zodra de test eindigt. Voor het testen van een Windows desktop-app bestaat er geen equivalent.

Bij het testen van Windows-apps spelen de volgende problemen:

  • Elke test heeft een volledige VM nodig, omdat Windows-containers geen grafische desktop bevatten om de app uit te voeren.
  • De machines starten traag. Een koude Windows VM doet er 2 tot 3 minuten over om op te starten, terwijl een browsercontainer slechts seconden nodig heeft.
  • Er moet iemand zijn ingelogd. Windows UI Automation (UIA), de toegankelijkheidsinterface die testtools gebruiken om knoppen en velden te vinden, werkt alleen binnen een actieve, ingelogde desktopsessie. Een machine die simpelweg is aangezet, is niet voldoende.
  • Machines onthouden zaken. Geïnstalleerde bestanden, registry-sleutels, gebruikersprofielen, opgeslagen logins en weesprocessen overleven tussen runs.
  • Elke app brengt zijn eigen omgeving mee. Installatieprogramma's, admin-prompts, gelicentieerde afhankelijkheden, dialoogvensters bij de eerste start, aanmeldingen, etc.
  • Er is geen ingebouwde zichtbaarheid. Een browser levert traces, logs en screenshots. Een remote Windows-machine levert niets, tenzij je dit zelf bouwt.
  • De tooling is ontworpen voor Linux. Kubernetes, CI-runners en de meeste testinfrastructuur zijn ontworpen rondom Linux-containers.

Ons antwoord was om Windows-machines niet langer als servers te behandelen, maar als telefoons: een apparaat dat door één test wordt geclaimd, één keer wordt gebruikt en daarna wordt weggegooid.

De Architectuur

De basisarchitectuur is ontleend aan onze mobiele testinfrastructuur, omdat mobiel testen dezelfde beperkingen heeft: de testcode kan niet draaien op het apparaat dat wordt getest, en elke test heeft tijdens de uitvoering een eigen apparaat nodig. Omdat een Windows-machine in Kubernetes wordt gemodelleerd als een apparaat, kan een test via onze aangepaste device scheduler een VM aanvragen met een DeviceClaim, de verbindingsgegevens ontvangen, het apparaat op afstand besturen via WebdriverIO en Appium, en de machine vrijgeven wanneer de test is voltooid.

Windows vereiste twee uitbreidingen van het apparaatmodel: een apparaat hoeft niet langer binnen het Kubernetes-cluster te leven (een GCE VM bevindt zich volledig daarbuiten), en een claim kan de verbindingsadressen van een desktop teruggeven, inclusief CDP voor browsers. Verder volgt de Windows-runner stap voor stap de levenscyclus van de mobiele runner (claim, heartbeat, record, connect, run, release). Tests communiceren met native apps via dezelfde WebdriverIO- en Appium-koppeling, en private Windows-pools werken op dezelfde manier als private iOS-apparaten.

Het systeem bestaat uit drie delen:

  1. Een controller die in Kubernetes draait. Deze beheert een pool van Windows VM's op Google Compute Engine (GCE) en wijst deze toe aan tests.
  2. Een runner, een Linux-pod die de testcode uitvoert.
  3. Een Windows VM waarop de te testen app, de automatiseringstools en een kleine desktop-agent draaien.

De runner communiceert met de VM via drie verbindingen. Playwright verbindt via CDP (Chrome DevTools Protocol) voor alles in een browser. Appium handelt de native Windows-apps af. Een WebRTC (Web Real-Time Communication) stream verzendt het scherm, zodat mensen de test live kunnen volgen en achteraf een opname kunnen bekijken.

Net als een Android-emulator is elke Windows VM wegwerpbaar. Niemand logt er handmatig in, patched de machine of houdt deze wekenlang aan. Een test claimt de VM, gebruikt deze één keer en geeft hem terug, waarna een verse VM de plaats inneemt.

Wanneer een gebruiker een run start, gebeurt het volgende:

  1. De test start in een Linux-runner pod, die vraagt om een Windows-apparaat.
  2. De scheduler wijst een VM toe uit een pool van machines die al zijn opgestart en ingelogd.
  3. De runner ontvangt de verbindingsadressen van die VM voor CDP, Appium en screen streaming.
  4. De te testen app wordt naar de VM verzonden en gelanceerd.
  5. De test wordt uitgevoerd. Het scherm wordt live gestreamd en opgenomen.
  6. De test eindigt, de claim wordt vrijgegeven en de VM wordt vernietigd. Een verse VM neemt de plaats in de pool in.

Windows-machines on-demand huren

De eerste actie van een Windows-test is het aanvragen van een machine. Dit is de taak van onze device scheduler. Voor Windows hebben we deze service twee nieuwe taken geleerd: het aanhouden van een voorraad VM's gehuurd van Google Compute Engine, en het toewijzen van één VM aan elke test die hierom vraagt.

De scheduler houdt de vloot bij via drie nieuwe Kubernetes custom resources:

  • WindowsVMTemplate: beschrijft hoe een VM eruitziet (machine-image, grootte en configuratie).
  • WindowsVMPool: beschrijft hoeveel klaarstaande VM's er voor een bepaald template bewaard moeten worden.
  • WindowsVM: representeert één individuele machine.

Het toewijzen van een machine begint met een DeviceClaim. De scheduler koppelt de claim aan een beschikbare VM uit de juiste pool en stuurt de verbindingsadressen (CDP, Appium en screen stream) terug naar de runner.

Het op voorraad houden van machines is de taak van een reconciler binnen de scheduler. Deze vergelijkt constant de records met wat er daadwerkelijk draait in GCE. Als een pool drie machines tekortkomt, worden er drie aangemaakt. Wanneer een test zijn claim vrijgeeft, wordt die VM verwijderd. Omdat Windows VM's minuten nodig hebben om te creëren en op te starten, verplaatst de reconciler elke machine stap voor stap via korte passes en schrijft de voortgang naar het record van de VM. Hierdoor vertraagt een trage boot-tijd de andere processen van de scheduler niet.

Snel schone VM's leveren

De centrale spanning in desktop-testinfrastructuur is dat schone machines traag zijn en snelle machines niet schoon. Opstarten vanaf een verse image kost minuten, en het hergebruiken van een machine betekent dat elke test overneemt wat de vorige heeft achtergelaten.

Ons systeem lost beide kanten op:

  • Elke VM is voor eenmalig gebruik. Een VM wordt nooit hergebruikt na een test. Hierdoor ontstaan er geen problemen waarbij een test in isolatie wel slaagt, maar in een suite faalt.
  • Warm pools absorberen de opstarttijd. Elke pool houdt VM's opgestart, ingelogd en wachtend, zodat een test een machine claimt die al klaar is en de koude start van 2 tot 3 minuten overslaat.
  • Verlaten machines worden teruggeclaimd. Een geclaimde VM rapporteert activiteit tijdens de test. Als deze 60 seconden stil is, gaat de controller ervan uit dat de test is gestopt en claimt hij de machine terug.
  • Capaciteitstekorten worden opgevangen via zones en regio's. Als een cloudzone geen Windows VM kan leveren, maakt de pool deze aan in een andere zone.
  • Updates gebeuren door natuurlijke uitloop. Omdat VM's wegwerpbaar zijn, is er geen upgradeproces nodig voor een nieuwe machine-image. Oude VM's worden opgebruikt en vervangen door nieuwe, en idle VM's met een verouderde image worden gerecycled.

Een Windows-image gebouwd voor automatisering

Native apps hebben geen DOM die Playwright kan aansturen, dus een standaard Windows-installatie kan geen geautomatiseerde UI-tests uitvoeren. Appium, de Appium Windows driver en Microsoft's WinAppDriver moeten zijn geïnstalleerd, geconfigureerd en draaien in een actieve desktopsessie voordat de eerste test arriveert. Dit is allemaal ingebakken in een basis-machine-image. De image draait ook een aparte root-level sessie voor delen van Windows die niet tot de app behoren, zoals notificaties en het Actiecentrum.

Een PowerShell-setup-script produceert de image en publiceert deze naar een image-family in GCE, zodat deze eenvoudig reproduceerbaar is. Er wordt geen "golden machine" handmatig onderhouden.

De basis-image bevat veelvoorkomende software zoals Google Chrome en Adobe Acrobat. We ondersteunen ook varianten van de basis-image; een Office-image ruimt bijvoorbeeld de dialoogvensters bij de eerste start van Microsoft Word op voordat een test deze ziet. Voor specifieke behoeften (gelicentieerde software, specifieke configuraties) kunnen we private images bouwen op basis van een setup-script van de klant.

Testcode blijft op Linux

Alles wat betreft het uitvoeren van een QA Wolf-test (runners, opnames, CI-integratie, resultaatrapportage) is gebouwd op Linux. Het uitvoeren van testcode op Windows zou betekenen dat we twee kopieën van alles moeten onderhouden. Daarom draait de testcode in een Linux-runner pod en bestuurt deze de VM op afstand; de VM draait alleen de app en de automatiseringseindpunten.

Wanneer de runner verbindt met zijn VM, opent hij twee kanalen naar dezelfde machine. Browsers op de VM worden bereikt via Playwright's chromium.connectOverCDP(), en native apps via WebdriverIO en Appium.

Test-auteurs werken met één API. Het @qawolf/flows/windows entrypoint biedt launch() voor browsers en startApp() voor native apps. Een enkele expect maakt gebruik van de matchers van Playwright of WebdriverIO, afhankelijk van waar op wordt gecontroleerd. Een test kan inloggen via een browser, overschakelen naar een native app en het resultaat controleren zonder dat de auteur hoeft te weten welk protocol elke stap aanstuurt.

const app = await startApp("C:\\qawolf\\apps\\Invoicing.exe");
await app.$("~NewInvoiceButton").click();
await app.$("~CustomerName").setValue("Acme Corp");
await app.$("~SaveButton").click();
await expect(app.$("~StatusBar")).toHaveText("Invoice saved");

Het praktische effect is dat een Windows-test eruitziet als elke andere QA Wolf-test en door dezelfde pipeline loopt.

De app op een gloednieuwe machine krijgen

Bij wegwerpbare VM's moet de te testen app bij elke run snel en veilig op een nieuwe machine terechtkomen. Vaak arriveert de app als een installer in plaats van een direct lanceerbaar bestand.

De bestanden van de app passeren nooit de runner. Wanneer een test startApp() aanroept, genereert het systeem een kortstondige signed URL en downloadt de VM de app rechtstreeks uit storage via Private Google Access (een netwerkpad binnen het netwerk van Google). Alleen de centrale API-service van QA Wolf kan deze URL's signeren. Testcode draait als een onbevoorrechte gebruiker en vraagt downloads aan via een vertrouwde parent-process, zodat de code in een test geen willekeurige URL's kan signeren.

Zodra het bestand is binnengekomen, handelt de VM de installatie af. Er is ondersteuning voor .exe, .msi en .msix installers, inclusief het doorgeven van MSI-properties. Tests kunnen ook bestanden naar de VM pushen of bestanden terugtrekken, wat scenario's mogelijk maakt zoals het importeren van een document of het controleren van een geëxporteerd rapport.

Inkijken in een remote desktop

Een remote Windows VM is standaard een black box. Het schrijven van een test, het debuggen van een fout of het reviewen van een run vereist dat men kan zien wat de test zag op het moment dat het gebeurde.

Elke VM draait een kleine desktop-agent geschreven in Go. Deze legt het scherm vast met FFmpeg en streamt dit via WebRTC, en simuleert muis-, toetsenbord- en klembord-input door Windows' eigen user32.dll aan te roepen. De agent gebruikt geen CGo, waardoor hij vanuit een Mac of Linux-machine kan worden gecross-compileerd voor Windows. De Linux-, Windows- en Mac-agents delen één codebase, waarbij de Linux-build niet wordt beïnvloed door de Windows-code.

De agent leest de werkelijke schermgrootte uit Windows in plaats van te vertrouwen op configuraties, zodat de stream altijd het volledige bureaublad op de juiste schaal toont. Ook biedt de agent diagnostics- en logs-eindpunten die rapporteren wat de capture-pipeline en de WebRTC-verbinding precies doen.

Voor de gebruikers van QA Wolf betekent dit:

  • Live bekijken van een Windows-test tijdens de uitvoering.
  • Een video-opname van elke run.
  • Een element-chooser voor native apps om UIA-elementen te selecteren bij het schrijven van een test.
  • Screenshots, acties en element-inspectie via de QA Wolf MCP (Model Context Protocol) server en publieke API, zodat AI-agents op dezelfde manier met Windows-runs kunnen werken als met browser-runs.

Klantnetwerken gesloten houden

Desktop-apps communiceren vaak met systemen achter een firewall, zoals interne API's, licentieservers of databases. Klanten moeten exact weten wat er verbinding met hen maakt.

  • Eén bekend IP-bereik. VM-verkeer vertrekt vanuit het eigen IP-bereik van QA Wolf, zodat een klant één bereik op een allowlist zet in plaats van een wisselende set cloud-adressen.
  • Private pools blijven privé. Een dedicated pool kan alleen worden geclaimd door de workspace waartoe deze behoort.
  • Geen opgeslagen cloud-sleutels. De controller authenticeert bij Google Cloud via Workload Identity, dus er liggen geen langdurige inloggegevens op een machine.
  • Niets persisteert. Omdat VM's na elke test worden vernietigd, blijven er geen klantgegevens op een machine staan na een run.

Voeg Windows vandaag toe aan je E2E-testsuite

Testen van Windows desktop-apps is nu beschikbaar. Het werkt via het QA Wolf-platform en onze MCP, met een enkele API voor het schrijven van tests over web, iOS en Android. Eén test kan een browser openen met Playwright, een native app besturen via WebdriverIO en Appium, en het resultaat controleren met één expect, ongeacht of de app op het web, op mobiel of op een Windows-desktop draait.