pester / Pester

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

Mocking with Foreach-Object and Invoke-Restmethod #1078

Closed paraknell closed 6 years ago

paraknell commented 6 years ago

1. General summary

Mocking with Foreach-Object and Invoke-Restmethod doesn't return correct result

2. Environment

Operating System, Pester version and PowerShell version:

Pester version     : 4.3.1 3.4.0 C:\Program Files\WindowsPowerShell\Modules\Pester\4.3.1\Pester.psd1 C:\Program Files\WindowsPowerShell\Modules\Pester\3.4.0\Pester.psd1
PowerShell version : 5.1.17134.165
OS version         : Microsoft Windows NT 10.0.17134.0

3. Expected Behavior

$values = "500","200","404"
    $values | ForEach-Object{

        Context "Foreach-Object Restmethod returns $_ code" {
            Mock Invoke-RestMethod {
                "$_"
            }

            $result = Restart-Device

            It "returns $_" {
                $($result) | Should Be $($_)
            }
            It "should be a string" {
                $result.gettype() | Should beoftype System.Object
            }
            It "Should not be empty" {
                $result | Should not be ""
            }
            It "$_ should be a valid entry" {
                $_ | Should BeExactly $_
            }
            it 'should be mocked' {
                $assMParams = @{
                    CommandName = 'Invoke-Restmethod'
                    Times = 1
                    Exactly = $true
                }
                Assert-MockCalled @assMParams
            }
            It "should not throw an exception" {
                {$result }| Should not throw
            }
        }

    }

    Foreach($value in $values){

        Context "Foreach Restmethod returns $value code" {
            Mock Invoke-RestMethod {
                "$value"
            }

            $result = Restart-Device

            It "returns $value" {
                $result | Should Be $value
            }
            It "should be a string" {
                $result.gettype() | Should beoftype System.Object
            }
            It "Should not be empty" {
                $result | Should not be ""
            }
            It "$value should be a valid entry" {
                $value | Should BeExactly $value
            }
            it 'should be mocked' {
                $assMParams = @{
                    CommandName = 'Invoke-Restmethod'
                    Times = 1
                    Exactly = $true
                }
                Assert-MockCalled @assMParams
            }
            It "should not throw an exception" {
                {$result }| Should not throw
            }
        }

    }

4. Current Behavior

