pester / Pester

Pester is the ubiquitous test and mock framework for PowerShell.
https://pester.dev/
Other
3.11k stars 473 forks source link

Piping to Should does result in ParameterBindingValidationException #1702

Closed DEberhardt closed 4 years ago

DEberhardt commented 4 years ago

1. General summary of the issue

Apologies for this might be a noob question or User Error, but I cannot square this with my other PowerShell knowledge.

My first tests are riddled with red text. One of which cost me a few hours today until I got it at least working (my examples were from 2018 so there were some issues with ParameterNames in in (-Exists VS -Exist) but all in all, they were solid.

I was given this block which I only adopted to suit my needs. $function is coming from a foreach loop and the array behind it is a collection generated with Get-ChildItem. This does not work:

It "should have a param block" {
    "$($function.FullName)" | Should -Exist
}

[-] TeamsFunctions Module Tests.X - Function.should exist 4ms (3ms|1ms)
 ParameterBindingValidationException: Cannot bind argument to parameter 'Path' because it is an empty string.

Lots of tinkering later. This passes:

It "should have a param block" {
    Should "$($function.FullName)" -Exist
}

I cannot understand why the pipeline would not work, I tried everything incl. Hardcoding the value to try to circumvent lookup errors. The variable is full, the NoteProperty FullName is populated, using Write-Host beforehand validates the thing is correct. Same thing. Is there an issue with Should and its pipeline input?

2. Describe Your Environment

Pester version     : 5.0.4 C:\Program Files\WindowsPowerShell\Modules\Pester\5.0.4\Pester.psd1
PowerShell version : 5.1.19041.1
OS version         : Microsoft Windows NT 10.0.19041.0

3. Expected Behavior

According to Using Assertions this should work.

I see lots of examples like

10 | Should -Be 10

Which I tried emulating and is working

4.Current Behavior

[-] TeamsFunctions Module Tests.X - Function.should exist 4ms (3ms|1ms)
 ParameterBindingValidationException: Cannot bind argument to parameter 'Path' because it is an empty string.

5. Possible Solution

I don't have any.

6. Context

I am at a loss. I don't even know whether I am making a mistake, whether there are limitations on what you can do with variables in an IT block or why other examples work, but mine just simply doesn't want to.

I am trying to get into proper unit testing and test my, now 72 Functions in my Module, but I am failing horribly at this seemingly easy task -.-

Please help a poor soul out? Cheers.

nohwnd commented 4 years ago

I see the same error when I run the test in PowerShell 5.1, and when the passed string is empty.

Describe "a" {
    It "should have a param block" {
        "$($function.FullName)" | Should -Exist
    }

    It "should have a param block" {
        "$($null)" | Should -Exist
    } 

    It "should have a param block" {
        $null | Should -Exist
    }
}
Starting discovery in 1 files.
Discovery finished in 310ms.
[-] a.should have a param block 254ms (145ms|109ms)
 ParameterBindingValidationException: Cannot bind argument to parameter 'Path' because it is an empty string.

[-] a.should have a param block 15ms (13ms|2ms)
 ParameterBindingValidationException: Cannot bind argument to parameter 'Path' because it is an empty string.

[-] a.should have a param block 18ms (16ms|2ms)
 ParameterBindingValidationException: Cannot bind argument to parameter 'Path' because it is null.

Tests completed in 1.36s
Tests Passed: 0, Failed: 3, Skipped: 0 NotRun: 0

What output do you get when you do:

It "should have a param block" {
        Write-Host "->$($function.FullName)<-"
    "$($function.FullName)" | Should -Exist
}

Also could you post a bit more code surrounding the test? I am interested in how you are using the foreach. Variables that are defined around It are not propagated inside of the It block in Pester v5.

DEberhardt commented 4 years ago

Thank you for looking into this - I thought I was going crazy^^ Here is more code:

$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$module = 'TeamsFunctions'

Describe -Tags ('Unit', 'Acceptance') "$module Module Tests"  {

  Context 'Module Setup' {
    It "has the root module $module.psm1" {
      "$here\$module.psm1" | Should -Exist
    }

  } # Context 'Module Setup'

  $functions = Get-ChildItem "$here\Public", "$here\Private" -Include "*.ps1" -ExClude "*.Tests.ps1" -Recurse #| Select-Object -First 1

  foreach ($function in $functions) {
    Context "$($function.BaseName) - Function" {

      It "should exist" {
        Should "$($function.FullName)" -Exist
      }

      It "should have help block" {
        Should "$($function.FullName)" -FileContentMatch "<#"
        Should "$($function.FullName)" -FileContentMatch "#>"
      }

      It "should have a SYNOPSIS section in the help block" {
        Should "$($function.FullName)" -FileContentMatch '.SYNOPSIS'
      }

#last one
      $psFile = Get-Content -Path $function.FullName -ErrorAction Stop
      $errors = $null
      $null = [System.Management.Automation.PSParser]::Tokenize($psFile, [ref]$errors)
      It "is valid PowerShell code" {
        $errors.Count | Should -Be 0
      }
  } # foreach ($function in $functions)
}

The last one is interesting, my example had the first 3 lines in the it block, which resulted in error, once I ran them outside the it block it worked. As if the variables are not available inside of it

On my work machine, I seem to have a different behaviour, here not even the defined Variables above seem to be making it inside the block:

[-] TeamsFunctions Module Tests.Module Setup.has the root module TeamsFunctions.psm1 6.91s (5.9s|1.01s)
 Expected path 'C:\Code\private\TeamsFunctions\.psm1' to exist, but it did not exist.
 at "$here\$module.psm1" | Should -Exist, C:\Code\private\TeamsFunctions\TeamsFunctions.Tests.ps1:8
 at <ScriptBlock>, C:\Code\private\TeamsFunctions\TeamsFunctions.Tests.ps1:8
[-] TeamsFunctions Module Tests.Module Setup.has the a manifest file of TeamsFunctions.psd1 93ms (55ms|38ms)
 Expected path 'C:\Code\private\TeamsFunctions\.psd1' to exist, but it did not exist.
 at "$here\$module.psd1" | Should -Exist, C:\Code\private\TeamsFunctions\TeamsFunctions.Tests.ps1:12
 at <ScriptBlock>, C:\Code\private\TeamsFunctions\TeamsFunctions.Tests.ps1:12
[-] TeamsFunctions Module Tests.Module Setup.TeamsFunctions is valid PowerShell code 34ms (30ms|4ms)
 ItemNotFoundException: Cannot find path 'C:\Code\private\TeamsFunctions\.psm1' because it does not exist.
 at <ScriptBlock>, C:\Code\private\TeamsFunctions\TeamsFunctions.Tests.ps1:22
-><-
Should : Legacy Should syntax (without dashes) is not supported in Pester 5. Please refer to migration guide at: https://pester.dev/docs/migrations/v3-to-v4
At C:\Code\private\TeamsFunctions\TeamsFunctions.Tests.ps1:44 char:9
+         Should "$($function.FullName)" -Exist
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Should

There shouldn't be much of a difference between my home PC (environment above) and this one:

Pester version     : 5.0.4 C:\Program Files\WindowsPowerShell\Modules\Pester\5.0.4\Pester.psd1
PowerShell version : 5.1.18362.752
OS version         : Microsoft Windows NT 10.0.18362.0

just a slightly older windows build, yet here not even my first tests pass

DEberhardt commented 4 years ago

Can reproduce an update with the Write-Host from my own machine later today, but just running it before the it (just in case) has displayed the whole string correctly

nohwnd commented 4 years ago

Can reproduce an update with the Write-Host from my own machine later today, but just running it before the it (just in case) has displayed the whole string correctly

Yeah that is the problem. It does not scope the same way as it did in Pester v4. In v5 all the the It, Before* and After* blocks are captured, but the context is not, so the code that runs inside of Describe or Context will not have effect on what happens in It because It will run much later. This causes the variable from foreach to not be defined inside of It, and your test failing. You can add -TestCases @{ Function = $function } to your It block to pass the value inside of the It, or you can upgrade to Pester 5.1.0-beta1 and use the new implementation of -TestCases to pass the $functions directly to it without using the foreach keyword at all, and then in the test you can refer to the current item by $_.

Here is your code how it would look like in Pester 5 (did not test it, so hopefully there are not too many typos).

BeforeAll {
    $module = 'TeamsFunctions'
}

Describe -Tags ('Unit', 'Acceptance') "<module> Module Tests" {
  Context 'Module Setup' {
    It "has the root module <module>.psm1" {
      "$PSScriptRoot\$module.psm1" | Should -Exist
    }
  } # Context 'Module Setup'

    $functions = Get-ChildItem "$PSScriptRoot\Public", "$PSScriptRoot\Private" -Include "*.ps1" -Exclude "*.Tests.ps1" -Recurse #| Select-Object -First 1
    Context "<function.BaseName> - Function" -ForEach $functions {
      BeforeAll { 
          # $_ is the current item coming from the -ForEach, which is a new feature 
          # in the 5.1-beta1, we rename it here to give it a better name
          $function = $_ 
      }

      It "should exist" {
        Should "$($function.FullName)" -Exist
      }

      It "should have help block" {
        Should "$($function.FullName)" -FileContentMatch "<#"
        Should "$($function.FullName)" -FileContentMatch "#>"
      }

      It "should have a SYNOPSIS section in the help block" {
        Should "$($function.FullName)" -FileContentMatch '.SYNOPSIS'
      }

      It "is valid PowerShell code" {
        $psFile = Get-Content -Path $function.FullName -ErrorAction Stop
        $errors = $null
        $null = [System.Management.Automation.PSParser]::Tokenize($psFile, [ref]$errors)
        $errors.Count | Should -Be 0
      }
  }
DEberhardt commented 4 years ago

Thank you! That is good to know. As someone relatively new to Pester, the nuances of changes are not easily recognised. I think I will go update to the prerelease version and pass it through with $PSItem / $_

DEberhardt commented 4 years ago

Good news everyone :) I have now over 700 successful tests and pipeline works as it should 👍

v5.1.0-prerelease worked like a charm - this is the sense of discovery I love when working with PowerShell!

Thank you for helping me understand Pester a bit more :D