Als je merkt dat je meer tijd besteedt aan het onderhouden van je Pester-tests dan aan het daadwerkelijk maken van nieuwe, dan is deze post voor jou. In deze post zal ik een project delen waar ik aan heb gewerkt met Devolutions.
Achtergrondverhaal
We hadden een probleem bij Devolutions. De vlaggenschip PowerShell-module Devolutions.PowerShell ontbrak het aan Pester-tests. Ik weet het, ik weet het. Ze hadden C#-tests, maar dat is een ander verhaal.
Als consultant voor Devolutions, gericht op PowerShell, werd mij gevraagd om een reeks Pester-tests te maken die gebruikt konden worden in de CI/CD-pipeline en vóór de productie-implementatie uitgevoerd moesten worden. Geen probleem, dacht ik. Ik heb de module een paar keer gebruikt, en het kon toch niet zo moeilijk zijn. Ik had het mis.

Zonder specifieke verzoeken, alleen “bouw wat tests”, begon ik aan het maken van tests voor de hele module, alleen om te ontdekken dat het bijna 500 commando’s had! Dit zou even duren.
Aangezien ik altijd oplossingen wilde bouwen niet alleen voor vandaag maar ook voor de lange termijn, wilde ik geen stapel tests in een enkel script gooien en het daarbij laten. Dat script zou enorm zijn!
In plaats daarvan, voordat ik een enkele test schreef, wilde ik een framework ontwerpen dat ik kon gebruiken dat zou:
- Schaalbaar zijn – Ikzelf of anderen konden eenvoudig tests aan het framework toevoegen zonder veel na te denken.
- Om code duplicatie te voorkomen, moet dit project DRY (don’t repeat yourself) zijn. Ik wilde geen code dupliceren, dus gebruikte ik in plaats daarvan gedeelde scripts, functies, enz.
- Om datagestuurde tests te gebruiken: Datagestuurde tests bevorderen de herbruikbaarheid van code door tests in een datastructuur te definiëren en vervolgens tests deze structuur te laten refereren in plaats van testcode voor elke test te dupliceren.
- Wees modulair – Alle tests moeten worden opgesplitst in een structuur die voorkomt dat je door onnodige code moet scrollen om te vinden wat je zoekt.
- Om omgevingsconfiguratie en teardown-scenario’s te ondersteunen – Deze tests zijn end-to-end (E2E)/integratie/acceptatietests, wat je maar wilt. Ze testen het gebruik door de eindgebruiker; er zijn geen unittests, dus ze moeten verschillende zaken in de omgeving hebben om te kunnen draaien.
- Om groepen van tests te ondersteunen zonder ze allemaal tegelijk uit te voeren.
Deze vereisten leiden tot het Pester-testframework voor Devolutions.
Vereisten
Als je dit framework wilt gebruiken, kan ik niet garanderen dat het 100% werkt, tenzij je in dezelfde omgeving werkt als ik. Om dit framework te gebruiken, heb ik:
- PowerShell v7.4.5 op Windows
- Pester 5.6.1
Dit werkt nog steeds op Windows PowerShell en eerdere versies van Pester v5, maar er zijn geen garanties!
Overzicht van het framework
Dit framework bestaat uit vier componenten: een aanroepscript, verschillende testdefinitie-scripts, een optioneel script om hulpfuncties op te slaan, en optionele before all
/ before each
scripts. Elk van deze componenten is als volgt gestructureerd in het bestandssysteem:
📁 root/ 📄 caller.tests.ps1 📄 helper_functions.ps1 📁 test_definitions/ 📁 group1/ 📄 beforeall.tests.ps1 📄 beforeeach.tests.ps1 📄 core.tests.ps1 📄 subgroup.tests.ps1
Wanneer je het aanroep-script aanroept:
Invoke-Pester -Path root/caller.tests.ps1
Het aanroep-script:
- Vindt alle testdefinitiescripts in elke groep.
- Vindt alle
beforeeach
enbeforeall
scripts voor elke groep. - Creëert Pester-contexten voor elke groep en ondergroep.
context 'groep' { context 'ondergroep' { } }
- Roept het
beforeall
script eenmaal aan voordat een test wordt uitgevoerd in een groep. - Roept het
beforeeach
script aan voor elke test in de groep. - Tenslotte roept het de testasserties aan die gedefinieerd zijn in elke testdefinitie.
De Testscript van de Beller
Het aanroepscript is het uitvoeringspunt van Pester. Het is het script dat wordt aangeroepen wanneer je tests binnen het kader moet aanroepen via Invoke-Pester
.
Het aanroepscript is opgesplitst in X gebieden:
Het Blok VóórOntdekking
Een taak van het aanroepscript is om alle testdefinities te vinden en wat de ontdekkingsfase van Pester is. Binnen het Blok VóórOntdekking van het script worden testdefinities verzameld, evenals eventuele before each
of before all
scripts.
BeforeDiscovery { # Initialize hashtable to store all test definitions $tests = @{} # Iterate through test group directories (e.g. datasources, entries) Get-ChildItem -Path "$PSScriptRoot\test_definitions" -Directory -PipelineVariable testGroupDir | ForEach-Object { $testGroup = $testGroupDir.BaseName $tests[$testGroup] = @{} # Load beforeall script for test group if it exists # This script runs once before all tests in the group $testGroupBeforeAllScriptPath = Join-Path -Path $testGroupDir.FullName -ChildPath 'beforeall.ps1' if (Test-Path -Path $testGroupBeforeAllScriptPath) { $tests[$testGroup]['beforeall'] += . $testGroupBeforeAllScriptPath } # Load beforeeach script for test group if it exists # This script runs before each individual test in the group $testGroupBeforeEachScriptPath = Join-Path -Path $testGroupDir.FullName -ChildPath 'beforeeach.ps1' if (Test-Path -Path $testGroupBeforeEachScriptPath) { $tests[$testGroup]['beforeeach'] += . $testGroupBeforeEachScriptPath } # Load all test definition files in the group directory Get-ChildItem -Path $testGroupDir.FullName -Filter '*.ps1' -PipelineVariable testDefinitionFile | ForEach-Object { $tests[$testGroup][$testDefinitionFile.BaseName] = @() $tests[$testGroup][$testDefinitionFile.BaseName] += . $testDefinitionFile.FullName } } }
Het Blok BeforeAll
Het Blok BeforeAll
wordt uitgevoerd om hulpfuncties beschikbaar te maken voor het aanroepscript of de testdefinities. In Pester v5 kan deze taak niet in BeforeDiscovery
staan; anders zou het niet beschikbaar zijn voor de tests.
# Load helper functions used across all tests BeforeAll { . (Join-Path $PSScriptRoot -ChildPath "_helper_functions.ps1") }
Het Blok Describe
en Contexts
Elke omgevingsconfiguratie die je nodig hebt om tests tegen uit te voeren is opgesplitst in contexten met testgroepen daaronder.
Voor de Devolutions PowerShell-module, aangezien we cmdlets moeten testen tegen veel verschillende soorten gegevensbronnen, heb ik contexten gemaakt per gegevensbron, maar je kunt hier alles gebruiken. Vervolgens, met de ForEach
-parameter van Pester, is elke testdefinitiemap een context, net als elke subgroep. Daarna wordt elke test gedefinieerd als it
-blokken.
Let op de where()
-methode op het it
-blok. Where({ $_.environments -contains 'xxx' -or $_.environments.count -eq 0})
. Dit is waar de structuur van elke definitie in het spel komt. Dit deel is waar we aanwijzen welke tests in welke omgeving worden uitgevoerd.
# Main test container for all tests Describe 'RDM' { # Tests that run against an environment Context 'Environment1' -Tag 'Environment1' { BeforeEach { ## Do stuff to execute before each test in this content. For example, this is used to set up a specific data source for Devolutions Remote Desktop Manager } # Clean up AfterAll { } # Run each test group against the environment Context '<_>' -ForEach $tests.Keys -Tag ($_) { $testGroup = $_ # Run each test subgroup (core, properties, etc) Context '<_>' -ForEach $tests[$testGroup].Keys -Tag ($_) { $testSubGroup = $_ # Run tests marked for this environment or all It '<name>' -ForEach ($tests[$testGroup][$testSubGroup]).Where({ $_.environments -contains 'xxx' -or $_.environments.count -eq 0}) { & $_.assertion } } } } ## Other environments. If you have specific configurations some tests must have, you can define them here by creating more context blocks. }
Testdefinities
Vervolgens heb je het belangrijkste deel: de tests! De tests worden gemaakt in subgroepen (subgroup.tests.ps1
) binnen elke groepsmap en moeten worden aangemaakt als een array van hashtables met de volgende structuur:
@( @{ 'name' = 'creates a thing' 'environments' = @() ## Nothing means all environments 'assertion' = { ## Teating something $true | should -Be $true } } )
Hier kun je de omgevingen in het aanroepende script definiëren om de scripts uit te voeren. Als je bijvoorbeeld een omgeving hebt voor elke gegevensbron die ik heb gebruikt voor Devolutions RDM, zijn mijn omgevingen xml
, sqllite
, enzovoort.
Hulpfuncties
Ten slotte hebben we het hulpfunctiescript. Dit script bevat functies die we dot source in het BeforeAll
-blok in het aanroepende script. Dit is waar je alle functies kunt plaatsen die je opnieuw wilt gebruiken. In mijn voorbeeld heb ik functies om gegevensbronnen op te zetten en ze allemaal te verwijderen.
# Helper function to remove all entries from the current data source # Used for cleanup in test scenarios function Remove-AllEntries { try { # Get all entries and their IDs # Using ErrorAction SilentlyContinue to handle case where no entries exist $entries = @(Get-RDMEntry -ErrorAction SilentlyContinue) # If no entries found, just return silently # No cleanup needed in this case if ($entries.Count -eq 0) { return } # Delete entries one at a time # Using foreach loop to handle errors for individual entries foreach ($entry in $entries) { try { # Remove entry and refresh to ensure UI is updated Remove-RDMEntry -ID $entry.ID -Refresh -ErrorAction Stop } catch [System.Management.Automation.ItemNotFoundException] { # Silently ignore if entry is already gone # This can happen if entry was deleted by another process continue } } } catch { # Only warn about unexpected errors # Ignore "Connection not found" as this is expected in some cases if ($_.Exception.Message -ne 'Connection not found.') { Write-Warning "Error during cleanup: $_" } } } # Helper function to remove all data sources and their files # Used for cleanup in test scenarios function Remove-AllDatasources { # Remove any data sources currently loaded in memory # This ensures clean state for tests Get-RDMDataSource | ForEach-Object { Remove-RDMDataSource -DataSource $_ } # Delete any existing data source folders on disk # These are identified by GUIDs in the RDM application data folder Get-ChildItem $env:LOCALAPPDATA\Devolutions\RemoteDesktopManager | Where-Object { $_.Name -match '^[0-9a-fA-F\-]{36} }
Hoe je je framework bouwt
Ziet dit framework er nuttig uit? Denk je dat het voordelig zou zijn voor jouw organisatie? Zo ja, hier zijn een paar tips en vragen om over na te denken.
- Hoeveel tests verwacht je nodig te hebben? Is jouw gebruiksscenario voldoende om dit framework te ondersteunen in plaats van slechts een paar testscripts?
- Voordat je PowerShell schrijft, beschrijf hoe je van plan bent groepen en subgroepen te definiëren. Maak ze een logische abstractie van de elementen die je test.
- Denk na over omgevingen en de configuratie die je nodig hebt voor elke set tests; maak die als contexten in het aanroepscript.
- Terwijl je testdefinities opbouwt, als je merkt dat je code herhaalt, maak dan een functie ervoor en plaats deze in helper_functions.ps1.
Het bouwen van een schaalbaar Pester-testframework vereist zorgvuldige planning en organisatie, maar de voordelen zijn de moeite waard. Door de structuur te volgen die in dit artikel wordt uiteengezet – met zijn modulaire ontwerp, herbruikbare componenten en flexibele omgevingsafhandeling – kun je een robuuste testoplossing creëren die met je behoeften meegroeit. Of je nu PowerShell-modules, infrastructuurconfiguraties of complexe applicaties test, dit framework biedt een solide basis voor het handhaven van codekwaliteit en betrouwbaarheid binnen jouw organisatie.
Source:
https://adamtheautomator.com/powershell-pester-testing-framework/