Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to test Invoke-Command with external cmdlets #791

Closed
b4tt1enet opened this issue Jun 19, 2017 · 3 comments
Closed

How to test Invoke-Command with external cmdlets #791

b4tt1enet opened this issue Jun 19, 2017 · 3 comments

Comments

@b4tt1enet
Copy link

b4tt1enet commented Jun 19, 2017

I'm facing some trouble trying to write unit tests for some code, and maybe you guys can help.

I need to remote into a different server and run the code, because that server has modules and cmdlets that I currently do not have (nor should I have). However, I have been unable to mock calls to those external cmdlets, because I get the Pester error: "Could not find Command 'External-Command'". This makes sense because my machine does not have the "External-Command" cmdlet.

I tried making a wrapper function for "External-Command", and mocking that, which makes my unit tests work fine, but it makes my Invoke-Command call fail because the server does not know about my wrapper function. Putting the wrapper function inside of the top-level function makes the Invoke-Command work, but breaks the unit tests (which is expected, as per #247). Does this make sense?

I made private functions just so I could test the logic without having to worry about Invoke-Command. But I think the behavior would be the same if I took out the private functions and put the code inside the ScriptBlock parameter.

Here's a small example.

Script file:

function My-Function
{
    Param( $Param )
    $ReturnValue = Invoke-Command -ComputerName 'MyServerName'
                                  -ArgumentList $Param
                                  -ScriptBlock ${ function:MyFunctionPrivate }
    return $ReturnValue
}

function MyFunctionPrivate
{
    Param( $Param )
    return External-Command $Param
}

Unit test:

Describe 'MyFunctionPrivate' {
    Context 'When run' {
        It 'Executes successfully and calls External-Command' {
            Mock -CommandName External-Command -MockWith { return 'Mocked!' }

            $ReturnValue = MyFunctionPrivate -Param 'Test'
            $ReturnValue | Should Be 'Mocked!'
        }
    }
}

And changing the script/test file to this makes the unit test pass, but breaks the code:
Script file:

function My-Function
{
    Param( $Param )
    $ReturnValue = Invoke-Command -ComputerName 'MyServerName'
                                  -ArgumentList $Param
                                  -ScriptBlock ${ function:MyFunctionPrivate }
    return $ReturnValue
}

function MyFunctionPrivate
{
    Param( $Param )
    return return Wrapper-Command $Param
}

function Wrapper-Command
{
    Param( $Param )
    return External-Command $Param
}

Unit test:

Describe 'MyFunctionPrivate' {
    Context 'When run' {
        It 'Executes successfully and calls External-Command' {
            Mock -CommandName Wrapper-Command -MockWith { return 'Mocked!' }

            $ReturnValue = MyFunctionPrivate -Param 'Test'
            $ReturnValue | Should Be 'Mocked!'
        }
    }
}

Is there a way I can make this work? It seems like a difficulty between managing Invoke-Command remoting nuances and Pester mocking nuances.

Thanks for any help!

@alx9r
Copy link
Member

alx9r commented Jun 20, 2017

I think I follow what you are trying to do. Please forgive me if I've missed something important about your goals; I'm guessing at some of them here. Here's how I would set this up:

function Wrapper-Command
{
    Param( $Param )
    
    $splat = @{
        ComputerName = 'MyServerName'
        ArgumentList = $Param
        Scriptblock = {
            param($Param)
            External-Command $Param
        }
    }
    return Invoke-Command @splat
}

function SomeFancyFunction
{
    Param($Param)
    
    # ... code of some complexity

    Wrapper-Command -Param $Param

    # ... maybe some more code of some complexity
}

Describe 'SomeFancyFunction' {
    Context 'basic usage' {
        Mock -CommandName Wrapper-Command { 'mocked return value' }
        It 'returns correct value' {
            $r = SomeFancyFunction -Param 'test parameter value'
            $r | Should be 'mocked return value'
        }
        It 'correctly invokes Wrapper-Command' {
            Assert-MockCalled Wrapper-Command 1 {
                $Param -eq 'test parameter value'
            }
        }
    }
}

That puts most of the complexity into SomeFancyFunction which you are able to thoroughly test. The only remaining untested stuff is the plumbing in Wrapper-Command. But that plumbing is not very complex and, once working, you can expect it will behave predictably. As you decide on more complex behavior, that behavior can be implemented inside SomeFancyFunction where its behavior (in particular the invocation of Wrapper-Command and its impact on the target computer) can be tested.

@b4tt1enet
Copy link
Author

Thank you @alx9r, that was very helpful! I realized that most of my "code of some complexity" was inside my Invoke-Command ScriptBlock. However, after your reply I realized that most of it can be extracted into a local function, and only the part that needs to be remoted can be inside of the Invoke-Command wrapper, like in the example you gave.

I guess the takeaway is that an external cmdlet that needs to be run on a remote computer cannot be mocked, or is very difficult to mock (still not sure how yet). If you do find a way to do that, let me know. But otherwise, I am satisfied with extracting most of the logic out like you proposed.

@alx9r
Copy link
Member

alx9r commented Jun 20, 2017

@b4tt1enet You're welcome.

You could inject a "command invoker" (see dependency inversion principle). In production you would inject a "command invoker" that uses the real Invoke-Command. For unit testing, you would inject a stub that emulates what the real invoker would do if the remote system were there.

I used dependency injection with Pester in this project. In that case the behavior of the dependency was simple enough to use a mock. In this case I think you'd probably need to unit test the behavior of the "command invoker" stub itself to make sure it meaningfully emulates the real "command invoker" that uses Invoke-Command. I'm not sure if that would be better for you. For the cases I've encountered it's definitely a lot more work than refactoring to put the complexity into the local function. I'd try to get away with the less-work approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants