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

Add Ability to Override Properties on New-MockObject #1838

Merged
merged 8 commits into from
Jun 27, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/csharp/Pester/Factory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public static PSNoteProperty CreateNoteProperty(string name, object value)
return new PSNoteProperty(name, value);
}

public static PSScriptMethod CreateScriptMethod(string name, ScriptBlock scriptBlock)
{
return new PSScriptMethod(name, scriptBlock);
}

public static Dictionary<string, object> CreateDictionary()
{
return new Dictionary<string, object>();
Expand Down
108 changes: 103 additions & 5 deletions src/functions/New-MockObject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,22 @@ Using the New-MockObject you can mock an object based on .NET type.
An .NET assembly for the particular type must be available in the system and loaded.

.PARAMETER Type
The .NET type to create an object based on.
The .NET type to create. This creates the object without calling any of its constructors or initializers. Use this to instantiate an object that does not have a public constructor. If your object has a constructor, or is giving you errors, try using the constructor and provide the object using the InputObject parameter to decorate it.

.PARAMETER InputObject
And already constucted object to decorate. Use New-Object or ::new to create it.

.PARAMETER Properties
Properties to define, specified as a hashtable, in format @{ PropertyName = value }.

.PARAMETER Methods
Methods to define, specified as a hashtable, in format @{ MethodName = scriptBlock }.

ScriptBlock can define param block, and it will recieve arguments that were provided to the function call based on order.

Method overloads are not supported because ScriptMethods are used to decorate the object, and ScriptMethods do not support method overloads.

For each method a property named _MethodName is defined which holds history of the invocations of the method and the arguments that were provided.

.EXAMPLE
```powershell
Expand All @@ -18,6 +33,27 @@ $obj.GetType().FullName
System.Diagnostics.Process
```

.EXAMPLE
```powershell
$obj = New-MockObject -Type 'System.Diagnostics.Process' -Properties @{ Id = 123 }
```

.EXAMPLE
```powershell
$obj = New-MockObject -Type 'System.Diagnostics.Process' -Methods @{ Kill = { param($entireProcessTree) "killed" } }
$obj.Kill()
$obj.Kill($true)
$obj.Kill($false)

$obj._Kill

Call Arguments
---- ---------
1 {}
2 {True}
3 {False}
```

.LINK
https://pester.dev/docs/commands/New-MockObject

Expand All @@ -27,11 +63,73 @@ https://pester.dev/docs/usage/mocking
#>

param (
[Parameter(Mandatory = $true)]
[Parameter(ParameterSetName = "Type", Mandatory)]
[ValidateNotNullOrEmpty()]
[type]$Type,
[Parameter(ParameterSetName = "InputObject", Mandatory)]
[ValidateNotNullOrEmpty()]
[type]$Type
$InputObject,
[Parameter(ParameterSetName = "Type")]
[Parameter(ParameterSetName = "InputObject")]
[hashtable]$Properties,
[Parameter(ParameterSetName = "Type")]
[Parameter(ParameterSetName = "InputObject")]
[hashtable]$Methods,
[string] $MethodHistoryPrefix = "_"
)

[System.Runtime.Serialization.Formatterservices]::GetUninitializedObject($Type)
$mock = if ($PSBoundParameters.ContainsKey("InputObject")) {
$PSBoundParameters.InputObject
}
else {
[System.Runtime.Serialization.Formatterservices]::GetUninitializedObject($Type)
}

if ($null -ne $Properties) {
foreach ($property in $Properties.GetEnumerator()) {
if ($mock.PSObject.Properties.Item($property.Key)) {
$mock.PSObject.Properties.Remove($property.Key)
}
$mock.PSObject.Properties.Add([Pester.Factory]::CreateNoteProperty($property.Key, $property.Value))
}
}

if ($null -ne $Methods) {
foreach ($method in $Methods.GetEnumerator()) {
$historyName = "$($MethodHistoryPrefix)$($method.Key)"
if ($mock.PSObject.Properties.Item($historyName)) {
$mock.PSObject.Properties.Remove($historyName)
}
$mock.PSObject.Properties.Add([Pester.Factory]::CreateNoteProperty($historyName, [System.Collections.Generic.List[object]]@()))

$saveHistoryAndInvokeUserScriptBlock = & {
# this new scriptblock ensures we only copy the variables here
# because closure only copies local variables, the scriptblock execution
# returns a scriptblock that is a closure

# save the provided scriptblock as $scriptblock in the closure
$scriptBlock = $method.Value
# save history name as $historyName in the closure
$historyName = $historyName
# save count as reference object so we can easily update the value
$count = @{ Count = 0 }

{
# before invoking user scriptblock up the counter by 1 and save args
$this.$historyName.Add([PSCustomObject] @{ Call = ++$count.Count; Arguments = $args })

# then splat the args, if user specifies parameters in the scriptblock they
# will get the values by order, same as if they called the script method
& $scriptBlock @args
nohwnd marked this conversation as resolved.
Show resolved Hide resolved
}.GetNewClosure()
}
if ($mock.PSObject.Methods.Item($method.Key)) {
$mock.PSObject.Methods.Remove($method.Key)
}

$mock.PSObject.Methods.Add([Pester.Factory]::CreateScriptMethod($method.Key, $saveHistoryAndInvokeUserScriptBlock))
}
}

}
$mock
}
59 changes: 56 additions & 3 deletions tst/functions/New-MockObject.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,9 +1,62 @@
Set-StrictMode -Version Latest

describe 'New-MockObject' {
Describe 'New-MockObject' {

it 'instantiates an object from a class with no public constructors' {
It 'instantiates an object from a class with no public constructors' {
$type = 'Microsoft.PowerShell.Commands.Language'
New-MockObject -Type $type | should -beoftype $type
New-MockObject -Type $type | Should -BeOfType $type
}

It 'Add a property to existing object' {
$o = New-Object -TypeName 'System.Diagnostics.Process'
$mockObject = New-MockObject -InputObject $o -Properties @{ Id = 123 }

$mockObject | Should -Be $o
$mockObject.Id | Should -Be 123
}

Context 'Methods' {
It "Adds a method to the object" {
$o = New-Object -TypeName 'System.Diagnostics.Process'
$mockObject = New-MockObject -InputObject $o -Methods @{ Kill = { param() "killed" } }

$mockObject | Should -Be $o
$mockObject.Kill() | Should -Be "killed"
}

It "Counts history of the invocation" {
$o = New-Object -TypeName 'System.Diagnostics.Process'
$mockObject = New-MockObject -InputObject $o -Methods @{ Kill = { param($entireProcessTree) "killed" } }

$mockObject | Should -Be $o
$mockObject.Kill() | Should -Be "killed"
$mockObject._Kill[-1].Call | Should -Be 1
$mockObject._Kill[-1].Arguments | Should -Be $null
$mockObject.Kill($true) | Should -Be "killed"
$mockObject._Kill[-1].Call | Should -Be 2
$mockObject._Kill[-1].Arguments | Should -Be $true
}
}


Context 'Modifying readonly-properties' {
# System.Diagnostics.Process.Id is normally read only, using the
# Properties switch in New-MockObject should allow us to mock these.
it 'Fails with just a normal mock' {
$mockedProcess = New-MockObject -Type 'System.Diagnostics.Process'

{ $mockedProcess.Id = 123 } | Should -Throw
}

it 'Works when you mock the property' {
$mockedProcess = New-MockObject -Type 'System.Diagnostics.Process' -Properties @{ Id = 123 }

$mockedProcess.Id | Should -Be 123
}

it 'Should preserve types' {
$mockedProcess = New-MockObject -Type 'System.Diagnostics.Process' -Properties @{Id = 123 }
$mockedProcess.Id | Should -BeOfType [int]
}
}
}