After some experimentation, I was able to come up with some satisfying answers. For the most part, the answer is yes, with a bit of trickery.
Test examples will follow, but here are the contents of StateChangingDemo.ps1 that I developed tests against:
[CmdletBinding(DefaultParameterSetName='SetA')]
param(
[Parameter(ParameterSetName='SetA')]
[object[]]$TypeA_Requests,
[Parameter(ParameterSetName='SetB')]
[switch]$TypeB_All
)
function ChangeSystemStateSomehow($changeRequestParameter)
{
# ... apply $changeRequestParameter to system state
"system state was modified"
}
$transformedRequests = $TypeA_Requests | % {
# do some processing of requests
$_
}
$transformedRequests | % {
ChangeSystemStateSomehow $_
}
Mocking a function internal to a .ps1
If the .ps1 calls a function that needs to be mocked (such as ChangeSystemStateSomehow
), a version of the function needs to exist and be mocked before calling the .ps1.
Describe 'test StateChangingDemo.ps1' {
Context 'no mock' {
It 'needs to be mocked' {
& .\StateChangingDemo.ps1 | Should -Be "system state was modified"
}
}
Context 'no mock, but placeholder function defined' {
function ChangeSystemStateSomehow {
"placeholder function"
}
# Show that a function in current scope won't displace the one in the script
It 'still needs to be mocked' {
& .\StateChangingDemo.ps1 | Should -Be "system state was modified"
}
}
Context 'function called by script is mocked' {
# This is required because Mock can't work before a function by the same name exists.
# However, the mock persists even though the function is later replaced.
function ChangeSystemStateSomehow {
"placeholder function"
}
Mock ChangeSystemStateSomehow {"fake execution"}
It 'can have functions within mocked' {
& .\StateChangingDemo.ps1 | Should -Be "fake execution"
}
}
}
Testing parameter parsing on the script
This aspect of testing the script benefits from two approaches.
Should -HaveParameter
The first approach is to recognize that Should -HaveParameter
can be used directly on the script:
Describe 'test StateChangingDemo.ps1' {
Context 'no mock again' {
It 'has expected parameters' {
Get-command .\StateChangingDemo.ps1 | Should -Not -BeNullOrEmpty
Get-command .\StateChangingDemo.ps1 | Should -HaveParameter TypeA_Requests -Type [object[]]
Get-command .\StateChangingDemo.ps1 | Should -HaveParameter TypeB_All -Type [switch]
}
}
}
The second approach is to mock the script itself. This is not as straightforward:
Context 'whole script mock attempted' {
Mock .\StateChangingDemo.ps1 {"fake script execution"}
It 'cannot be mocked directly' {
.\StateChangingDemo.ps1 | Should -Be "system state was modified"
}
}
The above passing test shows that even though we established a mock, the script still executed when invoked.
Mocking the Script itself
So instead we create a function whose implementation comes from treating the script as a script block:
Context 'whole script wrapped' {
Set-Item function:fnStateChangingDemo ([ScriptBlock]::Create((get-content -Raw .\StateChangingDemo.ps1)))
It 'has expected parameters' {
Get-command fnStateChangingDemo | Should -Not -BeNullOrEmpty
Get-command fnStateChangingDemo | Should -HaveParameter TypeA_Requests -Type "object[]" -Because 'it has TypeA_Requests'
Get-command fnStateChangingDemo | Should -HaveParameter TypeB_All -Type "switch" -Because 'it has TypeB_All'
}
Mock ChangeSystemStateSomehow {"fake execution"}
It 'can still have functions within mocked when it is wrapped with a function' {
& fnStateChangingDemo | Should -Be "fake execution"
}
}
The above test shows that the function presents the same interface as the script itself did, so we can test parameters on it in the same way. Moreover, we can now use the mock to safely make more complex assertions about parameter parsing:
Context 'whole script wrapped and mocked' {
Set-Item function:fnStateChangingDemo ([ScriptBlock]::Create((get-content -Raw .\StateChangingDemo.ps1)))
Mock fnStateChangingDemo {"fake script function execution"}
It 'can be mocked when wrapped as function' {
fnStateChangingDemo | Should -Be "fake script function execution"
}
It 'cannot accept conflicting parameters' {
# Using multiple parameter sets is not allowed
{fnStateChangingDemo -TypeA_Requests 1,2,3 -TypeB_All} | Should -Throw "Parameter set cannot be resolved"
}
}
Using more sophisticated mock bodies, Assert-MockCalled and Assert-VerifiableMocks and parameter filters, we can use the mocked function wrapper to design and test usages of the script without risking that the tests ask it to do something real on accident.