Private/Wissen/X_Technology/X02_Pester.ps1

<#
 
# Pester (UTest)
 
Unit-Test-Framework für die PowerShell
 
- **Hashtags** Pester UTest Context Should TestDrive Mocking
- **Version** 2019.12.12
 
#>


<#
 
? ## Testgetriebene Entwicklung
 
Testgetriebene Entwicklung (test first development, TDD) ist eine Methode, bei der der Programmierer Software-Tests konsequent vor den zu testenden Komponenten entwickelt.
 
? ### Die Vorzüge der testgetriebenen Entwicklung
 
* Eine boolesche Metrik d.h. die Tests werden bestanden oder nicht.
* Das Refactoring führt zu weniger Fehlern.
* Weil einfach und ohne großen Zeitaufwand getestet werden kann.
* Die Programmierer arbeiten die meiste Zeit an einem korrekten System.
* Alle Unit-Tests dokumentiert das System zugleich.
 
Für weitere Informationen lesen Sie den Artikel https://de.wikipedia.org/wiki/Testgetriebene_Entwicklung auf Wikipedia.
 
? ## Pester
 
Pester ist ein Test-Framework für Windows PowerShell. Mit der Pester-Sprache können Scripte, Cmdlets, Funktionen, CIM-Befehle, Workflows, DSC, Ressourcen und Module mittels Unit-Test testen.
 
Jeder Pester-Test vergleicht die tatsächliche mit der erwarteten Leistung unter Verwendung einer Sammlung von Vergleichsoperatoren.
 
? ### Was kann Pester
 
Pester wurde entwickelt, um die "testgetriebene Entwicklung" (TDD) zu unterstützen, bei der Sie
Schreiben und Ausführen von Tests, bevor Sie Ihren Code schreiben, wodurch der Test als ein
Codespezifikation.
 
* Pester unterstützt die "behavior-driven development" (BDD), bei der die Tests das Verhalten und die Ausgabe des Codes sowie die Benutzerfreundlichkeit überprüfen.
* Pester kann mit "Komponenten-Tests" einzelne Funktionen testen. Sowie Isolations- und "Integrationstests" die belegen, dass Funktionen genutzt werden können.
* Pester erstellt und verwaltet ein temporäres Laufwerk (TestDrive:), mit dem Sie ein Dateisystem simulieren können. (s. about_TestDrive).
* Pester hat auch "mocking"-Befehle, die die eigentliche Ausgabe von Befehle mit der von Ihnen angegebenen Ausgabe ersetzen. (s. about_Mocking, https://github.com/pester/Pester/wiki/Mocking-with-Pester)
 
Für weitere Informationen lesen Sie die Artikel about_Pester und https://github.com/pester/Pester/wiki/Pester.
 
#>


# TODO Weiterführende und Nachschlage-Informationen
Get-help -Name about_Pester         -ShowWindow
Get-Help -Name New-Fixture          -ShowWindow
Get-Help -Name Context              -ShowWindow # ? Bietet eine logische Gruppierung von It-Blöcken innerhalb eines einzelnen Describe-Blocks.
Get-Help -Name It                   -ShowWindow # ? Validiert die Ergebnisse eines Tests innerhalb eines Describe-Blocks.
Get-Help -Name about_Should         -ShowWindow # ? Bietet Assertion-Komfort-Methoden zum Vergleichen von Objekten und Werfen.
Get-Help -Name about_Mocking        -ShowWindow
Get-Help -Name about_TestDrive      -ShowWindow
Get-Help -Name Invoke-Pester        -ShowWindow
Get-Help -Name Set-TestInconclusive -ShowWindow

#region Pester vorab auf den aktuellsten Stand bringen

# ? Versionen der benötigten Module vergleichen
$ist  = Get-Module  -Name PSScriptAnalyzer, Pester -ListAvailable | Select-Object -Property Name, Version # ! IST
$soll = Find-Module -Name PSScriptAnalyzer, Pester                | Select-Object -Property Name, Version # ! SOLL
Compare-Object -ReferenceObject $ist -DifferenceObject $soll -IncludeEqual -Property Name, Version

# ! Mit Admin-Rechten in der PowerShell-Console ausführen

Remove-Module    -Name PSScriptAnalyzer, Pester -Force          # * Evtl. auftretende Fehler ignorieren
Uninstall-Module -Name PSScriptAnalyzer, Pester -Force -Verbose # * Evtl. auftretende Fehler ignorieren

$module = "$env:ProgramFiles\WindowsPowerShell\Modules\Pester"
TakeOwn /F $module /A /R ; icacls $module /reset ; icacls $module /grant "*S-1-5-32-544:F" /inheritance:d /T
Remove-Item -Path $module -Recurse -Force -Confirm:$false
Remove-Item -Path "$env:ProgramFiles\WindowsPowerShell\Modules\PSScriptAnalyzer" -Recurse -Force # * Evtl. auftretende Fehler ignorieren
Remove-Item -Path "$env:USERPROFILE\.vscode\extensions\ms-vscode.powershell-*\modules\PSScriptAnalyzer" -Recurse -Force # * Evtl. auftretende Fehler ignorieren

Install-Module -Name PSScriptAnalyzer, Pester -Confirm:$false -Force -Verbose -SkipPublisherCheck

#endregion

#region Eigene Cmdlets testgetrieben entwickeln

# ? Testgetriebene Entwicklung => test-driven development (TDD) s. https://de.wikipedia.org/wiki/Testgetriebene_Entwicklung

# ! 1. Arbeits-Verzeichnis setzen
Set-Location -Path C:\Temp

# ! 2. Grundgerüst Cmdlet und Cmdlet-Test erstellen
New-Fixture -Path . -Name Get-HalloOrt

# ! 3. Den Inhalt der Datei Get-HalloOrt.Tests.ps1 durch folgenden Code ersetzen (Start-Process -FilePath .\Get-HalloOrt.Tests.ps1 -Wait):
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
. "$here\$sut"
Describe "Get-HalloOrt" -Tag Grundtests {

    Context "Assertions" {
        It "Entspricht die Standard-Ausgabe 'Hallo Welt!'" {
            Get-HalloOrt | Should -Be 'Hallo Welt!'
        }
        It "Parameter -Ort köln => Entspricht die Ausgabe 'Hallo Köln!'" {
            Get-HalloOrt -Ort köln | Should -BeExactly 'Hallo Köln!'
        }
        It "Parameter -Ort vom Typ [String] vorhanden" {
            Get-Command -Name Get-HalloOrt | Should -HaveParameter -ParameterName Ort -Type String -DefaultValue 'Welt' -Mandatory:$false
        }
    }
    Context "Mocking" {
        It "Wird der Ort 'Brunsbrügelbreu' MIT den Parameter '-NeustartOrt' neu gestartet" {
            Mock Restart-Computer { return "Mocked" }
            Get-HalloOrt -Ort Brunsbrügelbreu -NeustartOrt | Should -BeExactly @('Hallo Brunsbrügelbreu, du wirst neu gestartet!', 'Mocked')
        }
        It "Wird der Ort 'Brunsbrügelbreu' OHNE den Parameter '-NeustartOrt' nicht neu gestartet" {
            Mock Restart-Computer { return "Mocked" }
            Get-HalloOrt -Ort Brunsbrügelbreu | Should -BeExactly 'Hallo Brunsbrügelbreu!'
        }
    }
    Context "TestDrive:\" {
        It "Wurde die Datei angelegt und ist der Inhalt korrekt" {
            Get-HalloOrt -Ort würzburg -Ordner TestDrive:\
            'TestDrive:\Würzburg.txt' | Should -Exist 
            'TestDrive:\Würzburg.txt' | Should -FileContentMatchExactly 'Hallo Würzburg!' 
        }
    }
    Context "Test Cases" {
        It "Stimmen die Rückgabewerte für -Ort <Ort>" -TestCases @{Ort='Würzburg'}, @{Ort='Köln'}, @{Ort='Düsseldorf'} {
            param([string]$Ort)
            Get-HalloOrt -Ort $Ort | Should -BeExactly "Hallo $Ort!"
        }
    }
}

