Pester: Mocking powershell 5 Classes

Created on 25 Jan 2017  路  20Comments  路  Source: pester/Pester

Now when PowerShell 5 finally supports native classes and enums, is there a way to mock their behavior with Pester without wrapping the call stack in functions.

Something like:

class Hello {
    [String]method($parameter) 
    {
        return "$parameter Hello"
    }
}

Mock [Hello] -member method {
    param([String]$parameter)
    return "$parameter Mocked Method"
}

$object = New-Object Hello
$object.method('parameter')
Classes Feature

Most helpful comment

I've been able to mock class methods using the following as an example:
$testObj = New-MockObject System.DirectoryServices.AccountManagement.PrincipalContext
$testobj = $testObj | Add-Member -MemberType ScriptMethod -Name ValidateCredentials -Value { $true } -Force -PassThru

$testObj.ValidateCredentials() | should be $true

I'm essentially overwriting the the ValidateCredentials method with my own implementation, In this case, always returning $true.

All 20 comments

Not at the moment, I will put this to v4.1 milestone. If you already have idea how to do it, implementation, or some articles that describe how to approach the problem and caveats please share :)

I second the value of this. I cannot use classes in my project code until they are properly testable. Thank you for putting this on the 4.1 milestone.

This would solve most of my issues with pester.

I like the implementation example above to start. Here is what the it block would look like:

