Reputation: 339
I am learning Pester and am trying to create a default template for my PowerShell modules.
I created this Pester ps1 file:
BeforeAll {
[System.IO.DirectoryInfo]$ModuleRoot = (Get-Item -Path $PSScriptRoot).Parent
[System.String]$ModulePath = $ModuleRoot.FullName
[System.String]$ModuleName = $ModuleRoot.Name
[System.String[]]$EssentialPublicFunctions = @(
'Get-Something'
'Set-Something'
)
$TestCases = @()
$EssentialPublicFunctions.ForEach({ $TestCases += @{ 'Function' = $PSItem } })
Function IsValidCode ([System.String]$Path) {
[System.String[]]$PSFile = (Get-Content -Path $Path -ErrorAction 'Stop')
New-Variable -Name 'Errors' -Value $Null
$Null = [System.Management.Automation.PSParser]::Tokenize($PSFile, [ref]$Errors)
Write-Output -InputObject $Errors.Count
}
}
Describe '<ModuleName> Tests' {
Context 'General Tests' {
It 'has a root module named "<ModuleName>.psm1"' {
"$ModulePath\$ModuleName.psm1" | Should -Exist
}
It 'has a manifest file named "<ModuleName>.psd1"' {
"$ModulePath\$ModuleName.psd1" | Should -Exist
}
It 'manifest references root module' {
"$ModulePath\$ModuleName.psd1" | Should -FileContentMatchExactly "$ModuleName.psm1"
}
It 'module has public functions' {
"$ModulePath\Public\*.ps1" | Should -Exist
}
It 'root module is valid PowerShell code' {
IsValidCode "$ModulePath\$ModuleName.psm1" | Should -Be 0
}
}
Context 'Specific Tests' {
It 'Get-Something.ps1 is present and public' {
"$ModulePath\Public\Get-Something.ps1" | Should -Exist
}
It 'Set-Something.ps1 is present and public' {
"$ModulePath\Public\Set-Something.ps1" | Should -Exist
}
}
Context 'Testing Loops' {
It '<Function>.ps1 is present' -TestCases $TestCases {
"$ModulePath\Public\$Function.ps1" | Should -Exist
}
}
}
The Testing Loops
was created since I couldn't stand copy pasting for every function.
However, it doesn't behave as expected.
When using VSCode (with the Pester Tests extension) I get this output:
Describing SomethingModule Tests
Context General Tests
[+] has a root module named "SomethingModule.psm1" 33ms (5ms|28ms)
[+] has a manifest file named "SomethingModule.psd1" 42ms (5ms|37ms)
[+] manifest references root module 64ms (40ms|24ms)
[+] module has public functions 79ms (54ms|25ms)
[+] root module is valid PowerShell code 198ms (75ms|123ms)
Context Specific Tests
[+] Get-Something.ps1 is present and public 46ms (27ms|19ms)
[+] Set-Something.ps1 is present and public 37ms (34ms|3ms)
Context Testing Loops
[+] Get-Something.ps1 is present 83ms (11ms|71ms)
[+] Set-Something.ps1 is present 40ms (35ms|5ms)
Tests completed in 2.36s
Tests Passed: 9, Failed: 0, Skipped: 0 NotRun: 0
I'm happy, it works as I hoped.
But when using Invoke-Pester from a standard PowerShell console/terminal, this is my output:
Describing SomethingModule Tests
Context General Tests
[+] has a root module named "SomethingModule.psm1" 11ms (4ms|8ms)
[+] has a manifest file named "SomethingModule.psd1" 10ms (8ms|2ms)
[+] manifest references root module 6ms (4ms|2ms)
[+] module has public functions 7ms (5ms|2ms)
[+] root module is valid PowerShell code 8ms (6ms|2ms)
Context Specific Tests
[+] Get-Something.ps1 is present and public 9ms (4ms|5ms)
[+] Set-Something.ps1 is present and public 5ms (3ms|2ms)
Tests completed in 343ms
Tests Passed: 7, Failed: 0, Skipped: 0 NotRun: 0
No errors, no info in Diagnostic as to why it skips the loop.
Can somebody tell me why?
Of course, since I started learning Pester less than a day ago (Pester 5 does not make my life easier compared to 4), any tips on how to improve the code or best practices are welcome. I tried to find a balance between readability and my normal strict way of working (define every type, always use the format operator on strings, never omit the parameter names and so on).
I did alter the code a little to remove the actual function names, but it should work just fine. In case it is not evident, I placed the .Tests.ps1
file in a Tests
-subfolder of the module, hence the .Parent
on line 2 and the Public
-subfolder in the paths. If it is important I can share the folder structure.
Hmm... looks like I have to place the necessary variables within the Describe Block, either at the top or right above the loop will do.
Like this:
Describe '<ModuleName> Tests' {
[System.String[]]$EssentialPublicFunctions = @(
'Get-Something'
'Set-Something'
)
$TestCases = @()
$EssentialPublicFunctions.ForEach({ $TestCases += @{ 'Function' = $PSItem } })
etc.
or this
Context 'Testing Loops' {
[System.String[]]$EssentialPublicFunctions = @(
'Get-Something'
'Set-Something'
)
$TestCases = @()
$EssentialPublicFunctions.ForEach({ $TestCases += @{ 'Function' = $PSItem } })
It '<Function>.ps1 is present' -TestCases $TestCases {
"$ModulePath\Public\$Function.ps1" | Should -Exist
}
}
}
So to revise my question then: is there a supported way to place the variables at the top? I really don't like having hardcoded values anywhere else in a script.
Although I might eventually create the collection with a wildcard, right now I'm playing with the idea of having a few mandatory functions to test but not necessarily all of the functions in the module.
Upvotes: 0
Views: 284
Reputation: 54911
You're looking for BeforeDiscovery. $TestCases
has to exist during the Discovery-phase of a Pester v5 run, but BeforeAll
is executed later in the Run-phase, see https://pester.dev/docs/usage/discovery-and-run#beforeall-and--testcases.
You can have both a top-level BeforeAll
for runtime-variables, functions etc and BeforeDiscovery
for testcase-related code. Try:
BeforeDiscovery {
[System.String[]]$EssentialPublicFunctions = @(
'Get-Something'
'Set-Something'
)
$TestCases = @()
$EssentialPublicFunctions.ForEach({ $TestCases += @{ 'Function' = $PSItem } })
}
BeforeAll {
[System.IO.DirectoryInfo]$ModuleRoot = (Get-Item -Path $PSScriptRoot).Parent
[System.String]$ModulePath = $ModuleRoot.FullName
[System.String]$ModuleName = $ModuleRoot.Name
Function IsValidCode ([System.String]$Path) {
[System.String[]]$PSFile = (Get-Content -Path $Path -ErrorAction 'Stop')
New-Variable -Name 'Errors' -Value $Null
$Null = [System.Management.Automation.PSParser]::Tokenize($PSFile, [ref]$Errors)
Write-Output -InputObject $Errors.Count
}
}
Describe '<ModuleName> Tests' {
Context 'General Tests' {
It 'has a root module named "<ModuleName>.psm1"' {
"$ModulePath\$ModuleName.psm1" | Should -Exist
}
It 'has a manifest file named "<ModuleName>.psd1"' {
"$ModulePath\$ModuleName.psd1" | Should -Exist
}
It 'manifest references root module' {
"$ModulePath\$ModuleName.psd1" | Should -FileContentMatchExactly "$ModuleName.psm1"
}
It 'module has public functions' {
"$ModulePath\Public\*.ps1" | Should -Exist
}
It 'root module is valid PowerShell code' {
IsValidCode "$ModulePath\$ModuleName.psm1" | Should -Be 0
}
}
Context 'Specific Tests' {
It 'Get-Something.ps1 is present and public' {
"$ModulePath\Public\Get-Something.ps1" | Should -Exist
}
It 'Set-Something.ps1 is present and public' {
"$ModulePath\Public\Set-Something.ps1" | Should -Exist
}
}
Context 'Testing Loops' {
It '<Function>.ps1 is present' -TestCases $TestCases {
"$ModulePath\Public\$Function.ps1" | Should -Exist
}
}
}
As for why it worked in VSCode, that's usually because a variable like $TestCases
has been assigned in the session, ex. because you ran code while it was set outside BeforeAll/It/BeforeDiscovery
. I bet it would fail too if you restarted the session.
Upvotes: 0