# ! 4. Den Inhalt der Datei Get-HalloOrt.ps1 durch folgenden Code ersetzen (Start-Process -FilePath .\Get-HalloOrt.ps1 -Wait):
function Get-HalloOrt {
    param(
        [string]$Ort = 'Welt',
        [switch]$NeustartOrt,
        [string]$Ordner
    )
    
    $messageText = (Get-Culture).TextInfo.ToTitleCase($Ort)

    if($Ort -eq "Unbekannt") {
        $messageText = $messageText.ToUpper()
    }

    if($NeustartOrt -and $Ort -eq 'Brunsbrügelbreu') {
        $messageText = "$Ort, du wirst neu gestartet"
    }

    $messageText = "Hallo {0}!" -f  $messageText
    $messageText | Write-Output

    if(-not [String]::IsNullOrWhiteSpace($Ordner)) {
        New-Item -Path $Ordner\$Ort.txt -Value $messageText -ItemType File
    }

    if($NeustartOrt -and $Ort -eq 'Brunsbrügelbreu') {
        Restart-Computer -ComputerName localhost -Force -WhatIf
    }
}

#endregion

# ? Den Test Get-HalloOrt.Tests.ps1 ausführen: (Oder über VSCode)
Invoke-Pester -Script .\Get-HalloOrt.Tests.ps1 -TestName Get-HalloOrt -EnableExit -Tag Grundtests -PassThru
# * Oder in VSCode die Datei Get-HalloOrt.Tests.ps1 öffnen und den Test mit F5 starten

#region Die Behauptungen (Assertions)

