PowerShell-Tests mit Pester ── Der praktische Ansatz, um Betriebsskripte robuster zu machen

· · 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-Item aufgerufen
  • Bei -WhatIf wird Remove-Item nicht 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
  • SupportsShouldProcess wurde 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.

  1. Der Unit-Test prüft „die eigene Entscheidung“. Ob Standardbefehle korrekt funktionieren, wird nicht geprüft (Kapitel 19).
  2. Gefährliche Operationen werden im Unit-Test nicht ausgeführt. Löschen, Verschieben, Senden und Benachrichtigen werden mit Mock ersetzt; 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.
  • -SkipPublisherCheck wird gesetzt. Das mitgelieferte Pester trägt eine Microsoft-Signatur; ohne diesen Parameter bricht die Installation ab, weil der Herausgeber abweicht.
  • shell: pwsh wird explizit angegeben. Bei GitHub-gehosteten Windows-Runnern ist pwsh der 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 $PSScriptRoot geladen
  • 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
  • Describe oder It erhalten Tags wie Unit, Smoke, RequiresAdmin
  • Die CI führt standardmäßig Unit aus
  • Ä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-ChildItem korrekt funktioniert
  • dass Remove-Item tatsä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 $TestDrive einschließen
  • Löschen, Verschieben, Benachrichtigen und API-Aufrufe mit Mock ersetzen
  • Ändernde Funktionen mit -WhatIf probelauffä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“.

Verwandte Artikel

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

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.

Zurück zum Blog