PowerShell-Tests mit Pester ── Der praktische Ansatz, um Betriebsskripte robuster zu machen
· Go Komura · PowerShell, Pester, Windows, Testen, Automatisierung, CI, Nutzung von Altbeständen
1. Was Sie zuerst verstehen sollten
PowerShell-Skripte beginnen meist als Automatisierung für kleine Aufgaben.
Dateien sammeln. Protokolle durchsuchen. Eine CSV erzeugen. Alte Dateien verschieben. Den Status eines Diensts prüfen.
Solange jedes davon nur ein paar Dutzend Zeilen umfasst, lässt sich die Funktion mit bloßem Auge prüfen. Doch während ein Skript im laufenden Betrieb weiterverwendet wird, schleichen sich Änderungen wie diese ein:
- weitere Zielordner hinzufügen
- Ausschlussbedingungen ergänzen
- CSV-Spalten ändern
- vor dem Löschen archivieren
- aus dem Taskplaner oder der CI heraus ausführen
- bei Fehlern benachrichtigen
Auf dieser Stufe reicht „es hat einmal bei mir funktioniert“ nicht mehr aus.
Das Beängstigende an PowerShell ist die Kehrseite seiner Bequemlichkeit. Rein lesende Operationen lassen sich unbeschwert ausprobieren, aber bei Vorgängen wie Löschen, Verschieben, Überschreiben, Dienste neu starten oder Berechtigungen ändern kann schon ein kleiner Fehler in einer Bedingung zu einem Vorfall führen.
Genau hier setzt Pester an, das Testframework für PowerShell. Dieser Artikel deckt nicht alle Funktionen von Pester ab, sondern zeigt, wie Sie bestehende PowerShell-Skripte mit „Testpflege“ in der Praxis robuster machen.
PowerShell-Tests dienen nicht nur dazu, sauberen Code zu schreiben. Sie sind ein Werkzeug, um vor einer Änderung Unsicherheit abzubauen und nach einer Änderung mit Belegen zu bestätigen, dass alles passt.
Der in diesem Artikel gezeigte Code ist außerdem als vollständiges, mit Invoke-Pester ausführbares Beispielpaket auf GitHub veröffentlicht (das zu testende Skript, die Pester-Tests und ein Ausführungsskript für die CI).
pester-powershell-test-maintenance - komurasoft-blog-samples (GitHub)
2. Was Pester eigentlich schützt
Wenn Sie Pester einführen, wird nicht automatisch alles sicher. Zuerst müssen Sie festlegen, „was die Tests schützen sollen“.
Bei PowerShell-Betriebsskripten zahlt es sich aus, diese vier Bereiche zu priorisieren.
| Was Sie schützen wollen | Was der Test prüft |
|---|---|
| Bedingungsprüfung | Welche Dateien, Zeilen, Benutzer oder Dienste betroffen sind |
| Form der Ausgabe | Spaltennamen in der CSV, Eigenschaften des Rückgabewerts, Anzahl der Einträge |
| Der Schritt vor einer gefährlichen Operation | Ob die Ziele von Löschen, Verschieben oder Stoppen wie beabsichtigt sind |
| Externe Abhängigkeiten | Der Umgang mit Dateisystem, APIs, Befehlsausführung, Datum/Uhrzeit und Umgebungsvariablen |
Besonders zuerst testen sollten Sie nicht den Löschvorgang selbst, sondern die Logik, die auswählt, was gelöscht wird.
Bei einem Skript, das alte Protokolle löscht, testen Sie also nicht sofort Remove-Item, sondern zuerst, „welche Protokolle als Ziel ausgewählt werden“.
Diese Trennung macht das Testen deutlich einfacher.
Funktion, die die Ziele sammelt
↓
Schritt, der die Ziele prüft und protokolliert
↓
Ändernder Schritt: Verschieben, Löschen, Benachrichtigen usw.
Testpflege bei PowerShell bedeutet nicht, ein bestehendes Skript sofort grundlegend umzubauen. Lagern Sie zunächst die Entscheidungslogik vor der gefährlichen Operation in eine Funktion aus und prüfen Sie deren Rückgabewert mit Pester.
3. Die Version abgleichen
Dieser Artikel geht von Pester v5 aus.
In älteren Windows-Umgebungen kann bereits Pester vorinstalliert sein — allerdings noch als v3-Serie. Verwenden Sie nicht einfach, was in der bestehenden Umgebung vorhanden ist, sondern prüfen Sie zuerst die Version.
Get-Module Pester -ListAvailable |
Sort-Object Version -Descending |
Select-Object Name, Version, Path
Für eine Neuinstallation installieren Sie aus der PowerShell Gallery.
Install-Module -Name Pester -Scope CurrentUser -Force -SkipPublisherCheck
Import-Module Pester
Get-Module Pester
-SkipPublisherCheck ist gesetzt, weil das in Windows mitgelieferte alte Pester mit einer Microsoft-Signatur versehen ist. Ohne diesen Parameter bricht die Installation ab, weil der Herausgeber abweicht.
Auf Firmenrechnern läuft dieser Befehl mitunter nicht ohne Weiteres durch. Proxy, TLS 1.2 und der NuGet-Provider sind typische Ursachen. Die Lösung dafür finden Sie in Kapitel 17.
Arbeiten Sie im Team, prüfen Sie, ob die Pester-Version zwischen dem eigenen Entwicklungsrechner, dem Build-Server und der Ausführungsumgebung für Aufgaben nicht auseinanderläuft.
Eine häufige Verwirrung bei PowerShell-Tests entsteht nicht durch den Code, sondern durch Versionsunterschiede beim Test-Runner.
Besonders in älteren Artikeln oder internen Notizen findet sich mitunter noch die Schreibweise von Pester v4 oder älter. Wenn Sie neu aufsetzen, ist es später leichter lesbar, sich an der Schreibweise von v5 zu orientieren.
4. Den Ablageort der Dateien festlegen
Bei Pester ist es üblich, Testdateien *.Tests.ps1 zu nennen.
Die minimale Struktur sieht so aus:
scripts/
Get-OldLogFile.ps1
Get-OldLogFile.Tests.ps1
Bei etwas größerem Umfang trennen Sie src und tests:
src/
public/
Get-OldLogFile.ps1
Remove-OldLogFile.ps1
tests/
public/
Get-OldLogFile.Tests.ps1
Remove-OldLogFile.Tests.ps1
Beides funktioniert. Wichtig ist, eine Regel festzulegen.
- Eine Testdatei pro Funktion
- Testdateinamen enden auf
.Tests.ps1 - Das Laden des Testobjekts überall gleich gestalten
- Unit-Tests und Integrationstests nicht zu sehr vermischen
Anfangs reicht es, die .ps1-Datei und ihre .Tests.ps1-Datei nebeneinander abzulegen.
5. Einen minimalen Test ausführen
Bereiten Sie zunächst eine einfache Funktion vor, Get-OldLogFile.ps1.
function Get-OldLogFile {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $Path,
[int] $Days = 30,
[string] $Filter = '*.log',
[datetime] $Now = (Get-Date)
)
if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
throw "Folder not found: $Path"
}
$limit = $Now.AddDays(-1 * $Days)
Get-ChildItem -LiteralPath $Path -Filter $Filter -File |
Where-Object { $_.LastWriteTime -lt $limit } |
Sort-Object -Property LastWriteTime |
Select-Object FullName, Name, Length, LastWriteTime
}
Um das Testen zu erleichtern, kann $Now hier als Argument übergeben werden.
Verwendet die Funktion intern jedes Mal direkt Get-Date, ändert sich das Ergebnis je nach Ausführungsdatum des Tests. Wird das Datum zum Argument, lässt sich eine Bedingung wie „Dateien, die zum 1. Juni 2026 älter als 30 Tage sind“ fixieren und testen.
Als Nächstes schreiben Sie den Test in Get-OldLogFile.Tests.ps1.
BeforeAll {
. $PSScriptRoot\Get-OldLogFile.ps1
}
Describe 'Get-OldLogFile' {
BeforeEach {
$script:Root = Join-Path $TestDrive 'logs'
New-Item -ItemType Directory -Path $script:Root -Force | Out-Null
$oldLog = Join-Path $script:Root 'old.log'
$newLog = Join-Path $script:Root 'new.log'
$oldTxt = Join-Path $script:Root 'old.txt'
Set-Content -LiteralPath $oldLog -Value 'old log' -Encoding UTF8
Set-Content -LiteralPath $newLog -Value 'new log' -Encoding UTF8
Set-Content -LiteralPath $oldTxt -Value 'old text' -Encoding UTF8
(Get-Item -LiteralPath $oldLog).LastWriteTime = [datetime]'2026-05-01T00:00:00'
(Get-Item -LiteralPath $newLog).LastWriteTime = [datetime]'2026-05-31T00:00:00'
(Get-Item -LiteralPath $oldTxt).LastWriteTime = [datetime]'2026-05-01T00:00:00'
}
It 'gibt nur .log-Dateien zurück, die älter als die angegebene Anzahl von Tagen sind' {
$result = Get-OldLogFile `
-Path $script:Root `
-Days 30 `
-Now ([datetime]'2026-06-01T00:00:00')
$result | Should -HaveCount 1
$result[0].Name | Should -Be 'old.log'
}
It 'schlägt bei einem nicht vorhandenen Ordner fehl' {
{ Get-OldLogFile -Path (Join-Path $TestDrive 'missing') } |
Should -Throw
}
}
Führen Sie den Test aus.
Invoke-Pester -Output Detailed .\Get-OldLogFile.Tests.ps1
Das hier verwendete $TestDrive ist ein temporärer Bereich, den Pester für Tests bereitstellt. Statt das echte C:\Logs oder einen Freigabeordner zu verwenden, arbeiten Sie nur mit Dateien, die innerhalb des Tests angelegt werden. Bei PowerShell-Skripten mit Dateioperationen ist es sicher, sich früh anzugewöhnen, zuerst $TestDrive zu verwenden.
6. Testnamen als Spezifikation schreiben
Die Zeichenkette, die Sie in Pesters It schreiben, ist nicht nur eine Beschreibung — für jemanden, der sie später liest, ist sie eine kleine Spezifikation.
Zum Beispiel ist ein Name wie dieser etwas schwach:
It 'works' {
# ...
}
Man erfährt nicht, was funktionieren soll.
In der Praxis sind Namen leichter lesbar, die Bedingung und erwartetes Ergebnis enthalten.
It 'gibt nur .log-Dateien zurück, die älter als die angegebene Anzahl von Tagen sind' {
# ...
}
It 'schließt Dateien genau am Stichtag nicht ein' {
# ...
}
It 'schlägt bei einem nicht vorhandenen Ordner fehl' {
# ...
}
Ein guter Testname zahlt sich aus, wenn ein Test fehlschlägt. Erscheint das in der CI-Ausgabe, wissen Sie sofort, was kaputt ist.
[-] Get-OldLogFile.schließt Dateien genau am Stichtag nicht ein
Der Testname ist eine Notiz an Ihr zukünftiges Ich.
7. Eine Randbedingung ergänzen
Das vorherige Get-OldLogFile entscheidet mit dieser Bedingung, ob eine Datei alt ist.
$_.LastWriteTime -lt $limit
Wegen -lt ist eine Datei mit exakt demselben Zeitstempel wie der Stichtag nicht im Ergebnis.
Diese Entscheidung ist klein, aber in der Praxis wichtig. Ob „älter als 30 Tage“ oder „30 Tage zurück einschließlich“ gilt, ändert die Anzahl der betroffenen Dateien.
Ergänzen Sie die Randbedingung im Test.
It 'schließt Dateien genau am Stichtag nicht ein' {
$border = Join-Path $script:Root 'border.log'
Set-Content -LiteralPath $border -Value 'border log' -Encoding UTF8
(Get-Item -LiteralPath $border).LastWriteTime = [datetime]'2026-05-02T00:00:00'
$result = Get-OldLogFile `
-Path $script:Root `
-Days 30 `
-Now ([datetime]'2026-06-01T00:00:00')
$result.Name | Should -Not -Contain 'border.log'
}
Mehr Tests sind nicht automatisch besser. Aber Logik mit Randbedingungen — Datum, Zahlen, Anzahl, Berechtigungen, Dateinamensmuster — bietet den größten Testnutzen.
8. Die Form des Rückgabewerts fixieren
Bei PowerShell-Skripten kann sich die Form des Rückgabewerts unbemerkt ändern.
Anfangs wurde FileInfo direkt zurückgegeben.
Unterwegs kam Select-Object hinzu.
Später wurden für die CSV-Ausgabe Spaltennamen geändert.
Solche Änderungen wirken sich auf nachgelagerte Verarbeitung aus. Testen Sie deshalb die Eigenschaften des Rückgabewerts, um unerwartete Änderungen zu bemerken.
It 'gibt die von der nachgelagerten Verarbeitung benötigten Eigenschaften zurück' {
$result = Get-OldLogFile `
-Path $script:Root `
-Days 30 `
-Now ([datetime]'2026-06-01T00:00:00')
$propertyNames = $result[0].PSObject.Properties.Name
$propertyNames | Should -Contain 'FullName'
$propertyNames | Should -Contain 'Name'
$propertyNames | Should -Contain 'Length'
$propertyNames | Should -Contain 'LastWriteTime'
}
Bei Funktionen, die für CSV-Ausgabe oder Berichtserstellung verwendet werden, sind nicht nur die Werte, sondern auch die Spaltennamen Teil der Spezifikation.
Prüfen Sie nicht nur, ob „es funktioniert hat“, sondern auch, ob „es in der von der nächsten Verarbeitung erwarteten Form zurückgegeben wird“.
9. Löschvorgang von Zielauswahl trennen
Als Nächstes betrachten Sie den Löschvorgang. Zunächst ein schlechtes Beispiel.
Get-ChildItem C:\Logs -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
Remove-Item -Force
Kurz und praktisch, aber schwer zu testen. Weil Zielauswahl und Löschen in einer einzigen Pipeline verbunden sind, ist unklar, wo überhaupt geprüft werden sollte.
In der Praxis trennen Sie das so:
function Remove-OldLogFile {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[string] $Path,
[int] $Days = 30,
[datetime] $Now = (Get-Date)
)
$targets = Get-OldLogFile -Path $Path -Days $Days -Now $Now
foreach ($target in $targets) {
if ($PSCmdlet.ShouldProcess($target.FullName, 'Remove old log file')) {
Remove-Item -LiteralPath $target.FullName -Force
}
}
}
Hier ist SupportsShouldProcess gesetzt, damit die Funktion -WhatIf entgegennehmen kann.
Remove-OldLogFile -Path C:\Logs -Days 30 -WhatIf
Bei löschenden PowerShell-Funktionen ist es sicherer, wo immer möglich einen Probelauf mit -WhatIf zu ermöglichen.
10. Gefährliche Operationen mit Mock ersetzen
In Pester können Sie mit Mock die tatsächliche Befehlsausführung ersetzen.
Beim Testen des Löschvorgangs müssen Sie Remove-Item nicht wirklich ausführen.
Wurde es aufgerufen, wenn es aufgerufen werden sollte? Wurde es nicht aufgerufen, wenn es nicht aufgerufen werden durfte?
Das zu prüfen genügt.
Ein Beispiel für Remove-OldLogFile.Tests.ps1:
BeforeAll {
. $PSScriptRoot\Get-OldLogFile.ps1
. $PSScriptRoot\Remove-OldLogFile.ps1
}
Describe 'Remove-OldLogFile' {
It 'ruft Remove-Item für alte Protokolldateien auf' {
Mock Get-OldLogFile {
[pscustomobject]@{
FullName = 'C:\Logs\old.log'
Name = 'old.log'
Length = 10
LastWriteTime = [datetime]'2026-05-01'
}
}
Mock Remove-Item {}
Remove-OldLogFile `
-Path 'C:\Logs' `
-Days 30 `
-Now ([datetime]'2026-06-01')
Should -Invoke Remove-Item `
-Times 1 `
-Exactly `
-ParameterFilter { $LiteralPath -eq 'C:\Logs\old.log' }
}
It 'ruft Remove-Item mit WhatIf nicht auf' {
Mock Get-OldLogFile {
[pscustomobject]@{
FullName = 'C:\Logs\old.log'
Name = 'old.log'
Length = 10
LastWriteTime = [datetime]'2026-05-01'
}
}
Mock Remove-Item {}
Remove-OldLogFile `
-Path 'C:\Logs' `
-Days 30 `
-Now ([datetime]'2026-06-01') `
-WhatIf
Should -Invoke Remove-Item -Times 0
}
}
In diesem Test sind sowohl Get-OldLogFile als auch Remove-Item gemockt, sodass das echte C:\Logs\old.log nicht existieren muss. Geprüft wird die Entscheidung von Remove-OldLogFile.
- Gibt es Ziele, wird
Remove-Itemaufgerufen - Bei
-WhatIfwirdRemove-Itemnicht aufgerufen - Beim Aufruf wird der beabsichtigte Pfad übergeben
Je gefährlicher eine Operation ist, desto sicherer ist es, die Aufrufbedingungen statt der Ausführung selbst zu testen.
11. Mock nicht übertreiben
Mock ist praktisch, aber übermäßiger Einsatz mindert den Wert der Tests. Mocken Sie alles, entfernen Sie sich zu weit vom tatsächlichen Verhalten von PowerShell.
Als Anhaltspunkt dient diese Übersicht.
| Vorgang | Empfehlung |
|---|---|
| Datum | Per Argument festlegen |
| Dateierstellung | $TestDrive verwenden |
| Löschen/Verschieben | Mit Mock und -WhatIf prüfen |
| Web-API-Aufruf | Invoke-RestMethod usw. mit Mock ersetzen |
| E-Mail-Versand/Benachrichtigung | Den Sendebefehl mit Mock ersetzen |
| CSV lesen/schreiben | Kleine echte Dateien in $TestDrive anlegen |
Mocken Sie sogar das Lesen und Schreiben von Dateien komplett, können Sie echte Probleme mit Zeichenkodierung, Zeilenumbrüchen und Spaltennamen übersehen.
Andererseits ist es bei Vorgängen wie Löschen, Benachrichtigen, externen APIs oder dem Stoppen von Diensten wirklich besser, sie nicht auszuführen.
Trennen Sie „wo Sie das Echte verwenden“ von „wo Sie mocken“.
12. Bestehende Skripte testfreundlich anpassen
Führen Sie Pester ein, ändert sich auch die Schreibweise bestehender Skripte etwas. Ein großer Design-Umbau ist dabei aber nicht von Anfang an nötig — dieses Maß an Anpassung reicht zunächst aus.
Vorher
$limit = (Get-Date).AddDays(-30)
Get-ChildItem C:\Logs -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt $limit } |
Remove-Item -Force
Nachher
function Get-OldLogFile {
param(
[string] $Path,
[int] $Days = 30,
[datetime] $Now = (Get-Date)
)
$limit = $Now.AddDays(-1 * $Days)
Get-ChildItem -LiteralPath $Path -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt $limit }
}
function Remove-OldLogFile {
[CmdletBinding(SupportsShouldProcess)]
param(
[string] $Path,
[int] $Days = 30,
[datetime] $Now = (Get-Date)
)
Get-OldLogFile -Path $Path -Days $Days -Now $Now |
ForEach-Object {
if ($PSCmdlet.ShouldProcess($_.FullName, 'Remove old log file')) {
Remove-Item -LiteralPath $_.FullName -Force
}
}
}
Die Änderungen sind nicht groß.
- Das Datum wurde zum Argument
- Die Zielauswahl wurde in eine Funktion ausgelagert
- Der Löschvorgang wurde in eine eigene Funktion verschoben
SupportsShouldProcesswurde ergänzt
Allein das macht den Code deutlich testfreundlicher.
Bei der Testpflege für PowerShell ist es wirkungsvoller, „Datum“, „Pfad“, „externer Befehl“ und „ändernde Operation“ von außen austauschbar zu machen, als mit Design-Theorie zu beginnen.
13. Die Testkategorien festlegen
In Pester können Sie Describe, Context und It mit Tags versehen.
Trennen Sie zum Beispiel schnelle Unit-Tests von Integrationstests, die die reale Umgebung anfassen.
Describe 'Get-OldLogFile' -Tag 'Unit' {
It 'gibt nur .log-Dateien zurück, die älter als die angegebene Anzahl von Tagen sind' {
# Schneller Test mit TestDrive
}
}
Describe 'Log maintenance smoke test' -Tag 'Smoke' {
It 'kann den echten Protokollordner lesen' {
Test-Path -LiteralPath 'C:\Logs' | Should -BeTrue
}
}
Führen Sie nur die Unit-Tests aus.
Invoke-Pester -TagFilter Unit
Schließen Sie langsame oder umgebungsabhängige Tests aus.
Invoke-Pester -ExcludeTagFilter Slow, RequiresAdmin, Network
In der Praxis hält es sich oft nicht durch, wenn Sie versuchen, jedes Mal alle Tests auszuführen.
Machen Sie zunächst schnelle Tests ohne Nebenwirkungen zum Standard und trennen Sie umgebungsabhängige Tests per Tag ab, um sie bei Bedarf gezielt auszuführen.
Zuordnungstabelle nach Skripttyp
„Was ist bei diesem Skript noch Unit-Test, was schon Integrationstest“ und „was verwenden Sie dabei“ hängt weitgehend vom Skripttyp ab.
| Skripttyp | Was der Unit-Test prüft | Verwendeter Mechanismus | Was der Integrationstest prüft |
|---|---|---|---|
| Dateisammlung/Zielauswahl | Randbedingungen. Bis zu wie vielen Tagen zurück, ob die Erweiterung korrekt filtert | Kleine echte Dateien in $TestDrive anlegen |
Ob die Anzahl im echten Ordner wie erwartet ist |
| CSV-/JSON-Ein-/Ausgabe | Spaltennamen, Spaltenreihenfolge, Zeichenkodierung, Zeilenumbrüche | Echte Dateien in $TestDrive lesen/schreiben |
Ob sich die Datei beim Empfängersystem tatsächlich öffnen lässt |
| Löschen/Verschieben/Überschreiben | Auswahlergebnis der Löschziele und Probelauf mit -WhatIf |
Mock Remove-Item / Mock Move-Item |
Einmal in der Testumgebung ausführen und das Ergebnis prüfen |
| Dienst-/Prozessoperationen | Entscheidungslogik, die aus dem Status die nächste Operation ableitet | Status mit Mock Get-Service u. Ä. erzeugen |
Auf dem Testrechner tatsächlich starten/stoppen |
| Externe API/Benachrichtigung/E-Mail-Versand | Inhalt der zusammengestellten Anfrage oder des Textes | Mock Invoke-RestMethod / Mock Send-MailMessage |
Einmal an eine Staging-Adresse senden |
| Verarbeitung, die anhand von Datum/Zeitraum entscheidet | Beurteilung des Stichtags. Heute, Vortag, Schaltjahr | Datum per Argument festlegen (nicht Mock) |
– |
| Lesen von Registrierungs-/Systemeinstellungen | Wie der gelesene Wert interpretiert wird | Mock Get-ItemProperty |
Den tatsächlichen Wert auf dem realen Gerät prüfen |
Es gibt zwei Blickwinkel.
- Der Unit-Test prüft „die eigene Entscheidung“. Ob Standardbefehle korrekt funktionieren, wird nicht geprüft (Kapitel 19).
- Gefährliche Operationen werden im Unit-Test nicht ausgeführt. Löschen, Verschieben, Senden und Benachrichtigen werden mit
Mockersetzt; die tatsächliche Ausführung wird im Integrationstest mit begrenzter Anzahl geprüft.
Beachten Sie, dass nur beim Datum nicht Mock, sondern ein Argument zur Festlegung verwendet wird. Mocken Sie Get-Date, betrifft das auch jede andere Datumsabfrage im selben Skript.
14. In der CI ausführen
Pester ist schon lokal nützlich, aber wenn ein Team Skripte gemeinsam verwaltet, gibt die Möglichkeit, es in der CI auszuführen, zusätzliche Sicherheit.
Legen Sie zum Beispiel eine Datei wie tools/Invoke-ProjectTests.ps1 an.
$ErrorActionPreference = 'Stop'
# In der Umgebung kann noch ein altes Pester vorhanden sein, deshalb Version 5 oder höher explizit laden
Import-Module Pester -MinimumVersion 5.0.0
$config = New-PesterConfiguration
$config.Run.Path = @(
Join-Path $PSScriptRoot '..\tests'
)
$config.Run.Exit = $true
$config.Output.Verbosity = 'Detailed'
$config.TestResult.Enabled = $true
$config.TestResult.OutputFormat = 'JUnitXml'
$config.TestResult.OutputPath = Join-Path $PSScriptRoot '..\test-results.xml'
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = @(
Join-Path $PSScriptRoot '..\src'
)
$config.CodeCoverage.OutputPath = Join-Path $PSScriptRoot '..\coverage.xml'
Invoke-Pester -Configuration $config
Führen Sie auf der CI-Seite dieses Skript aus.
pwsh -NoProfile -File .\tools\Invoke-ProjectTests.ps1
Wichtig ist, nicht zu viel CI-spezifische Konfiguration in die Testdateien selbst zu schreiben.
Testdateien sind der Ort, an dem Sie Spezifikationen schreiben. Ausgabeformat für die CI, Coverage und Exit-Codes lassen sich im Ausführungsskript übersichtlicher zusammenfassen.
Ist dieses Ausführungsskript einmal fertig, fällt die Konfiguration auf der CI-Seite bei jedem Dienst kurz aus.
Bei GitHub Actions
Legen Sie .github/workflows/pester.yml an.
name: pester
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Pester v5 installieren
shell: pwsh
run: |
Install-Module -Name Pester -MinimumVersion 5.0.0 `
-Scope CurrentUser -Force -SkipPublisherCheck
- name: Pester ausführen
shell: pwsh
run: ./tools/Invoke-ProjectTests.ps1
- name: Testergebnisse hochladen
if: always()
uses: actions/upload-artifact@v4
with:
name: pester-results
path: |
test-results.xml
coverage.xml
Beachten Sie diese Punkte.
- Pester wird explizit installiert. Windows liefert die v3-Serie von Pester mit, daher funktioniert die Schreibweise von v5 erst nach einer Neuinstallation.
-SkipPublisherCheckwird gesetzt. Das mitgelieferte Pester trägt eine Microsoft-Signatur; ohne diesen Parameter bricht die Installation ab, weil der Herausgeber abweicht.shell: pwshwird explizit angegeben. Bei GitHub-gehosteten Windows-Runnern istpwshder Standard, aber bei selbstgehosteten Runnern greift ohne PowerShell 7 ein Fallback auf Windows PowerShell. Die explizite Angabe erspart Ärger durch Umgebungsunterschiede.if: always()sammelt die Ergebnisdateien ein. Gerade wenn Tests fehlschlagen, wollen Sie das Ergebnis sehen, deshalb läuft dieser Schritt auch bei einem Fehlschlag.
$config.Run.Exit = $true lässt den Job fehlschlagen. Damit beendet sich Invoke-Pester bei einem fehlgeschlagenen Test mit einem Exit-Code ungleich null, sodass auch der Schritt fehlschlägt. Fehlt das, wird der Job grün, selbst wenn Tests rot sind.
Bei Azure Pipelines
azure-pipelines.yml sieht so aus.
trigger:
- main
pool:
vmImage: 'windows-latest'
steps:
- task: PowerShell@2
displayName: 'Pester v5 installieren'
inputs:
pwsh: true
targetType: 'inline'
script: |
Install-Module -Name Pester -MinimumVersion 5.0.0 `
-Scope CurrentUser -Force -SkipPublisherCheck
- task: PowerShell@2
displayName: 'Pester ausführen'
inputs:
pwsh: true
filePath: 'tools/Invoke-ProjectTests.ps1'
- task: PublishTestResults@2
displayName: 'Testergebnisse veröffentlichen'
condition: always()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'test-results.xml'
failTaskOnFailedTests: true
PublishTestResults@2 akzeptiert die Formate JUnit, NUnit, VSTest, XUnit und CTest. Da das Ausführungsskript $config.TestResult.OutputFormat = 'JUnitXml' verwendet, wählen Sie hier JUnit. Ändern Sie das Ausgabeformat, gleichen Sie beide Stellen aufeinander ab.
Mit failTaskOnFailedTests: true schlägt die Aufgabe fehl, sobald die Ergebnisdatei Fehlschläge enthält. Standardmäßig ist das false, in diesem Fall wird das Ergebnis nur angezeigt, aber die Aufgabe läuft trotzdem durch.
15. Coverage als Landkarte betrachten, nicht als Ziel
Pester kann auch Code Coverage ausgeben. Anfangs sollten Sie der Zahl aber nicht zu sehr hinterherjagen, denn Coverage ist nicht dasselbe wie Testqualität.
Rufen Sie zum Beispiel die Funktion zur Auswahl der Löschziele einmal auf, gilt diese Zeile als durchlaufen. Sind aber Randbedingungen und Ausschlussbedingungen nicht geprüft, bringt das in der Praxis keine Sicherheit.
Nutzen Sie Coverage so:
- Funktionen finden, die überhaupt nicht durchlaufen werden
- wichtige, aber ungetestete Verzweigungen finden
- Priorität nach Skripten mit hoher Änderungshäufigkeit setzen
- in der CI einen Nachweis der Testausführung hinterlassen
Schauen Sie eher darauf, „ob die wichtigen Entscheidungen getestet sind“, als die Zahl zu erhöhen.
Die Erfassung konfigurieren Sie über den CodeCoverage-Bereich von New-PesterConfiguration.
$config = New-PesterConfiguration
$config.Run.Path = @('.\tests')
$config.CodeCoverage.Enabled = $true
# Messziel. Nicht die Testdateien angeben, sondern die zu testenden Skripte
$config.CodeCoverage.Path = @('.\src')
# Ausgabeformat. Der Standard ist JaCoCo, das sich leicht in die Coverage-Anzeige der CI einbinden lässt
$config.CodeCoverage.OutputFormat = 'JaCoCo'
$config.CodeCoverage.OutputPath = '.\coverage.xml'
# Zielwert. Ein Unterschreiten lässt den Test nicht fehlschlagen, er dient nur als Richtwert
$config.CodeCoverage.CoveragePercentTarget = 75
Invoke-Pester -Configuration $config
Bei CodeCoverage.Path geben Sie nicht die Testdateien an, sondern die zu testenden Skripte. Setzen Sie hier .\tests, messen Sie die Coverage des Testcodes selbst, und Sie erhalten eine hohe Zahl ohne Substanz.
Das Standardausgabeformat ist JaCoCo. Wählen Sie CoverageGutters, erhalten Sie ein Format, mit dem sich der zeilenweise Durchlaufstatus im Editor anzeigen lässt. Für die Einbindung in die CI reicht JaCoCo aus.
CoveragePercentTarget ist ein Richtwert; ein Unterschreiten lässt den Lauf nicht fehlschlagen. Gerade wenn Sie versucht sind, Coverage zum Abnahmekriterium zu machen, kehren Sie zum Anfang dieses Kapitels zurück. Tests, die nur geschrieben werden, um eine Zahl zu erfüllen, sagen Ihnen meist nichts, wenn tatsächlich etwas kaputtgeht.
16. Reihenfolge für die Einführung bei bestehenden Skripten
Führen Sie Pester bei einem bestehenden PowerShell-Bestand ein, kommen Sie leichter voran, wenn Sie nicht sofort alles unter Test stellen. Hier eine empfohlene Reihenfolge.
1. Skripte auswählen, deren Ausfall wehtun würde
Zuerst eignen sich Skripte wie diese:
- sie löschen, verschieben oder überschreiben
- sie laufen täglich oder monatlich
- sie stehen in einer Anleitung, sind aber nur einer Person vertraut
- es gab in der Vergangenheit Bedingungsfehler
- ihre CSV-Ausgabe wird von anderen Geschäftsprozessen verwendet
Beginnen Sie mit dem, was nützlich ist, dessen Ausfall aber wehtun würde.
2. Nur den lesenden Teil in Funktionen auslagern
Zuerst testen Sie nicht die ändernde Logik, sondern die lesende.
Protokolle lesen
Ziele filtern
Anzahl zählen
Für CSV formatieren
Dieser Teil lässt sich mit $TestDrive gut testen, und Unfälle sind unwahrscheinlich.
3. Datum und Pfad von außen übergeben
Feste Daten und Pfade erschweren das Testen.
# Zu vermeiden
$root = 'C:\Logs'
$limit = (Get-Date).AddDays(-30)
So wird es testfreundlich.
param(
[string] $Path,
[datetime] $Now = (Get-Date)
)
Allein dass sich Werte von außen übergeben lassen, erhöht die Teststabilität erheblich.
4. Gefährliche Operationen zuletzt
Fassen Sie Löschen und Verschieben am Ende zusammen.
Ziele erstellen
↓
Ziele im Protokoll festhalten
↓
Mit -WhatIf prüfen
↓
Ausführen
Prüfen Sie im Test dieselbe Reihenfolge.
17. Häufige Stolperfallen
| Symptom | Ursache | Abhilfe |
|---|---|---|
| Läuft lokal durch, schlägt aber in der CI fehl | Unterschiedliches Arbeitsverzeichnis | $PSScriptRoot als Basis verwenden |
| Ergebnis ändert sich je nach Tag | Get-Date wird direkt verwendet |
Ein Argument wie -Now bereitstellen |
| Test droht, echte Dateien zu löschen | Echter Ordner wird verwendet | $TestDrive und Mock verwenden |
| Mock wirkt nicht | Modulgrenze oder Scope weicht ab | -ModuleName und Ladeart prüfen |
| Unklar, wie weit getestet werden soll | Logik ist nicht in Funktionen getrennt | In Zielauswahl, Formatierung und Änderung aufteilen |
| Test ist langsam | Berührt externe Dienste oder Netzwerk | Externe Abhängigkeiten im Unit-Test mit Mock ersetzen |
| Testname sagt nichts aus | Name wie It 'works' |
Bedingung und erwartetes Ergebnis in den Namen aufnehmen |
Was wie ein Pester-Problem aussieht, liegt in Wirklichkeit oft an der Struktur des Skripts.
Teile, die schwer zu testen sind, sind meist auch im Betrieb die Teile, die leicht kaputtgehen.
Stolperstein bei Install-Module (häufig in Firmenumgebungen)
Der Install-Module-Befehl aus Kapitel 3 läuft in einer Umgebung mit direktem Internetzugang problemlos durch. Auf Firmenrechnern, die von der IT-Abteilung verwaltet werden, bleibt er dagegen oft genau hier hängen.
| Symptom | Ursache | Abhilfe |
|---|---|---|
| Keine Verbindung zur PowerShell Gallery | Läuft nicht über den Firmenproxy | -Proxy und -ProxyCredential angeben |
| Fehlschlag mit „Die zugrunde liegende Verbindung wurde geschlossen“ o. Ä. | TLS 1.2 fehlt im Standardprotokoll von Windows PowerShell 5.1 | TLS 1.2 in der Sitzung aktivieren, bevor Sie fortfahren |
| Es wird nach der Installation des NuGet-Providers gefragt und dort bleibt es hängen | Es ist noch die mit Windows PowerShell mitgelieferte PowerShellGet 1.0.0.1 aktiv | Zuerst den NuGet-Provider installieren |
| Es gibt grundsätzlich keinen Zugang nach außen | Geschlossenes Netzwerk | Auf einem anderen Rechner per Save-Module herunterladen und mitbringen, oder ein internes Repository mit Register-PSRepository registrieren |
Die obigen drei Punkte lassen sich meist in dieser Reihenfolge lösen.
# 1. TLS 1.2 aktivieren (in Windows PowerShell 5.1 erforderlich, in PowerShell 7 nicht nötig)
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor
[Net.SecurityProtocolType]::Tls12
# 2. NuGet-Provider installieren (vorab installieren, damit es nicht an einer interaktiven Eingabeaufforderung hängen bleibt)
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force
# 3. Pester über den Proxy installieren
$proxyUri = 'http://proxy.example.local:8080'
$proxyCredential = Get-Credential -Message 'Proxy-Authentifizierung'
Install-Module -Name Pester -MinimumVersion 5.0.0 `
-Scope CurrentUser -Force -SkipPublisherCheck `
-Proxy $proxyUri -ProxyCredential $proxyCredential
Die TLS-1.2-Einstellung wirkt nur innerhalb dieser Sitzung. Ist es lästig, sie jedes Mal einzugeben, tragen Sie sie in ein Profilskript ein.
Verlangt der Proxy keine Authentifizierung, können Sie -ProxyCredential weglassen. Kennen Sie die Proxy-URL nicht, fragen Sie bei der IT-Abteilung nach „der Proxy-Einstellung für den Zugriff auf die PowerShell Gallery (www.powershellgallery.com)“.
Besteht in einem geschlossenen Netzwerk kein Zugang nach außen, führen Sie auf einem Rechner mit Internetzugang Folgendes aus und bringen den entstandenen Ordner einfach mit.
# Auf der Seite mit Internetzugang
Save-Module -Name Pester -MinimumVersion 5.0.0 -Path 'D:\modules'
Auf der Zielseite legen Sie D:\modules\Pester in einem Modul-Suchpfad ab, etwa unter $env:USERPROFILE\Documents\WindowsPowerShell\Modules. Den Ablageort können Sie mit $env:PSModulePath prüfen.
Dokumentieren Sie diesen Ablauf frühzeitig, geht der Einstieg schneller, sobald weitere Personen dazukommen. Dass die Testpflege ins Stocken gerät, weil „Pester sich nicht installieren lässt“, kommt in der Praxis tatsächlich oft vor.
18. Regeln, die Sie für die Testpflege festlegen sollten
Verwaltet ein Team PowerShell-Skripte gemeinsam, legen Sie zuerst Regeln fest, statt sich in Details der Schreibweise zu verlieren.
Zum Beispiel Regeln wie diese:
- Testdateien heißen
*.Tests.ps1 - Das Testobjekt wird ausgehend von
$PSScriptRootgeladen - Tests für Dateioperationen verwenden
$TestDrive - Löschen, Verschieben, Benachrichtigen und API-Aufrufe werden grundsätzlich gemockt
- Datum wird als Argument übergeben, damit es sich fixieren lässt
DescribeoderIterhalten Tags wieUnit,Smoke,RequiresAdmin- Die CI führt standardmäßig
Unitaus - Ändernde Funktionen erhalten wo immer möglich
SupportsShouldProcess - Frühere Fehler bleiben als Regressionstests erhalten
Zu viele Regeln werden nicht eingehalten. Anfangs reichen sogar nur diese drei aus.
TestDrive verwenden
Datum fixieren
Gefährliche Operationen mocken
Allein diese drei Regeln einzuhalten macht PowerShell-Tests schon deutlich stabiler.
19. Auch festlegen, was Sie nicht testen
Bei der Testpflege zählt „was Sie nicht testen“ genauso viel wie „was Sie testen“.
Diese Dinge sollten Sie zum Beispiel im Unit-Test nicht übermäßig prüfen:
- dass Windows’ eigenes
Get-ChildItemkorrekt funktioniert - dass
Remove-Itemtatsächlich Dateien löscht - die interne Spezifikation der PowerShell-Standardbefehle
- dass eine externe API immer antwortet
- dass eine Netzwerkfreigabe immer verfügbar ist
Testen sollten Sie Ihre eigenen Entscheidungen.
- unter welchen Bedingungen etwas zum Ziel wird
- welcher Pfad übergeben wird
- welche Spalte ausgegeben wird
- wie im Fehlerfall verfahren wird
- ob sich gefährliche Operationen im Probelauf prüfen lassen
Trennen Sie die Stelle, an der Sie den Standardbefehlen vertrauen, von der Stelle, an der Sie Ihre eigene Logik schützen.
20. Zusammenfassung
PowerShell ist ein praktisches Werkzeug, um alltägliche Arbeit schnell zu automatisieren. Aber Skripte, die im Betrieb lange verwendet werden, tragen nach und nach mehr Verantwortung. Was anfangs eine einzeilige Anweisung nur für den eigenen Gebrauch war, wird irgendwann zu einem täglich laufenden Betriebsprozess, der die Arbeit anderer und geschäftliche Daten beeinflusst.
Testpflege mit Pester ist die Arbeit, die das Skript bei diesem Wandel schützt.
Hier die wichtigsten Punkte im Überblick.
- Zuerst die Zielauswahl testen
- Datum und Pfad von außen übergeben können
- Dateioperationen in
$TestDriveeinschließen - Löschen, Verschieben, Benachrichtigen und API-Aufrufe mit
Mockersetzen - Ändernde Funktionen mit
-WhatIfprobelauffähig machen - Testnamen so schreiben, dass sie als Spezifikation lesbar sind
- In der CI mit den schnellen, nebenwirkungsfreien Tests beginnen
Sicherer PowerShell-Betrieb entsteht nicht dadurch, dass Sie plötzlich einen großen Mechanismus einführen.
In kleine Funktionen aufteilen. Kleine Tests schreiben. Vor der gefährlichen Operation eine Prüfmöglichkeit schaffen.
Durch diese Summe nähert sich PowerShell von „einem praktischen, aber etwas beängstigenden Skript“ einem „Geschäftswerkzeug, das sich nach jeder Änderung überprüfen lässt“.
Referenzlinks
- Vollständiger Beispielcode zu diesem Artikel (das zu testende Skript, die Pester-Tests, ein Ausführungsskript für die CI) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/pester-powershell-test-maintenance
- Pester Quick Start
- Pester Installation and Update
- Pester File placement and naming
- Pester TestDrive
- Pester Mocking
- Pester Tags
- Pester Configuration
- Pester Test Results
- Pester Code Coverage
- PowerShell Gallery: Pester
- PowerShell Documentation - Microsoft Learn
- Install a package manager for PowerShell (TLS 1.2 und NuGet-Provider) - Microsoft Learn
- Install-Module (-Proxy / -ProxyCredential) - Microsoft Learn
- PublishTestResults@2 - Azure Pipelines-Aufgabenreferenz
- Workflow syntax for GitHub Actions (jobs.<job_id>.steps[*].shell)
Verwandte Artikel
- PowerShell-Befehlsgrundlagen ── Die ersten Operationen, die man lernen sollte, und ihre sichere Anwendung
- PowerShell-Skripte in der Praxis ── Protokollauswertung, Archivierung und Reporting sicher automatisieren
- Praktische PowerShell-Befehlssammlung — Kleine, oft genutzte Funktionen für den Alltag ausbauen
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
PC-Kitting mit winget + PowerShell automatisieren — Aus dem Handbuch ein ausführbares Skript machen
Wie Sie die Einrichtung von PCs für neue Mitarbeitende reproduzierbar gestalten. Behandelt die Installation von Anwendungen mit winget so...
Mit REST-APIs aus PowerShell arbeiten — Invoke-RestMethod in der Praxis
Ein praktischer Leitfaden zum Aufruf interner und SaaS-REST-APIs aus PowerShell: das Übergeben von Authentifizierungsheadern, das Vermeid...
Wo Sie nachsehen sollten, wenn ein PowerShell-Skript langsam ist — Arrays, Pipelines und Abgleich
Die klassischen Ursachen langsamer PowerShell-Skripte im Überblick. Warum += bei einem Array O(n²) ist, der Unterschied zwischen Pipeline...
Schluss mit Write-Host — PowerShells Ausgabeströme und Log-Design
Wie Sie zwischen den sechs Ausgabeströmen von PowerShell wählen, welche Probleme Write-Host mit sich bringt und wo es tatsächlich hingehö...
Parallelverarbeitung in PowerShell — Die Wahl zwischen ForEach-Object -Parallel und Jobs
Ein praxisnaher Überblick über die Unterschiede zwischen ForEach-Object -Parallel, Start-ThreadJob und Start-Job und wann welches zum Ein...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Was sollte ich mit Pester zuerst testen?
- Beginnen Sie nicht mit dem Löschvorgang selbst, sondern mit der Logik, die die zu löschenden Ziele auswählt (die Zielauswahl). Bei Betriebsskripten zahlt es sich aus, diese vier Bereiche zu priorisieren: Bedingungsprüfung, Form der Ausgabe, den Schritt vor gefährlichen Operationen und externe Abhängigkeiten. Sie müssen ein bestehendes Skript nicht sofort grundlegend umbauen — beginnen Sie damit, die Entscheidungslogik vor der gefährlichen Operation in eine Funktion auszulagern und deren Rückgabewert mit Pester zu prüfen.
- Was ist $TestDrive in Pester?
- $TestDrive ist ein temporärer Bereich, den Pester für Tests bereitstellt. Statt das echte C:\Logs oder einen Freigabeordner zu verwenden, können Sie Dateioperationen mit Dateien überprüfen, die nur innerhalb des Tests angelegt werden. Bei Tests von PowerShell-Skripten mit Dateioperationen verhindert die Angewohnheit, zuerst $TestDrive zu verwenden, dass Sie versehentlich echte Dateien löschen.
- Wie weit sollte ich Pesters Mock einsetzen?
- Ersetzen Sie Vorgänge, die auf keinen Fall ausgeführt werden dürfen — Löschen/Verschieben, Web-API-Aufrufe, E-Mail-Versand und Benachrichtigungen — durch Mocks, legen Sie Datumswerte per Argument fest, und legen Sie für das Erstellen von Dateien oder das Lesen/Schreiben von CSV-Dateien besser kleine echte Dateien in $TestDrive an. Mocken Sie alles, entfernen Sie sich zu weit vom tatsächlichen Verhalten von PowerShell und übersehen dabei leicht Probleme mit Zeichenkodierung, Zeilenumbrüchen oder Spaltennamen. Entscheidend ist, „wo Sie das Echte verwenden“ und „wo Sie mocken“ zu trennen.
- Warum schlägt ein Pester-Test in der CI fehl, obwohl er lokal durchläuft?
- Eine häufige Ursache ist ein unterschiedliches Arbeitsverzeichnis; das lässt sich beheben, indem Sie das Laden des Testobjekts auf $PSScriptRoot beziehen. Ändern sich die Ergebnisse je nach Tag, liegt das meist daran, dass Get-Date direkt verwendet wird — legen Sie das Datum stattdessen über ein Argument wie -Now fest. Wirkt ein Mock nicht, vermuten Sie einen Unterschied bei Modulgrenzen oder Scope und prüfen Sie -ModuleName sowie die Art des Ladens. Was wie ein Pester-Problem aussieht, liegt in der Praxis oft an der Struktur des Skripts.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.