Describe "Die Behauptungen" {
    $actual = 1 ; $expected = 1
    It "Die Behauptung: $actual -eq $expected"  {          
        $actual | Should -Be $expected
    }
    $actual = "Köln" ; $expected = "Köln"
    It "Die Behauptung: $actual -ceq $expected" {
        $actual | Should -BeExactly $expected
    }
    $actual = 2 ; $expected = 1
    It "Die Behauptung: $actual -gt $expected" {
        $actual | Should -BeGreaterThan $expected
    }
    $actual = 1 ; $expected = 1
    It "Die Behauptung: $actual -ge $expected " {
        $actual | Should -BeGreaterOrEqual $expected
    }
    $actual = 10 ; $expected = 11, 10, 13
    It "Die Behauptung: $actual -in $expected " {
        $actual | Should -BeIn $expected
    }
    $actual = 1 ; $expected = 2
    It "Die Behauptung: $actual -lt $expected " {
        $actual | Should -BeLessThan $expected
    }
    $actual = 0 ; $expected = 1
    It "Die Behauptung: $actual -le $expected " {
        $actual | Should -BeLessOrEqual $expected
    }
    $actual = "abc" ; $expected = "A*"
    It "Die Behauptung: $actual -like $expected" {
        $actual | Should -BeLike $expected
    }
    $actual = "Abc" ; $expected = "A*"
    It "Die Behauptung: $actual -cLike $expected " {
        $actual | Should -BeLikeExactly $expected
    }
    $actual = "abc" ; $expected = "^Abc$"
    It "Die Behauptung: $actual -match $expected" {
        $actual | Should -Match $expected
    }
    $actual = "Abc" ; $expected = "^Abc$"
    It "Die Behauptung: $actual -cMatch $expected" {
        $actual | Should -MatchExactly $expectedRegEx
    }
    $actual = 1
    It "Die Behauptung: $actual -is [int] " {
        $actual | Should -BeOfType [int]
    }
    $actual = $true ; $expected = $true
    It "Die Behauptung: $actual -eq $expected " {
        $actual | Should -BeTrue
    } 
    $actual = $false ; $expected = $false
    It "Die Behauptung: $actual -eq $expected " {
        [bool]$actual | Should -BeFalse
    }
    $actual = 1, 2, 3, 4 ; $expected = 4
    It "Die Behauptung: $actual.Count -eq $expected " {
        $actual | Should -HaveCount $expected
    }
    $actual = 1, 2, 3, 4 ; $expected = 2
    It "Die Behauptung: $actual -contains $expected " {
        $actual | Should -Contain $expected
    }
    $actual = "C:\Windows"
    It "Die Behauptung: Test-Path $actual -eq $true " {
        $actual | Should -Exist
    }
    $actual = "$($env:windir)\win.ini" ; $expected = "^; for 16-bit app support"
    It "Select-String -Path ""$actual"" -Pattern ""$expected"" -Quiet -CaseSensitive" {
        $actual | Should -FileContentMatch $expected
    }
    $actual = "$($env:windir)\win.ini" ; $expected = "^; for 16-bit app support"
    It "Select-String -Path ""$actual"" -Pattern ""$expected"" -Quiet" {
        $actual | Should -FileContentMatchExactly $expected
    }
    $actual = "$($env:windir)\*.log" ; $expected = "error"
    It "Get-ChildItem ""$actual"" | Select-String -Pattern ""error"" -Quiet" {
        $actual | Should -FileContentMatchMultiLine $expected
    }
    $actual = { 1 / 0 }
    It "Die Behauptung: try { $actual } catch { return $true }" {
        { . $actual }  | Should -Throw
    }
    $actual = ""
    It "Die Behauptung: [String]::IsNullOrEmpty( $actual ) -eq $true" {
        $actual | Should -BeNullOrEmpty
    } 
    It "Die Behauptung: Besitzt das Cmdlet Get-Process einen Parameter Id vom Typ Int32[]?" {
        Get-Command -Name Get-Process | Should -HaveParameter -ParameterName Id -Type Int32[]
    }
    $actual = $true
    It "Die Behauptung: $actual -eq $ true zzgl. Meldung miz Details wenn die Prüfung fehl schlägt." {
        $actual | Should -BeTrue -Because "Nicht erfolgreich, weil $actual nicht gleich $true ist"
    }
    $actual = 2 ; $expected = 1
    It "Die Behauptung: Alle B. lassen sich negieren" {
        $actual | Should -Not -Be $expected
    }
}

#endregion

# TODO Testlaufwerk TestDrive:\
# TODO wird während eines Pester-Test bereitgestellt und anschließend wieder gelöscht!

# TODO Code-Abdeckungs-Analyse (Code Coverage)
Invoke-Pester -Script .\Get-HalloOrt.Tests.ps1 -CodeCoverage .\Get-HalloOrt.ps1 -Show Summary
Invoke-Pester -Script .\Get-HalloOrt.Tests.ps1 -CodeCoverage .\Get-HalloOrt.ps1 -Show Summary -CodeCoverageOutputFile .\Get-HalloOrt.CodeCoverage.txt # Viewer => https://jacocoviewer.pacien.org/
# * oder so
$info = Invoke-Pester -Script .\Get-HalloOrt.Tests.ps1 -CodeCoverage .\Get-HalloOrt.ps1 -Show Summary -PassThru
$info.CodeCoverage.HitCommands