3

I'm attempting to test this section of a PowerShell function:

# post
$Response = Invoke-WebRequest -Method POST -Uri $Uri -Body $Body -ContentType 'application/xml'

# parse Response.Content; return as System.Xml.XmlDocument
[xml]$Response.Content

by mocking the BasicHtmlWebResponseObject that is returned by Invoke-WebRequest:

Mock Invoke-WebRequest { 

    $WebResponse = [System.Net.HttpWebResponse]::new()
    [System.Net.HttpWebResponse].GetField('m_StatusCode', 'NonPublic, Instance').SetValue(
        $WebResponse,
        200,
        'NonPublic,SetField',
        $null,
        (Get-Culture)
    )

    $Content = '<?xml version="1.0" encoding="UTF-8"?><response><control>failure<status></status></control><operation><result><status>failure</status></result></operation></response>'
    $Response = [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]::new($WebResponse,$Content)
    return $Response
}

This assertion fails because I'm not creating the HttpWebResponse or BasicHtmlWebResponseObject correctly:

It "returns the response's Content object" {
    # act
    $Content = Send-Request -Session $Session

    # assert
    Assert-MockCalled Invoke-WebRequest
    $Content | Should -BeOfType [xml]
    $Content.response.control.status | Should -Be 'success'
    $Content.response.operation.result.status | Should -Be 'success'
}

** edit **

I thought about using New-MockObject:

Mock Invoke-WebRequest { 
    $Response = New‐MockObject -Type Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject
    $Response.Content = '<?xml version="1.0" encoding="...'
}

but, the Content property is read-only.

** /edit **

What am I missing?

craig
  • 25,664
  • 27
  • 119
  • 205

3 Answers3

2

A slightly simpler alternative might be to wrap your invoke-webrequest in a function and just mock that instead. E.g.

function Get-XmlFromUri
{
    param( $Uri, $Method, $Body )
     $Response = Invoke-WebRequest -Method $Method -Uri $Uri -Body $Body -ContentType 'application/xml’
    [xml]$Response.Content
}

Now you can mock Get-XmlFromUri and just return a System.Xml.XmlDocument object from hard-coded xml, which is much easier to create than a BasicHtmlWebResponseObject that needs reflection calls spin up.

Mock Get-XmlFromUri { 
    [xml] '<?xml version="1.0" encoding="UTF-8"?>
    <response>
        <control><status>success</status></control>
        <operation><result><status>success</status></result></operation>
    </response>'
}

Or, depending on how much like BasicHtmlWebResponseObject your code needs it to be, you can just return a hashtable from your invoke-webrequest mock that has the properties you need:

Mock Invoke-WebRequest { 
    new-object pscustomobject -property @{
        Content = '<?xml version="1.0" encoding="UTF-8"?>
    <response>
        <control><status>success</status></control>
        <operation><result><status>success</status></result></operation>
    </response>’
    }
}

(apologies for code formatting - currently typing one handed on an iPhone at 4 AM holding a not-very-sleepy baby :-S)

mclayton
  • 8,025
  • 2
  • 21
  • 26
  • Interesting idea--I can see the benefits. Delegate error handling to the calling function? – craig Feb 13 '20 at 13:33
  • @craig - re error handling, yeah let the caller do it. The nice thing is you can test the error handling as well by writing a mock that just throws an exception. The pattern works for other integration points as well - Invoke-SqlCmd, Get-Content, etc. – mclayton Feb 13 '20 at 22:25
  • I suppose you could still test this function's internals, with the technique that I wrote. Your approach adds a layer of indirection that makes subsequent tests easier. – craig Feb 13 '20 at 22:44
  • 1
    @craig - you could test the internals of Get-XmlFromUri using the second hashtable-based example of mocking Invoke-WebRequest. As long as the mock’s canned hashtable has the same property hierarchy as a BasicHtmlWebResponseObject then your implementation won’t know the difference (unless its calling methods on it) and you can avoid reflection, especially on private members who’s names (or existence) might change between versions... – mclayton Feb 13 '20 at 22:52
1

On PowerShell Core this doesn't work for me:

[System.Net.HttpWebResponse].GetField('m_StatusCode', 'NonPublic, Instance')

And thats why your Mock isn't returning what you expect. That line does work on Windows PowerShell however. Not sure what the right equivalent is on PSCore, needs research but thought i'd get you this far in the meantime.

Mark Wragg
  • 22,105
  • 7
  • 39
  • 68
  • `You cannot call a method on a null-valued expression.`? – craig Feb 12 '20 at 18:35
  • Yeah because this line doesn’t return an object, when you then try and use the setvalue method you’re executing it on nothing. Im not sure why it’s not working on PSCore, it will be something to do with .Net Core which PSCore is built on. – Mark Wragg Feb 12 '20 at 18:38
1

This works:

Mock Invoke-WebRequest { 
    $Response = New-MockObject -Type  Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject
    $Content = `
        '<?xml version="1.0" encoding="UTF-8"?>
        <response>
            <control><status>success</status></control>
            <operation><result><status>success</status></result></operation>
        </response>'
    $Response | Add-Member -NotePropertyName Content -NotePropertyValue $Content -Force
    $Response
}
craig
  • 25,664
  • 27
  • 119
  • 205