Improving Pester and Exception Assertion

Today’s post will cover the Pester testing framework. If you ever have used Pester for more the sunshine scenarios you will have noticed that the assertion of exceptions is a little bit, hmm, awkward. Pester provides an integrated Throw operator that will parse the exception message. This has some limitations such as when running in non-english environments the message may change due to the language used by the operating system or user.
Furthermore it proves nearly impossible to retrieve the actual Exception type. And to rely only on messages (even in english-only environments) is not really reliable. So most of us resorted to scenarios such as:

try
{
  1 / 0;
}
catch
{
  # do something with the exception
  $_.Exception.GetType().Name | Should Be 'System.DivideByZeroException`
}

This is not only laboursome, but also hard to read. What, if we could just write …

{ 1 / 0 } | Should Throw System.DivideByZeroException

… or even better:

{ 1 / 0 } | Should Throw DivideByZeroException

We can! Thanks to Pester, Extending Pester for fun and profit and the hint of Jakub Jareš I came across the Pester feature of writing custom assertions. When I gave it a try it turned out to be rather easy (except for a minor nuisance, which I will talk about later).

As outlined in the article, every assertion needs to supply a triple of functions which can reside in a module or anywhere accessible by Pester and must adhere to a special naming convention. After that you are all set and can be using that assertion right along as you can see in the following examples:

Describe -Tags "Test-ThrowException.Tests" "Test-ThrowException.Tests" {

  Mock Export-ModuleMember { return $null; }

  # . "$here\$sut"

  Context "Test-ThrowException" {

    It 'ThrowExceptionWithFullQualifiedExpectedExceptionSucceeds' -Test {
      { 1 / 0 } | Should ThrowException System.DivideByZeroException;
    }

    It 'ThrowExceptionWithExpectedExceptionSucceeds' -Test {
      { 1 / 0 } | Should ThrowException DivideByZeroException;
    }

    It 'ThrowExceptionWithPartialExpectedExceptionSucceeds' -Test {
      { 1 / 0 } | Should ThrowException 'DivideByZero';
    }

    It 'ThrowExceptionWithRegexExpectedExceptionSucceeds' -Test {
      { 1 / 0 } | Should ThrowException '\w+\.DivideByZeroException$';
    }

    It 'ThrowExceptionWithUnexpectedExceptionIsSupposedToFail' -Test {
      { 1 / 0 } | Should ThrowException CommandNotFoundException;
    }

    It 'ThrowExceptionWithoutExpectedExceptionSucceeds' -Test {
      { 1 / 0 } | Should Not ThrowException CommandNotFoundException;
    }

    It 'ThrowExceptionWithoutExceptionIsSupposedToFail' -Test {
      { 1 * 0 } | Should ThrowException System.DivideByZeroException;
    }

    It 'ThrowExceptionWithoutExceptionShouldSucceedButActuallyFails' -Test {
      { 1 * 0 } | Should Not ThrowException System.DivideByZeroException;
    }
  }

}

The corresponding test results will look similar to this:

Describing Test-ThrowException.Tests
   Context Test-ThrowException
    [+] Warmup 156ms
    [+] AssertionExists 334ms
    [+] GettingHelp-ShouldSucceed 47ms
    [+] ThrowExceptionWithFullQualifiedExpectedExceptionSucceeds 45ms
    [+] ThrowExceptionWithExpectedExceptionSucceeds 43ms
    [+] ThrowExceptionWithPartialExpectedExceptionSucceeds 41ms
    [+] ThrowExceptionWithRegexExpectedExceptionSucceeds 34ms
    [-] ThrowExceptionWithUnexpectedExceptionIsSupposedToFail 40ms
      Test was expected to throw exception of type 'CommandNotFoundException', but was not thrown.
      58:                       { 1 / 0 } | Should ThrowException CommandNotFoundException;
      at , C:\Github\biz.dfch.PS.Pester.Assertions\src\ThrowException.Tests.ps1: line 58
    [+] ThrowExceptionWithoutExpectedExceptionSucceeds 50ms
    [-] ThrowExceptionWithoutExceptionIsSupposedToFail 44ms
      RuntimeException: Test was supposed to throw exception 'System.DivideByZeroException', but was not thrown.
      at PesterThrowException, C:\Program Files\WindowsPowerShell\Modules\biz.dfch.PS.Pester.Assertions\ThrowException.ps1: line 84
      at Get-TestResult, C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Assertions\Should.ps1: line 45
      at , C:\Github\biz.dfch.PS.Pester.Assertions\src\ThrowException.Tests.ps1: line 70
    [-] ThrowExceptionWithoutExceptionShouldSucceedButActuallyFails 52ms
      RuntimeException: Test was supposed to throw exception 'System.DivideByZeroException', but was not thrown.
      at PesterThrowException, C:\Program Files\WindowsPowerShell\Modules\biz.dfch.PS.Pester.Assertions\ThrowException.ps1: line 84
      at Get-TestResult, C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Assertions\Should.ps1: line 45
      at , C:\Github\biz.dfch.PS.Pester.Assertions\src\ThrowException.Tests.ps1: line 76

When looking closely at the last the ThrowExceptionWithoutExceptionShouldSucceedButActuallyFails you will notice that if fails but it should actually not. This is what I mentioned before. As Pester will only pass the ScriptBlock to the assertion as the $value parameter, the assertion cmdlet cannot detect that is was actually called with a NOT operator. Therefore the test will fail, though it did not throw any exception.

You can partly work around this behaviour by using the integrated Should Not Throw from Pester to state you expect no exception at all:

It 'ShouldNotThrowWithNoException' -Test {
  { 1 * 0 } | Should Not Throw;
}

Or when you really want to assert that a specific was NOT thrown, you can always fall back to the known try/catch pattern. Personally I think I can live with this limitation.

But apart from that, the assertion extension mechanism works as expected and comes in really handy. In case you are interested have a look at the biz.dfch.PS.Pester.Assertions module with it’s Wiki, which is also available at NuGet. There you will find more assertions that will simplify your live with Pester.

And now happy testing…

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: