Pester
Pester copied to clipboard
Can't mock a function that takes pipeline input
Describe your environment
Pester 5.3.1 on PSv5 and v7 on Win 10
Steps to reproduce
function outer
{
1..3 | inner
}
function inner
{
param
(
[Parameter(ValueFromPipeline)]$InputObject
)
# implicit end block, runs once
"RETURN"
}
Describe "unmocked" {
It "runs the end block" {
(outer).Count | Should -Be 1
}
}
Describe "mocked" {
It "runs the end block" {
Mock inner {"FOO"}
(outer).Count | Should -Be 1
}
}
Expected Behavior
Both tests pass
Current Behavior
Second test fails (because mock returns 1 object for each piped input)
Possible Solution? (optional)
Steppable pipeline. For sample code, run [System.Management.Automation.ProxyCommand]::Create([System.Management.Automation.CommandMetadata]::new((Get-Command New-Item)))
This is a long-standing limitation of Mock. It is not impossible. Just complicated to do.
An alternative workaround may be to wrap a wrapper for testing.
function Wrapper{
param(
[Parameter(Mandatory = $true)]
[string]$str
)
return inner -str $str | outer
}
Describe "mocked" {
BeforeEach {
Mock Wrapper {}
}
It "testing" {
# ...
}
}
Nowind, please explain your above comment. Is there documentation on how to do this? I can't seem to find any.
This may be complicated, but it's def a requirement if you want to use Pester to test any existing powercli code.
My comment was supposed to mean: It is technically possible to implement this into Mock, but it is difficult.
I am not 100% sure about the use cases for this, and how hard or easy it would be to keep it backwards compatible.
I quickly looked at the implementation yesterday, and we can know if the function has begin, process or end by looking at ast:
function A {
begin
{
}
process
{
}
end
{
}
}
$a = Get-Command A
$a.ScriptBlock.Ast
$a.ScriptBlock.Ast.Body
function B {}
$b = Get-Command B
$b.ScriptBlock.Ast
$b.ScriptBlock.Ast.Body
Which outputs:
Attributes : {}
UsingStatements : {}
ParamBlock :
BeginBlock :
ProcessBlock :
EndBlock :
DynamicParamBlock :
ScriptRequirements :
Extent : {}
Parent : function B {}
Where BeginBlock, ProcessBlock and EndBlock are populated.
I have to look at how steppable pipeline works because I've read about it many years ago but never had the need to use it since.
It would be nice to have multiple examples of how people want to use this before implementation is started, because honestly the Mock code is very complicated.
Here's an example. I'm trying to mock RemoveDnsServerResourceRecord, which accepts objects returned by Get-DnsServerResourceRecord as input:
[Object[]] $records = Get-DnsServerResourceRecord -Name $curName @dnsArgs -ErrorAction Ignore
foreach ($record in $records)
{
if ($record.RecordType -ne 'CNAME' -or $record.RecordData.HostNameAlias -ne $HostNameAlias)
{
continue
}
Write-Information " - $($record.HostName) $($record.RecordType) $($record.RecordData.HostNameAlias)"
$record | Remove-DnsServerResourceRecord @dnsArgs -Force
}
Okay, been looking a bit more into the implementation of Mock, what we do now is that in begin we setup the mock, in process we run the MockWith scriptblock for every item that was received via pipeline, and in end we just check if we should invoke the original command, and if yes we invoke it.
From the examples above I am assuming that people want to do:
If there is just end block:
- autodetect if the mocked function has begin and process and use mockWith to replace just that?
- be able to assert (via Should -Invoke) on the number of calls of the end block?
If there is begin process and end:
- replace the end block of the mocked function and get all the input that was processed by the pipeline?
- replace the begin block as well?