Pester: How to unit test your PowerShell modules

I’ve written about Pester before on how & why you should really start testing your PowerShell code.

However, obstacles may arise when you want to write unit tests for PowerShell modules. In this post I’m going to present a solution that enables you to overcome some of these obstacles.
Let’s get started.

Problem

image

Let’s take the code above as an example.
It’s a PowerShell module that exposes just one function, Invoke-NUnit, but it relies heavily on two other functions; Get-NUnitPackage and Invoke-Executable.

Let’s now say that we want to write a test for the Invoke-NUnit function. One of them could look like this:

image

We need to mock the calls to those “helper” functions.
Firstly because we don’t actually want to execute any executable and secondly because the Get-NUnitPackage function currently throws an exception (it’s not finished yet).
Lastly, mocking allows us to write expectations that test whether the code actually does what we expect it to do (like using the labels switch as above).
That is a unit test, tested in isolation.

Now, we have some problems though, the module only exposes the “public” function.

  • We could expose the other functions as well, maybe with a conditional export in ‘test-only’ context.
    The real problem will however still reside with modules and the scope rules that surround them.
  • We could break out the separate functions into their own .ps1 files and “dot source” them into the module. This would probably work…but.
    What if I don’t want to do that…we just want that one file!

All I really want to do is dot source the .psm1 file just as I would have done with any other .ps1 file. The problem is that dot sourcing doesn’t work with any other file types than .ps1

Solution

At the top of the test file we’ll put this snippet of code:

$module = (Split-Path -Leaf $PSCommandPath).Replace(".Tests.ps1", ".psm1")
$code = Get-Content $module | Out-String
Invoke-Expression $code

If the test file follows Pester naming conventions and therefore is named Invoke-NUnit.Tests.ps1 we can simply replace part of the name to get to the module/psm1 file (line 1 shows that).

image

Secondly we’ll get the contents of the .psm1 file (Get-Content) and put it into a string (line 2).
Lastly, we’ll use Invoke-Expression to just run that code just as if we would have dot sourced the file (line 3).

And now it works just as if we would have used a .ps1 file although we didn’t…

Downsides?

Yes, there’s one at least and that is you can have trouble debugging since you’re no longer using the “real” module but just the contents of it so to say. I haven’t found a way to debug code that has been put into a script using Invoke-Expression at least.

However, if you use TDD practices you normally won’t need to do much debugging and setting breakpoints…my experience at least. Just trust the expectations.

note: the code used in this post can be found right here.

Advertisements

,

  1. #1 by ErPe on August 13, 2015 - 13:58

    Hey ,

    Recently I blogged around possible solutions to such complex situations. My approach would be following. If you import your module you could then iterate all available functions by using ‘get-command -module ABC’. Then on that basis we can export every function to string representation. What that gives you ? Well you can call [scriptblock]::Create($someFunc) and you have your function recreated (look here for example http://rafpe.ninja/2015/08/10/powershell-local-functions-in-remote-sessions-with-scriptblock/ )

    Now that leaves us in theory with problem of depending on functions … Well I tried tackling this by building dependency tree abusing 🙂 the comment based help (you may find it here http://rafpe.ninja/2015/08/12/powershell-build-complex-module-with-functions-dependency-tree/ )

    Now since I have a whole module full of those I need to somehow get testign working 😀

    Hope this would help! Otherwise let me know how you overcome this!

    Regards
    Raf

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: