Friday, 30 October 2015

PowerShell Unit Testing with Pester 3.3

I have been working with the Pester unit testing framework at the moment, so I thought I would share some of the more interesting bits...

PowerShell returns everything from a function!

I think this is a trap every PowerShell programmer has fallen into at some point - probably at the beginning. The return statement will return everything which is not "captured" in a function. In my case the answer was to wrap some xml manipulation code in a function which casts to void so that the return statement would actually return the custom PSObject I was expecting instead of an XmlElement which was being created at some point in my code.


   [void](do-XmlManipulation $myParamater)

This was the explanation which really hit home

It helped me realise the awful truth!

Mocking

Mocking in Pester is fantastically simple, there is an object called Mock!

You simply type Mock followed by the function that you wish to mock and then return.

Mock get-MyObject {
   return { [PSCustomObject]@{ Project = "New and Exciting Project" } }
}


In this case I have returned a Powershell object with the property "Project" which is a string. For example, the code in the module may look like this:
## myRealCodeModule.psm1

$document = get-MyObject

function get-MyObject {  
   $xmlDocument = Get-Content -Path "C:\temp.xml"
      return $xmlDocument
   }
}

But the unit test will simply bypass the attempt to get the xml file and will return our object, as defined in our unit test, instead.

Unit testing powershell modules

I keep all my most important logic in PowerShell modules. Sure, I will unit test the main PowerShell script but probably this will be mostly mocking, the real meat should be found in nicely named and separated modules.

Example of testing mocked functions in the main PowerShell script:

$scriptPath = Split-Path $script:MyInvocation.MyCommand.Path
Import-Module ("{0}\migrate.ps1" -f $scriptPath) -Verbose

Describe 'Migrate' {
    Context "When calling" {
        Mock Restore-Database {
            return "OK"
        }
    }
 
 It 'Should call all mocks' {
  Assert-VerifiableMocks
 }
}

One of the strengths of modules is that they must be imported. When they are imported a snapshot is kept in the scope of the importing script. Unfortunately, that means when changing the module, the changes are not immediately reflected because a snapshot still exists against the unit test script. My particular solution is to use "remove-module". There is the recommended solution from Pester here. However, for my purposes this is overkill.


Get-Module | Remove-Module
$scriptPath = (Get-Location).Path
Import-Module ("{0}\myRealCodeModule.psm1" -f $scriptPath)


The other strength of modules is that only exported functions are exposed, which means that functions can be internalised or made "private". This is a good thing because I have more scope for writing neat, well named functions which will make my code more readable to future maintainers.

Unfortunately this is a potential barrier to unit testing effectively. Luckily Pester has the answer here as well with "InModuleScope":


   Describe 'When unit testing get-MyObject which is not exported by the module' {
      InModuleScope myRealCodeModule {
         It 'Should have called the mock' {
            
            Mock get-MyObject {
               return { [PSCustomObject]@{ Project = "New and Exciting Project" } }
            }
            
            my-MainExportedAndThereforePublicFunction

            Assert-MockCalled get-MyObject -Times 1
         }
      }
   }




For a getting started guide: see this excellent blog

No comments:

Post a Comment