End-to-end test of simple script
With Pester it is really easy to test a single PowerShell function or a library of them, but what if your script contains some code that runs those functions?
Let’s say we have a script like this one:
# file GetSomethingScript.ps1 |
This script defines a function and then calls it. This is pretty normal approach if you want your script to actually do something, instead of being just a library of functions.
But see what happens if we decide to test this script using Pester:
#file GetSomethingScript.Tests.ps1 |
And run the test code:
As you can see the Get-Something
function was run before running the tests as well. This is because we are dot-sourcing the tested script which means running it in the current scope (Using Dot Source Notation with Scope.). This is not good because the script should not run at this time.
So we need a way to dot source the script without running the Get-Something
function. To do that we check the value of $MyInvocation.InvocationName
as such:
#file GetSomethingScript.Tests.ps1 |
That condition makes sure the script imports properly when dot sourced, and that it runs properly when invoked by the user from PowerShell console or PowerShell ISE.
Now you might be tempted to check if that worked with tests that go like this:
#file GetSomethingScript.Tests.ps1 |
And on the first look it might seem to work correctly, but the second test is not proving anything. Dot sourcing the script inside the It
block redefines the Get-Something
function which makes it invisible for Mocking. So even if the function was called hundred times you’d still get green result. Checking for 0 calls with Mocking is always tricky.
Let’s prove that I am right by calling the Get-Something
function before Assert-MockCalled
which should make the It
fail. We also save the result, which should be empty as defined by the Mock, and check that as well.
#file GetSomethingScript.Tests.ps1 |
This puts us in a weird situation, we have code in our script that we are unable to test. Unfortunately there is no simple solution for this at the moment.
How about more complicated scripts?
Another problem you might face is that you want to check if the entry-point function of your script is run, but you don’t want to run the whole script to do that. The reason might simply be that the script takes too long to run.
Your knee-jerk reaction to this might be writing a test like this one:
#file GetSomethingScript.Tests.ps1 |
But unfortunately that won’t work because invoking the script with & starts it in another scope where, Mocking can’t see it.
To run it in the current scope you could use dot sourcing, but as we learned earlier dot sourcing the script inside the It block redefines the Get-Something function which makes it invisible for Mocking. Not to mention you now have to find a different mechanism to solve the first (dot sourcing the script to Pester) problem.
Unfortunately there is no elegant way around it at the moment so the low-tech and frankly horrible solution I use is regexing the last lines of the script:
#file GetSomethingScript.Tests.ps1 |
It gets job done, and actually works pretty well, but a better solution is definitely needed.
Different approach
There is also a different approach to the whole problem, that Dave Wyatt suggested when I talked with him about the dot sourcing problem few months ago: Provide a parameter that is used only by Pester.
I don’t like it much because I believe the script should not expose anything test framework specific. But I will use that approach to show what is needed to actually test all those cases properly.
First here is a basic check if the script is being dot sourced, we just add and use the $UnderTest parameter instead of $MyInvocation.InvocationName:
#file GetSomethingScript.ps1 |
This again gives you the power to stop the Get-Something from running when dot sourced, but still you can’t test that. And you can’t test if main entry point is run (when the script is invoked normally) either.
What would be actually needed is something like this:
#file GetSomethingScript.ps1 |
But of course we can’t require Pester users to put all this logic in their scripts. Another way to do this would be to AST the script, make a backup copy of it and inject those definitions in, but I am reluctant to edit the tested script in any way.
BTW: $MyInvocation.InvocationName
works flawelessly in ISE and Console, but it does not work in PowerGUI because it dot sources all scripts when running them, so as a result you won’t be able to run your scripts in PowerGUI.