Search code examples
powershellpester

Pester 5: Automate It


For my hobby project ConvertTo-Expression, I am rebuilding my test (Pester 5) script. I would like to automate the It (and possibly the Context part) as there are large number of syntax formats to test for and the function actually roundtrips which &([ScriptBlock]::Create("$Expression")). For a Minimal, Reproducible Example, I am using ConvertTo-Json which roundtrips with ConvertTo-Json.
My goal for this question is basically to create an easy test syntax whether the concerned function correctly roundtrips, e.g.:

Test -Compress '{"a":1}'

I would like to do something like this:

Function Test ([String]$Json, [Switch]$Compress, [String]$It = $Json) {
    $Context = if ($Compress) { 'Compress' } Else { 'Default' }
    $Object = ConvertFrom-Json $Json
    Context $Context {
#       BeforeEach {
#           $Compress = $True
#           $Json = '{"a":1}'
#           $Object = ConvertFrom-Json $Json
#       }
        It $It { ConvertTo-Json -Compress:$Compress -Depth 9 $Object | Should -Be $Json }
    }
}

Describe 'Syntax check' {

    Test -Compress '{"a":1}'

}

but this results in errors like:

Starting discovery in 1 files.
Discovery finished in 35ms.
[-] Syntax check.Compress.{"a":1} 15ms (13ms|2ms)
 RuntimeException: The variable '$Compress' cannot be retrieved because it has not been set.
 at <ScriptBlock>, C:\Test.ps1:6
Tests completed in 140ms
Tests Passed: 0, Failed: 1, Skipped: 0 NotRun: 0

Enabling the commented-out hardcoded BeforeEach returns the expected results:

Starting discovery in 1 files.
Discovery finished in 32ms.
[+] C:\Test.ps1 197ms (80ms|88ms)
Tests completed in 202ms
Tests Passed: 1, Failed: 0, Skipped: 0 NotRun: 0

I would like to put It (and Context) in a function and control them with arguments so that I can do a simple test like Test -Compress '{"a":1}'. Unfortunately, I got completely lost in the new Pester discovery scopes and start wondering whether this is actually possible at all with Pester 5.


Solution

  • I think the problem is that the variables of interest are only referenced inside the nested script block (the one passed to It), not the outer one (the one passed to Context).

    A simple solution is to call the .GetNewClosure() method on the outer script block, which forms a closure around the calling scope's local variables:

    Function Test ([String]$Json, [Switch]$Compress, [String]$It = $Json) {
        $Context = if ($Compress) { 'Compress' } Else { 'Default' }
        $Object = ConvertFrom-Json $Json
        Context $Context {
            It $It { ConvertTo-Json -Compress:$Compress -Depth 9 $Object | Should -Be $Json }
        }.GetNewClosure() # This captures $Json, $Compress, $It, and $Object
    }
    
    Describe 'Syntax check' {
        Test -Compress '{"a":1}'
    }
    

    Note that the docs are very terse at the moment, but the conversation in GitHub issue #9077 suggests that the script block returned by .GetNewClosure() runs in a newly created dynamic (in-memory) module.