Mark
Mark

Reputation: 339

Invoke-Pester v5 doesn't run my TestCases

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

Answers (1)

Frode F.
Frode F.

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

Related Questions