```
class Hello {
[String]method($parameter)
{
return "$parameter Hello"
}
}

Mock [Hello] -member method {
param([String]$parameter)
return "$parameter Mocked Method"
}

it "Should be mocked" {
$object = New-Object Hello
$object.method('parameter') | should be "parameter Mocked Method"
}

That looks great, but I don't think there's any way for us to actually do it. When you call $object.method(), there's no opportunity for Pester to hook up a mock there. It's going to call the original implementation every time. Microsoft would need to provide some sort of hook for us to use.

I wonder if we can just define the class on the pester.tests file --

Code File:

class Hello {
    [String]method($parameter) 
    {
        return "$parameter Hello"
    }
}

Tests File:

class Hello {
    [String]method($parameter) 
    {
        return "$parameter Mocked Method"
    }
}

it "Should be mocked" {
     $object = New-Object Hello
     $object.method('parameter') | should be "parameter Mocked Method"
}

That should work? If so what if we just wrap that and make pester create a new class in the runtime? I hope I am making sense

btw, I do understand the problem you are talking about, I am just wondering if there is another way...

What would it take to discuss doing this with Microsoft?

@pixelrebirth You can talk to Powershell devs here directly and ask them what do they think about all this
PowerShell 6.0 Plan

@g8tguy I've found that I can get rather far testing classes without changes to Pester or PowerShell. The following code

  • tests construction of a class,
  • tests one of the class's methods,
  • tests instantiation of the class by a function, and
  • tests the method invokation of the resultant instance inside that function.
class Hello {
    [String]method($parameter) 
    {
        return "$parameter Hello"
    }
}

Describe 'class Hello' {
    Context 'contruction' {
        It 'does not throw' {
            [Hello]::new()
        }
    }
    Context 'method' {
        It 'returns correct value' {
            $r = [Hello]::new().method('argval')
            $r | Should be 'argval hello'
        }
    }
}

function UseHello { 
    param($arg)
    $o = New-Object Hello
    $o.method($arg)
}

Describe 'UseHello' {
    class mock : hello {
        static $methodInvokations = [System.Collections.ArrayList]::new()
        [string] method ($parameter) {
            [mock]::methodInvokations.Add($parameter)
            return 'hello method return value'
        }
    }
    Mock New-Object { [mock]::new() } -Verifiable
    It 'returns the value returned by .method()' {
        $r = UseHello 'UseHello arg'
        $r | Should be 'hello method return value'
    }
    It 'correctly creates the hello object' {
        Assert-MockCalled New-Object 1 {
            $TypeName -eq 'Hello'
        }
    }
    It 'correctly invokes .method()' {
        [mock]::methodInvokations.Count | Should be 1
        [mock]::methodInvokations[0] | Should be 'UseHello arg'
    }
}

I recently have shifted to only using New-Object and now my code is fully testable with classes. No need for function wrapping or anything. To me Id rather see the [Hello]::new() be eradicated anyway because it isnt the PowerShell way -- I am comfortable with using pester in a fully class based architecture now. Mocking works great within methods, I really dont have any limitations any longer.

Maybe we should get this formally documented? (or if it is, I just missed it)

@pixelrebirth I haven't found anything official around testing with PowerShell classes yet. For better or for worse, I worked those techniques on my own just to get by. Developing with PowerShell classes is still pretty dodgy because of the stale class bug in PowerShell 5.1, so I'm not surprised there isn't much writing around it yet.

@nohwnd Not sure where this stands but I wrote a proof of concept. It takes over the ScriptBlockMemberMethodWrapper that stores the AST used by methods created in PowerShell. It works for new and existing instances, static methods, and constructors. The main draw back being the amount of reflection required.

@SeeminglyScience, thank you.

Not sure where this change is. It looks like it's slated for 4.3. However, I'm using 4.3.1, and I'm running into the issue being reported.

I have a script with the function:

function f {
  Param($ComputerName, $ServiceName)
  $server = New-Object 'Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer' $ComputerName 

  $target_service = $server.Services | Where-Object { $_.name -eq $ServiceName }
  $target_service.SetServiceAccount('foo', 'bar')
}

I have a context block with the following mock:

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SqlWmiManagement") | out-null
New-MockObject -Type 'Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer'

When I run the test, I get the following:

MethodException: Cannot find an overload for "ManagedComputer" and the argument count: "2".

I'm running Pester 4.3.1, Powershell 5.1.15063.909, and Windows 10.

@SeeminglyScience I totally missed that :/ Have you since been using it? Did it prove useful? Any new known limitations?

Also I talked with SteveL and they are open to enabling Pester to mock better, but of course we need to work out the details, write a proposal and have it approved. Imho there will be a lot of concerns of security, because we need to pretty much hook pester before any keyword or type is resolved, but this might be overcome by a special Test mode that prints a big fat warning I think? Anyways I need to gather a lot of info to make proposal that would make sense. So it's on my todo list.

I've been able to mock class methods using the following as an example:
$testObj = New-MockObject System.DirectoryServices.AccountManagement.PrincipalContext
$testobj = $testObj | Add-Member -MemberType ScriptMethod -Name ValidateCredentials -Value { $true } -Force -PassThru

$testObj.ValidateCredentials() | should be $true

I'm essentially overwriting the the ValidateCredentials method with my own implementation, In this case, always returning $true.

I read this issue with much interest. Unfortunately it seems that the issue is stalled. Fair enough. What is the status of mocking classes?

I'm trying to do this:

Describe "Tests the xyz class" {
  Context "Class tests" {  
    It "Returns something" {
      . $PSScriptRoot/../../Classes/xyz/xyzClass.ps1
      $BasketClient = [xyzClass]::new($config, "falseserver1", "a_false_token_1234")
      Mock $Client.DoRequest("Test/Test", "", "Get", "api") {
        $BasketIdObject = New-Object -TypeName PSObject -Property @{
          MessageHeader = "A header"
        }
        $BasketIdObject.MessageHeader.BasketId = "4321"
      } -Verifiable
      $Client.DoRequest("Test/Test", "", "Get", "api")
    }
  }
}

Is the above possible?

Thank you very much

@nohwnd ... do you have any idea on this. Your help or others is highly regarded.

@it-praktyk .... do you have a response to my question. Thank you very much if you can spare the time.

@larssb The best way to mock classes currently is the same way it's typically done in C#, use inheritance.

I personally don't believe it's feasible to add support for mocking classes from Pester currently. It would require a pretty substantial block of API surface in PowerShell itself to achieve any real consistency. Even then it would not be likely to beat the consistency of just using inheritance.

Inheritance still isn't a perfect solution though. You have to design your code to be tested, which can be challenging especially for existing code. On top of that, some things you can do in C# aren't possible in PowerShell, things that make this type of testing a lot easier.

@nohwnd To make this type of "mocking" easier, it might be beneficial for the Pester team to push for some specific enhancements to be prioritized by the PowerShell team:

  1. Interface declaration
  2. Accessor declaration (with virtual as default, even for auto accessors). This one is big because there isn't currently a way to "mock" a PS class property with inheritance (assuming it wasn't part of an interface implementation)

Also I missed this way back when but:

@SeeminglyScience I totally missed that :/ Have you since been using it? Did it prove useful? Any new known limitations?

Nah, nah, and probably. The above is what I'd say if I was asked today about mocking classes. The PoC was neat, but far from useful or practical :/

Hmm thank you for responding @SeeminglyScience .... sadly class use in PowerShell is not really a first class citizen (pun intended). At least not yet. That's the takeaway from your input.

Well you're not wrong 馃槃 but I wouldn't say that because of a lack of mock support. It's significantly more difficult to implement than it appears on the surface. Partially because it's a largely parse time concept. Trying to dynamically alter something at parse time is next to impossible to do reliably, even with support from the engine. Even if Pester had hooks at parse time, it would still have to register those hooks dynamically.

Also there's a reason that most of the mocking tools for C# that I know of are typically just tools that dynamically make interface implementations. The common language runtime that C# and PowerShell run in just don't really have support for the concept of mocking.

That said, once you get used to designing your classes and code around them in a way that enables inheritance based mocking, it's really not that bad. Better support for that style of testing would go a long way for the ease of testing PS classes.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dantraMSFT picture dantraMSFT  路  22Comments

nohwnd picture nohwnd  路  129Comments

vors picture vors  路  43Comments

nohwnd picture nohwnd  路  22Comments

nohwnd picture nohwnd  路  22Comments