If I use the Foreach loop it will pass all tests. If I use the Foreach-Object it will fail on blocks: It "returns $_" It "Should not be empty"


  Describing Unit testing

    Context Foreach-Object Restmethod returns 500 code
      [-] returns 500 125ms
        Expected strings to be the same, but they were different.
        Expected length: 3
        Actual length:   0
        Strings differ at index 0.
        Expected: '500'
        But was:  ''
        -----------^
        43:                 $result | Should Be $_
        at Invoke-LegacyAssertion, C:\Program Files\WindowsPowerShell\Modules\Pester\4.3.1\Functions\Assertions\Should.ps1: line 188
        at <ScriptBlock>, c:\project.tests.ps1: line 43
      [+] should be a string 38ms
      [-] Should not be empty 24ms
        Expected <empty> to be different from the actual value, but got the same value.
        49:                 $result | Should not be ""
        at Invoke-LegacyAssertion, C:\Program Files\WindowsPowerShell\Modules\Pester\4.3.1\Functions\Assertions\Should.ps1: line 188
        at <ScriptBlock>, c:\project.tests.ps1: line 49
      [+] 500 should be a valid entry 24ms
      [+] should be mocked 18ms
      [+] should not throw an exception 15ms

    Context Foreach-Object Restmethod returns 200 code
      [-] returns 200 92ms
        Expected strings to be the same, but they were different.
        Expected length: 3
        Actual length:   0
        Strings differ at index 0.
        Expected: '200'
        But was:  ''
        -----------^
        43:                 $result | Should Be $_
        at Invoke-LegacyAssertion, C:\Program Files\WindowsPowerShell\Modules\Pester\4.3.1\Functions\Assertions\Should.ps1: line 188
        at <ScriptBlock>, c:\project.tests.ps1: line 43
      [+] should be a string 23ms
      [-] Should not be empty 16ms
        Expected <empty> to be different from the actual value, but got the same value.
        49:                 $result | Should not be ""
        at Invoke-LegacyAssertion, C:\Program Files\WindowsPowerShell\Modules\Pester\4.3.1\Functions\Assertions\Should.ps1: line 188
        at <ScriptBlock>, c:\project.tests.ps1: line 49
      [+] 200 should be a valid entry 22ms
      [+] should be mocked 11ms
      [+] should not throw an exception 14ms

    Context Foreach-Object Restmethod returns 404 code
      [-] returns 404 77ms
        Expected strings to be the same, but they were different.
        Expected length: 3
        Actual length:   0
        Strings differ at index 0.
        Expected: '404'
        But was:  ''
        -----------^
        43:                 $result | Should Be $_
        at Invoke-LegacyAssertion, C:\Program Files\WindowsPowerShell\Modules\Pester\4.3.1\Functions\Assertions\Should.ps1: line 188
        at <ScriptBlock>, c:\project.tests.ps1: line 43
      [+] should be a string 24ms
      [-] Should not be empty 20ms
        Expected <empty> to be different from the actual value, but got the same value.
        49:                 $result | Should not be ""
        at Invoke-LegacyAssertion, C:\Program Files\WindowsPowerShell\Modules\Pester\4.3.1\Functions\Assertions\Should.ps1: line 188
        at <ScriptBlock>, c:\project.tests.ps1: line 49
      [+] 404 should be a valid entry 20ms
      [+] should be mocked 12ms
      [+] should not throw an exception 11ms

    Context Foreach Restmethod returns 500 code
      [+] returns 500 86ms
      [+] should be a string 19ms
      [+] Should not be empty 18ms
      [+] 500 should be a valid entry 18ms
      [+] should be mocked 15ms
      [+] should not throw an exception 14ms

    Context Foreach Restmethod returns 200 code
      [+] returns 200 98ms
      [+] should be a string 19ms
      [+] Should not be empty 15ms
      [+] 200 should be a valid entry 12ms
      [+] should be mocked 16ms
      [+] should not throw an exception 11ms

    Context Foreach Restmethod returns 404 code
      [+] returns 404 84ms
      [+] should be a string 12ms
      [+] Should not be empty 14ms
      [+] 404 should be a valid entry 15ms
      [+] should be mocked 14ms
      [+] should not throw an exception 11ms

    Context host is back up
      [+] can ping the dev device after reboot 77ms
Tests completed in 1.43s
Tests Passed: 36, Failed: 6, Skipped: 0, Pending: 0, Inconclusive: 0

5. Possible Solution

Not at this time

6. Context

I would like to eventually use the Begin, process and End blocks and I'm for other pipeline output in future scripts, such as $_.filename | Should Match "c:\path\to\file" and others. I haven't specifically tested this example but I'm assuming something similar would cause an error as well.

mikeclayton commented 6 years ago

Hi @mwtilton,

I had to make a small change to reproduce your error: $result = Restart-Device becomes $result = Invoke-RestMethod -Uri "myuri", but once I did that I got the same result as you.

So I think the issue is that $_ refers to the "current object" in the "current pipeline" (see https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-6#-3) and it resolves to a different value inside the ForEach-Object and Mock because they're using running in different pipeline contexts.

You can test this by using write-host '$_' to write out the value of from both scopes - it'll write 200 (for example) from the ForEach-Object and an empty string (or maybe $null) from inside the Mock body.

What you can do, is "capture" the $_ from the ForEach-Object into a named variable and then refer to that in the Mock instead of $_. For example:

$values | ForEach-Object {

        $myItem = $_ # <-- capture the current pipeline item

        Context "Foreach-Object Restmethod returns $_ code" {

            Mock Invoke-RestMethod {
                $myItem # <-- return the captured value
            }
... etc ...

That made the tests start working on my local machine.

Hope this helps.

M

paraknell commented 6 years ago

oh yea I forgot to post my function

Function Restart-Device {
    Invoke-RestMethod -Uri "someuri" -ContentType application/x-www-form-urlencoded -Method POST -SessionVariable WebSession -ErrorAction stop
}

If I had done that you probably wouldn't have gotten a weird initial result.

Anyways this seems to have cleaned up my issue at least for my unit testing, I'll know better once I go through some more acceptance tests. But assuming that works I should still be able to use the $_ outside of the Context "Foreach-Object Restmethod returns $_ code" I'll update once I have this tested a little further.

Thanks for the help!

paraknell commented 6 years ago

This is working fine. Closing.