<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mathieu BUISSON</title>
    <description>The latest articles on DEV Community by Mathieu BUISSON (@mathieubuisson).</description>
    <link>https://dev.to/mathieubuisson</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F33502%2F23deeeaa-cab2-4a68-a8a8-a59aaa1cb38e.png</url>
      <title>DEV Community: Mathieu BUISSON</title>
      <link>https://dev.to/mathieubuisson</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mathieubuisson"/>
    <language>en</language>
    <item>
      <title>Writing and Using Custom Assertions for Pester Tests</title>
      <dc:creator>Mathieu BUISSON</dc:creator>
      <pubDate>Tue, 31 Oct 2017 00:00:00 +0000</pubDate>
      <link>https://dev.to/mathieubuisson/writing-and-using-custom-assertions-for-pester-tests-3ghc</link>
      <guid>https://dev.to/mathieubuisson/writing-and-using-custom-assertions-for-pester-tests-3ghc</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/pester/Pester"&gt;Pester&lt;/a&gt;, the awesome PowerShell testing framework has recently introduced a capability which allows us to extend our test assertions. This allows to simplify our tests by abstracting custom or complex assertion logic away from the tests and into separate scripts or modules.&lt;/p&gt;

&lt;p&gt;Keeping the test scripts as clean and readable as possible is central to leveraging the simplicity and elegance of Pester’s DSL. Also, I really like the idea that test scripts can act as an &lt;strong&gt;executable&lt;/strong&gt; (potentially business-readable) &lt;strong&gt;specification&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Concretely, “&lt;em&gt;custom assertions&lt;/em&gt;” means that we can plug additional operators into &lt;strong&gt;Pester&lt;/strong&gt; ’s assertion function : &lt;code&gt;Should&lt;/code&gt;. Assuming we are using the &lt;code&gt;Pester&lt;/code&gt; version 4.0.8, there are quite a few built-in &lt;code&gt;Should&lt;/code&gt; operators :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\&amp;gt; $PesterPath = (Get-Module -Name 'Pester' -ListAvailable)[0].ModuleBase
C:\&amp;gt; $AllItems = (Get-ChildItem "$PesterPath\Functions\Assertions\").BaseName
C:\&amp;gt; $AllItems.Where({ $_ -notmatch 'Should|Set-' })
Be
BeGreaterThan
BeIn
BeLessThan
BeLike
BeLikeExactly
BeNullOrEmpty
BeOfType
Exist
FileContentMatch
FileContentMatchExactly
FileContentMatchMultiline
Match
MatchExactly
PesterThrow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But in some cases, it is possible that none of these operators fit the type of assertion/comparison needed in our tests.&lt;br&gt;&lt;br&gt;
For example, let’s say we need to validate that a number is within an &lt;strong&gt;inclusive&lt;/strong&gt; range. With the built-in operators we are limited to the &lt;code&gt;BeGreaterThan&lt;/code&gt; and &lt;code&gt;BeLessThan&lt;/code&gt;, so our test would look like this :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Describe 'Built-in assertions with numbers' {
    It '55.5 should be in range [0-100]' {
        55.5 | Should -BeGreaterThan -0.000001
        55.5 | Should -BeLessThan 100.000001
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, it forces us to have 2 assertions in a single test, which is not ideal.&lt;br&gt;&lt;br&gt;
Second, because we want an &lt;strong&gt;inclusive&lt;/strong&gt; range and there is no inclusive operators like &lt;code&gt;BeGreaterThanOrEqualTo&lt;/code&gt; or &lt;code&gt;BeLessThanOrEqualTo&lt;/code&gt;, we have to modify the “&lt;em&gt;expected&lt;/em&gt;” part of the assertion.&lt;/p&gt;

&lt;p&gt;Instead of comparing the value to the &lt;em&gt;expected&lt;/em&gt; low end of the range (0), we have to compare it with the “&lt;em&gt;expected&lt;/em&gt;” minus “&lt;em&gt;just a little bit&lt;/em&gt;” to ensure that the assertion passes if the value &lt;strong&gt;equals&lt;/strong&gt; the &lt;em&gt;expected&lt;/em&gt; low end of the range. A similar gymnastic is needed when asserting the high end of the range.&lt;/p&gt;

&lt;p&gt;This is not only confusing, but also unreliable. The precision of the assertion depends on how small is the “&lt;em&gt;just a little bit&lt;/em&gt;”, or in other words, how many decimal places we add/remove to the “&lt;em&gt;expected&lt;/em&gt;” part of the assertion.&lt;/p&gt;

&lt;p&gt;This whole thing feels wrong so let’s write a better assertion using a custom &lt;code&gt;Should&lt;/code&gt; operator.&lt;/p&gt;
&lt;h2&gt;
  
  
  Writing a custom Should operator
&lt;/h2&gt;

&lt;p&gt;There isn’t much documentation nor examples out there yet, so for now, the best place to start is &lt;a href="https://github.com/pester/Pester/wiki/Add-AssertionOperator"&gt;this example in the Pester wiki&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our custom operator is going to be named &lt;code&gt;BeInRange&lt;/code&gt; and unlike the examples we can find, it will require 2 values to represent the “&lt;em&gt;expected&lt;/em&gt;” part of the assertion :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$Min&lt;/code&gt; for the low end of the range&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$Max&lt;/code&gt; for the high end of the range&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So here is what the assertion operator function &lt;code&gt;BeInRange&lt;/code&gt; looks like :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Function BeInRange {
&amp;lt;#
.SYNOPSIS
Tests whether a value is in a given inclusive range
#&amp;gt;
    [CmdletBinding()]
    Param(
        $ActualValue,
        $Min,
        $Max,
        [switch]$Negate
    )

    [bool]$Pass = $ActualValue -ge $Min -and $ActualValue -le $Max
    If ( $Negate ) { $Pass = -not($Pass) }

    If ( -not($Pass) ) {
        If ( $Negate ) {
            $FailureMessage = 'Expected: value {{{0}}} to be outside the range {{{1}-{2}}} but it was in the range.' -f $ActualValue, $Min, $Max
        }
        Else {
            $FailureMessage = 'Expected: value {{{0}}} to be in the range {{{1}-{2}}} but it was outside the range.' -f $ActualValue, $Min, $Max
        }
    }

    $ObjProperties = @{
        Succeeded = $Pass
        FailureMessage = $FailureMessage
    }
    return New-Object PSObject -Property $ObjProperties
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First thing to note : the parameter representing the “&lt;em&gt;actual&lt;/em&gt;” part of the assertion has to be named &lt;code&gt;ActualValue&lt;/code&gt;. If not, &lt;strong&gt;Pester&lt;/strong&gt; ’s internal function &lt;code&gt;Invoke-Assertion&lt;/code&gt; blows up because it calls any assertion function with the &lt;code&gt;ActualValue&lt;/code&gt; parameter to pass the asserted value.&lt;/p&gt;

&lt;p&gt;All &lt;code&gt;Should&lt;/code&gt; operators can be negated by inserting &lt;code&gt;-Not&lt;/code&gt; before them. For our custom operator to respect this behavior, we need to implement the &lt;code&gt;Negate&lt;/code&gt; switch parameter, as seen above.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What’s the deal with the triple braces in the &lt;code&gt;$FailureMessage&lt;/code&gt; string ?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Brace #1 : The string formatting operator (&lt;code&gt;-f&lt;/code&gt;) uses &lt;code&gt;{}&lt;/code&gt; as placeholders&lt;/li&gt;
&lt;li&gt;Brace #2 : We enclose the currently asserted values in &lt;code&gt;{}&lt;/code&gt; to be consistent with built-in assertion’s failure messages&lt;/li&gt;
&lt;li&gt;Brace #3 : The string formatting operator freaks out if the string contains braces, so we double the second set of braces to escape them&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Our assertion operator function has a contractual obligation : it has to return an object of the type &lt;code&gt;[PSObject]&lt;/code&gt; with a property named &lt;code&gt;Succeeded&lt;/code&gt; and a property named &lt;code&gt;FailureMessage&lt;/code&gt;. So we built &lt;code&gt;$ObjProperties&lt;/code&gt; accordingly.&lt;/p&gt;

&lt;p&gt;How do we know that ?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\&amp;gt; Get-Help 'Add-AssertionOperator' -Parameter 'Test'

-Test &amp;lt;ScriptBlock&amp;gt;
    The test function. The function must return a PSObject with a [Bool]succeeded
    and a [string]failureMessage property.

    Required? true
    Position? 2
    Default value
    Accept pipeline input? false
    Accept wildcard characters? false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using a custom Should operator in our tests
&lt;/h2&gt;

&lt;p&gt;We put our &lt;code&gt;BeInRange&lt;/code&gt; function in a module named &lt;code&gt;CustomAssertions.psm1&lt;/code&gt; to make it easy to reuse. We could even store the module within a location in our &lt;code&gt;$Env:PSModulePath&lt;/code&gt; to allow importing it by name, instead of by path.&lt;/p&gt;

&lt;p&gt;There is another way : put the custom operator function in a .ps1 file inside &lt;strong&gt;Pester&lt;/strong&gt; ’s assertions folder : &lt;code&gt;"$((Get-Module Pester).ModuleBase)\Functions\Assertions\"&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Pester&lt;/strong&gt; picks up the operators automatically from this location but this might be less flexible, so choose a method based on your preference/scenario.&lt;/p&gt;

&lt;p&gt;Now is the time to write tests to verify how much cleaner our assertions can be with our custom operator and ensure that it behaves as expected :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Import-Module "$PSScriptRoot\CustomAssertions.psm1" -Force
Add-AssertionOperator -Name 'BeInRange' -Test $Function:BeInRange

Describe 'BeInRange assertions with numbers' {
    It '55.5 should be in range [0-100]' {
        55.5 | Should -BeInRange -Min 0 -Max 100
    }
    It '0 should be in range [0-100] (inclusive range)' {
        0 | Should -BeInRange -Min 0 -Max 100
    }
    It '80 should not be in range [0-55.5]' {
        80 | Should -Not -BeInRange -Min 0 -Max 55.5
    }
    It 'Should fail (to verify the failure message)' {
        1 | Should -BeInRange -Min 10 -Max 20
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line imports the module to make our &lt;code&gt;BeInRange&lt;/code&gt; function visible in the global scope to ensure &lt;code&gt;Add-AssertionOperator&lt;/code&gt; will see it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Add-AssertionOperator&lt;/code&gt; is the function from &lt;strong&gt;Pester&lt;/strong&gt; which enables the magic. It registers a custom assertion operator function with &lt;strong&gt;Pester&lt;/strong&gt; which integrates it with the &lt;code&gt;Should&lt;/code&gt; function as a dynamic parameter. Clever stuff.&lt;/p&gt;

&lt;p&gt;Unfortunately, as soon as we run this tests script more than once (without unloading the &lt;code&gt;Pester&lt;/code&gt; module), we get this error :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executing script .\CustomAssertions.Tests.ps1
  [-] Error occurred in test script '.\CustomAssertions.Tests.ps1' 35ms
    RuntimeException: Assertion operator name 'BeInRange' has been added multiple times.
    at Assert-AssertionOperatorNameIsUnique, C:\Program Files\WindowsPowerShell\Modules\Pester\4.0.8\Pester.psm1: line 243
    at Add-AssertionOperator, C:\Program Files\WindowsPowerShell\Modules\Pester\4.0.8\Pester.psm1: line 211
    at &amp;lt;ScriptBlock&amp;gt;, C:\CustomAssertions\CustomAssertions.Tests.ps1: line 2
    at &amp;lt;ScriptBlock&amp;gt;, C:\Program Files\WindowsPowerShell\Modules\Pester\4.0.8\Pester.psm1: line 802
    at Invoke-Pester&amp;lt;End&amp;gt;, C:\Program Files\WindowsPowerShell\Modules\Pester\4.0.8\Pester.psm1: line 817
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thankfully &lt;a href="https://github.com/pester/Pester/issues/891"&gt;this bug&lt;/a&gt; is fixed in the master branch and slated for milestone 4.1.&lt;/p&gt;

&lt;p&gt;As we can see above, the assertions using the &lt;code&gt;BeInRange&lt;/code&gt; operator are simpler and more readable than the initial example using the built-in operators.&lt;/p&gt;

&lt;p&gt;Now let’s see if it works :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\&amp;gt; $TestsPath = 'C:\CustomAssertions\CustomAssertions.Tests.ps1'
C:\&amp;gt; Invoke-Pester -Script $TestsPath
Executing all tests in 'C:\CustomAssertions\CustomAssertions.Tests.ps1'

Executing script C:\CustomAssertions\CustomAssertions.Tests.ps1

  Describing BeInRange assertions with numbers
    [+] 55.5 should be in range [0-100] 94ms
    [+] 0 should be in range [0-100] (inclusive range) 53ms
    [+] 80 should not be in range [0-55.5] 51ms
    [-] Should fail (to verify the failure message) 38ms
      Expected: value {1} to be in the range {10-20} but it was outside the range.
      15: 1 | Should -BeInRange -Min 10 -Max 20
      at Invoke-Assertion, C:\Program Files\WindowsPowerShell\Modules\Pester\4.0.8\Functions\Assertions\Should.ps1: line 209
      at &amp;lt;ScriptBlock&amp;gt;, C:\CustomAssertions\CustomAssertions.Tests.ps1: line 15
Tests completed in 237ms
Tests Passed: 3, Failed: 1, Skipped: 0, Pending: 0, Inconclusive: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works as expected.&lt;/p&gt;

&lt;p&gt;But does it work with objects of the type &lt;code&gt;[string]&lt;/code&gt; ? This would allow us to do assertions related to alphabetical ordering.&lt;br&gt;&lt;br&gt;
How about &lt;code&gt;[datetime]&lt;/code&gt; objects ? This would allow to validate that a &lt;code&gt;[datetime]&lt;/code&gt; value is within an expected date (or time) range.&lt;/p&gt;

&lt;p&gt;We add the following tests to our tests script :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Context 'Assertions on [string] objects' {

    It '"abcd" should be in range ["ab"-"yz"]' {
        'abcd' | Should -BeInRange -Min 'ab' -Max 'yz'
    }
    It '"a" should be in range ["a"-"yz"]' {
        'a' | Should -BeInRange -Min 'a' -Max 'yz'
    }
    It '"az" should not be in range ["ab"-"aefg"]' {
        'az' | Should -Not -BeInRange -Min 'ab' -Max 'aefg'
    }
}
Context 'Assertions on [datetime] objects' {
    $Now = [datetime]::Now

    It 'The 1st of October 2017 should be in the range representing the current year' {
        $YearStart = Get-Date -Month 1 -Day 1
        $YearEnd = Get-Date -Month 12 -Day 31
        $FirstOct = Get-Date -Year 2017 -Month 10 -Day 1
        $FirstOct | Should -BeInRange -Min $YearStart -Max $YearEnd
    }
    It 'Today at 10 AM should be between today at 01 AM and now' {
        $DayStart = Get-Date -Hour 1 -Minute 0
        Get-Date -Hour 10 -Minute 0 | Should -BeInRange -Min $DayStart -Max $Now
    }
    It 'Now should not be in the range representing last month' {
        $LastMonthStart = (Get-Date -Day 1 -Hour 0 -Minute 0).AddMonths(-1)
        $LastMonthEnd = (Get-Date -Day 1 -Hour 0 -Minute 0).AddMinutes(-1)
        $Now | Should -Not -BeInRange -Min $LastMonthStart -Max $LastMonthEnd
    }
    It 'Now should not be in a range located in the future' {
        $FutureStart = Get-Date -Year 2117
        $FutureEnd = Get-Date -Year 2217
        $Now | Should -Not -BeInRange -Min $FutureStart -Max $FutureEnd
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we run the tests script again :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\&amp;gt; Invoke-Pester -Script $TestsPath                                                         
Executing all tests in 'C:\CustomAssertions\CustomAssertions.Tests.ps1'                       

Executing script C:\CustomAssertions\CustomAssertions.Tests.ps1                               

  Describing BeInRange assertions with numbers                                                

    Context Assertions on numbers                                                             
      [+] 55.5 should be in range [0-100] 76ms                                                
      [+] 0 should be in range [0-100] (inclusive range) 27ms                                 
      [+] 80 should not be in range [0-55.5] 28ms                                             
      [-] Should fail (to verify the failure message) 28ms                                    
        Expected: value {1} to be in the range {10-20} but it was outside the range.          
        17: 1 | Should -BeInRange -Min 10 -Max 20                                 
        at Invoke-Assertion, C:\Program Files\WindowsPowerShell\Modules\Pester\4.0.8\Functions
\Assertions\Should.ps1: line 209                                                              
        at &amp;lt;ScriptBlock&amp;gt;, C:\CustomAssertions\CustomAssertions.Tests.ps1: line 17             

    Context Assertions on [string] objects                                                    
      [+] "abcd" should be in range ["ab"-"yz"] 65ms                                          
      [+] "a" should be in range ["a"-"yz"] 26ms                                              
      [+] "az" should not be in range ["ab"-"aefg"] 16ms                                      

    Context Assertions on [datetime] objects                                                  
      [+] The 1st of October 2017 should be in the range representing the current year 68ms   
      [+] Today at 10 AM should be between today at 01 AM and now 31ms                        
      [+] Now should not be in the range representing last month 32ms                         
      [+] Now should not be in a range located in the future 33ms                             
Tests completed in 436ms                                                                      
Tests Passed: 10, Failed: 1, Skipped: 0, Pending: 0, Inconclusive: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Indeed, it does work for these 3 types of objects.&lt;/p&gt;

&lt;p&gt;Now that we know that the “&lt;em&gt;expected&lt;/em&gt;” part of the assertion can be comprised of multiple values, this enables complex or specialized assertions. Let’s try a more complex assertion operator which may be useful in infrastructure validation scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asserting that an IP address is in a given subnet
&lt;/h2&gt;

&lt;p&gt;We are going to write a new function named &lt;code&gt;BeInSubnet&lt;/code&gt; containing logic to check if an IPv4 address is in the same subnet as a specified address (this can be the network ID but it doesn’t have to be) and a specified subnet mask. We’ll add it to our existing &lt;strong&gt;CustomAssertions&lt;/strong&gt; module.&lt;/p&gt;

&lt;p&gt;Here is the function :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Function BeInSubnet {
&amp;lt;#
.SYNOPSIS
Tests whether an IPv4 address in the same subnet as a given address with a given subnet mask.
#&amp;gt;
    [CmdletBinding()]
    Param(
        $ActualValue,
        $Network,
        $Mask,
        [switch]$Negate
    )
    If ( $ActualValue -isnot [ipaddress] ) {
        $ActualValue = $ActualValue -as [ipaddress]
    }
    If ( $Network -isnot [ipaddress] ) {
        $Network = $Network -as [ipaddress]
    }
    If ( $Mask -isnot [ipaddress] ) {
        $Mask = $Mask -as [ipaddress]
    }

    $ActualNetworkBinary = $ActualValue.Address -band $Mask.Address
    $ExpectedNetworkBinary = $Network.Address -band $Mask.Address

    [bool]$Pass = $ActualNetworkBinary -eq $ExpectedNetworkBinary
    If ( $Negate ) { $Pass = -not($Pass) }

    If ( -not($Pass) ) {
        $ActualSubnetString = ($ActualNetworkBinary -as [ipaddress]).IPAddressToString
        If ( $Negate ) {
            $FailureMessage = 'Expected: address {{{0}}} to be outside subnet {{{1}}} with mask {{{2}}} but was within it.' -f $ActualValue.IPAddressToString, $Network.IPAddressToString, $Mask.IPAddressToString
        }
        Else {
            $FailureMessage = 'Expected: address {{{0}}} to be in subnet {{{1}}} with mask {{{2}}} but was in subnet {{{3}}}.' -f $ActualValue.IPAddressToString, $Network.IPAddressToString, $Mask.IPAddressToString, $ActualSubnetString
        }
    }

    $ObjProperties = @{
        Succeeded = $Pass
        FailureMessage = $FailureMessage
    }
    return New-Object PSObject -Property $ObjProperties
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function’s parameters don’t enforce a specific &lt;code&gt;[type]&lt;/code&gt;, this is to make the function more flexible. That way, the assertions using this operator will be able to pass &lt;code&gt;[string]&lt;/code&gt; or &lt;code&gt;[ipaddress]&lt;/code&gt; objects into it.&lt;/p&gt;

&lt;p&gt;This is why we check the type of each value passed via the function’s parameters &lt;code&gt;$ActualValue&lt;/code&gt;, &lt;code&gt;$Network&lt;/code&gt;, &lt;code&gt;$Mask&lt;/code&gt; and convert them to the type &lt;code&gt;[ipaddress]&lt;/code&gt; for further manipulation.&lt;/p&gt;

&lt;p&gt;The rest of the function is fairly similar to our previous custom operator function.&lt;/p&gt;

&lt;p&gt;Then, we create a tests script (&lt;code&gt;BeInSubnet.Tests.ps1&lt;/code&gt;) to verify that our &lt;code&gt;BeInSubnet&lt;/code&gt; custom assertion operator behaves as expected :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Import-Module "$PSScriptRoot\CustomAssertions.psm1" -Force
Add-AssertionOperator -Name 'BeInSubnet' -Test $Function:BeInSubnet

Describe 'BeInSubnet assertions' {
    Context 'Assertions on [string] objects' {

        It '"10.1.5.193" should be in the same subnet as "10.1.5.0" with mask "255.255.255.0"' {
            '10.1.5.193' | Should -BeInSubnet -Network '10.1.5.0' -Mask '255.255.255.0'
        }
        It '"10.1.5.0" should be in the same subnet as "10.1.5.0" with mask "255.255.255.0"' {
            '10.1.5.0' | Should -BeInSubnet -Network '10.1.5.0' -Mask '255.255.255.0'
        }
        It '"10.1.5.193" should be in the same subnet as "10.1.5.0" with mask "255.0.0.0"' {
            '10.1.5.193' | Should -BeInSubnet -Network '10.1.5.0' -Mask '255.0.0.0'
        }
        It '"10.1.5.193" should not be in the same subnet as "10.1.5.0" with mask "255.255.255.128"' {
            '10.1.5.193' | Should -Not -BeInSubnet -Network '10.1.5.0' -Mask '255.255.255.128'
        }
        It 'Should fail (to verify the failure message)' {
            '10.1.5.193' | Should -BeInSubnet -Network '10.1.5.0' -Mask '255.255.255.128'
        }
    }
    Context 'Assertions on [ipaddress] objects' {
        $Value = '10.1.5.193' -as [ipaddress]
        $Network = '10.1.5.0' -as [ipaddress]
        $SubnetMask = '255.255.255.0' -as [ipaddress]
        $LargeSubnetMask = '255.0.0.0' -as [ipaddress]
        $SmallSubnetMask = '255.255.255.128' -as [ipaddress]

        It '"10.1.5.193" should be in the same subnet as "10.1.5.0" with mask "255.255.255.0"' {
            $Value | Should -BeInSubnet -Network $Network -Mask $SubnetMask
        }
        It '"10.1.5.0" should be in the same subnet as "10.1.5.0" with mask "255.255.255.0"' {
            $Network | Should -BeInSubnet -Network $Network -Mask $SubnetMask
        }
        It '"10.1.5.193" should be in the same subnet as "10.1.5.0" with mask "255.0.0.0"' {
            $Value | Should -BeInSubnet -Network $Network -Mask $LargeSubnetMask
        }
        It '"10.1.5.193" should not be in the same subnet as "10.1.5.0" with mask "255.255.255.128"' {
            $Value | Should -Not -BeInSubnet -Network $Network -Mask $SmallSubnetMask
        }
        It 'Should fail (to verify the failure message)' {
            $Value | Should -BeInSubnet -Network $Network -Mask $SmallSubnetMask
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests are clean and readable. Now, imagine if we had to do the same assertions with the built-in &lt;code&gt;Should&lt;/code&gt; operators… The tests script would have been cluttered with a large amount of &lt;code&gt;[ipaddress]&lt;/code&gt; manipulation code, unless this code was extracted into a helper function.&lt;/p&gt;

&lt;p&gt;Let’s run these tests and check the result :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\&amp;gt; $SubnetTestsPath = 'C:\CustomAssertions\BeInSubnet.Tests.ps1'
C:\&amp;gt; Invoke-Pester -Script $SubnetTestsPath
Executing all tests in 'C:\CustomAssertions\BeInSubnet.Tests.ps1'

Executing script C:\CustomAssertions\BeInSubnet.Tests.ps1

  Describing BeInSubnet assertions

    Context Assertions on [string] objects
      [+] "10.1.5.193" should be in the same subnet as "10.1.5.0" with mask "255.255.255.0" 138ms
      [+] "10.1.5.0" should be in the same subnet as "10.1.5.0" with mask "255.255.255.0" 20ms
      [+] "10.1.5.193" should be in the same subnet as "10.1.5.0" with mask "255.0.0.0" 23ms
      [+] "10.1.5.193" should not be in the same subnet as "10.1.5.0" with mask "255.255.255.128" 23ms
      [-] Should fail (to verify the failure message) 27ms
        Expected: address {10.1.5.193} to be in subnet {10.1.5.0} with mask {255.255.255.128} but was in subnet {10.1.5.128}.
        20: '10.1.5.193' | Should -BeInSubnet -Network '10.1.5.0' -Mask '255.255.255.128'
        at Invoke-Assertion, C:\Program Files\WindowsPowerShell\Modules\Pester\4.0.8\Functions\Assertions\Should.ps1: line 209
        at &amp;lt;ScriptBlock&amp;gt;, C:\CustomAssertions\BeInSubnet.Tests.ps1: line 20

    Context Assertions on [ipaddress] objects
      [+] "10.1.5.193" should be in the same subnet as "10.1.5.0" with mask "255.255.255.0" 80ms
      [+] "10.1.5.0" should be in the same subnet as "10.1.5.0" with mask "255.255.255.0" 26ms
      [+] "10.1.5.193" should be in the same subnet as "10.1.5.0" with mask "255.0.0.0" 28ms
      [+] "10.1.5.193" should not be in the same subnet as "10.1.5.0" with mask "255.255.255.128" 18ms
      [-] Should fail (to verify the failure message) 29ms
        Expected: address {10.1.5.193} to be in subnet {10.1.5.0} with mask {255.255.255.128} but was in subnet {10.1.5.128}.
        43: $Value | Should -BeInSubnet -Network $Network -Mask $SmallSubnetMask
        at Invoke-Assertion, C:\Program Files\WindowsPowerShell\Modules\Pester\4.0.8\Functions\Assertions\Should.ps1: line 209
        at &amp;lt;ScriptBlock&amp;gt;, C:\CustomAssertions\BeInSubnet.Tests.ps1: line 43
Tests completed in 418ms
Tests Passed: 8, Failed: 2, Skipped: 0, Pending: 0, Inconclusive: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good, everything works as expected.&lt;/p&gt;

&lt;p&gt;Also, we can see that the failure message is quite helpful. It tells us exactly what is going on, with the “&lt;em&gt;actual&lt;/em&gt;” values and the “&lt;em&gt;expected&lt;/em&gt;” values. This is a major advantage of custom assertions, it enables us to create more meaningful failure messages because these messages can be tailored for specific assertions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While there are a few quirks to iron out to make this feature fully usable and mature, the ability to extend &lt;strong&gt;Pester&lt;/strong&gt; ’s assertion operators with simple PowerShell functions is very powerful.&lt;/p&gt;

&lt;p&gt;This allows to perform very complex or specialized assertions in our tests while keeping them relatively human-readable and remaining true to &lt;strong&gt;Pester&lt;/strong&gt; ’s DSL. This expands &lt;strong&gt;Pester&lt;/strong&gt; ’s assertions capabilities, as well as its use cases.&lt;/p&gt;

&lt;p&gt;Don’t go too crazy, though. I would advise keeping the complexity of the assertion logic to a minimum, because the more complexity, the more chance that something may not work completely as expected. And we want assertions to be &lt;strong&gt;very reliable&lt;/strong&gt; , we want tests to fail because the code under test doesn’t behave as intended, &lt;strong&gt;not the assertion logic&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>pester</category>
    </item>
    <item>
      <title>Making PSScriptAnalyzer a first-class citizen in a PowerShell CI pipeline</title>
      <dc:creator>Mathieu BUISSON</dc:creator>
      <pubDate>Wed, 30 Nov 2016 00:00:00 +0000</pubDate>
      <link>https://dev.to/mathieubuisson/making-psscriptanalyzer-a-first-class-citizen-in-a-powershell-ci-pipeline</link>
      <guid>https://dev.to/mathieubuisson/making-psscriptanalyzer-a-first-class-citizen-in-a-powershell-ci-pipeline</guid>
      <description>&lt;p&gt;As you already know if you have read &lt;a href="https://mathieubuisson.github.io/psscriptanalyzer-appveyor/" rel="noopener noreferrer"&gt;this&lt;/a&gt; or &lt;a href="https://mathieubuisson.github.io/create-custom-rule-psscriptanalyzer/" rel="noopener noreferrer"&gt;this&lt;/a&gt;, I’m a big fan of &lt;code&gt;PSScriptAnalyzer&lt;/code&gt; to maintain a certain coding standard. This is especially powerful inside a release pipeline because this allows us to enforce that coding standard.&lt;/p&gt;

&lt;p&gt;In our CI pipeline, we can easily make the build fail if our code violates &lt;code&gt;PSScriptAnalyzer&lt;/code&gt; rule(s).&lt;br&gt;&lt;br&gt;
That’s great, but the main point of &lt;a href="http://martinfowler.com/articles/continuousIntegration.html" rel="noopener noreferrer"&gt;continuous integration&lt;/a&gt; is to give quick feedback to developers about their code change(s). It is about catching problems early &lt;strong&gt;to fix them&lt;/strong&gt; early. So the question is :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How can we make our CI tool publish &lt;code&gt;PSScriptAnalyzer&lt;/code&gt; results with the information we need to remediate any violation ?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All CI tools have ways to publish test results to make them highly visible, to drill down into a test failure and do some reporting.&lt;/p&gt;

&lt;p&gt;Since we are talking about a &lt;strong&gt;PowerShell&lt;/strong&gt; pipeline, we are most likely already using Pester to test our PowerShell code.&lt;br&gt;&lt;br&gt;
Pester can spit out results in the same &lt;code&gt;XML&lt;/code&gt; format as NUnit and these NUnit &lt;code&gt;XML&lt;/code&gt; files can be consumed and published by most CI tools.&lt;/p&gt;

&lt;p&gt;It makes a lot of sense to leverage this Pester integration as a universal CI glue and run our &lt;code&gt;PSScriptAnalyzer&lt;/code&gt; checks as Pester tests.&lt;br&gt;&lt;br&gt;
Let’s look at possible ways to do that.&lt;/p&gt;
&lt;h2&gt;
  
  
  One Pester test checking PSScriptAnalyzer output
&lt;/h2&gt;

&lt;p&gt;Here is probably the simplest way to invoke &lt;code&gt;PSScriptAnalyzer&lt;/code&gt; from Pester :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Describe 'PSScriptAnalyzer analysis' {    
    $ScriptAnalyzerResults = Invoke-ScriptAnalyzer -Path '.\Example.ps1' -Severity Warning

    It 'Should not return any violation' {
        $ScriptAnalyzerResults | Should BeNullOrEmpty
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we are checking all the rules which have a Warning severity within one single test.&lt;br&gt;&lt;br&gt;
Then, we rely on the fact that if &lt;code&gt;PSScriptAnalyzer&lt;/code&gt; returns something, it means that they were at least one violation and if &lt;code&gt;PSScriptAnalyzer&lt;/code&gt; returns nothing, it’s all good.&lt;/p&gt;

&lt;p&gt;There are 2 problems here :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We are evaluating a whole bunch of rules in a single test, so the test name cannot tell us which rule was violated&lt;/li&gt;
&lt;li&gt;If there are more than one violation, the Pester message gives us useless information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How useless ? Well, let’s see :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invoke-Pester -Script '.\Example.Tests.ps1'
Executing all tests in .\Example.Tests.ps1

Executing script .\Example.Tests.ps1

  Describing PSScriptAnalyzer analysis
    [-] Should not return any violation 1.52s
      Expected: value to be empty but it was {Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord}
      at &amp;lt;ScriptBlock&amp;gt;, E:\Example.Tests.ps1: line 5
      5: $ScriptAnalyzerResults | Should BeNullOrEmpty
Tests completed in 1.52s
Tests Passed: 0, Failed: 1, Skipped: 0, Pending: 0, Inconclusive: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Pester failure message gives us the object type of the &lt;code&gt;PSScriptAnalyzer&lt;/code&gt; results, instead of their contents.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Pester test per PSScriptAnalyzer rule
&lt;/h2&gt;

&lt;p&gt;This is a pretty typical (and better) way of running &lt;code&gt;PSScriptAnalyzer&lt;/code&gt; checks via Pester.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Describe 'PSScriptAnalyzer analysis' {    
    $ScriptAnalyzerRules = Get-ScriptAnalyzerRule -Name "PSAvoid*"

    Foreach ( $Rule in $ScriptAnalyzerRules ) {
        It "Should not return any violation for the rule : $($Rule.RuleName)" {
            Invoke-ScriptAnalyzer -Path ".\Example.ps1" -IncludeRule $Rule.RuleName |
            Should BeNullOrEmpty
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, the first step is to get a list of the rules that we want to evaluate. Here, I changed the list of rules to : all rules which have a name starting with &lt;code&gt;PSAvoid&lt;/code&gt;. This is just to show that we can filter the rules by name, as well as by severity.&lt;/p&gt;

&lt;p&gt;Then, we loop through this list of rules and have a Pester test evaluating each rule, one by one. As we can see below, the output is much more useful :&lt;br&gt;&lt;br&gt;
 &lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmathieubuisson.github.io%2Fimages%2F2016-11-30-psscriptanalyzer-first-class-citizen-by-rule.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmathieubuisson.github.io%2Fimages%2F2016-11-30-psscriptanalyzer-first-class-citizen-by-rule.png" alt="By ScriptAnalyzer Rule"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is definitely better but we still encounter the same issue as before because there were more than one violation for that &lt;code&gt;PSAvoidUsingWMICmdlet&lt;/code&gt; rule. So we still don’t get the file name and the line number.&lt;/p&gt;

&lt;p&gt;We could use a nested loop : for each rule, we would loop through each file.&lt;br&gt;&lt;br&gt;
That would be more granular and reduce the risk of this particular issue. But if a single file violated the same rule more than once, we would still have the same problem.&lt;/p&gt;

&lt;p&gt;So, I decided to go take a different route to address this problem : taking the output from &lt;code&gt;PSScriptAnalyzer&lt;/code&gt; and converting it to a test result file, using the same &lt;code&gt;XML&lt;/code&gt; schema as Pester and NUnit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Converting PSScriptAnalyzer output to a test result file
&lt;/h2&gt;

&lt;p&gt;For that purpose, I wrote a function named &lt;code&gt;Export-NUnitXml&lt;/code&gt;, which is available &lt;a href="https://github.com/MathieuBuisson/PowerShell-DevOps/tree/master/Export-NUnitXml" rel="noopener noreferrer"&gt;in this module&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here are the high-level steps of what &lt;code&gt;Export-NUnitXml&lt;/code&gt; does :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Take the output of &lt;code&gt;PSScriptAnalyzer&lt;/code&gt; as its input&lt;/li&gt;
&lt;li&gt;Create an &lt;code&gt;XML&lt;/code&gt; document containing a “test-case” node for each input object(s)&lt;/li&gt;
&lt;li&gt;Write this &lt;code&gt;XML&lt;/code&gt; document to the file specified via the &lt;code&gt;Path&lt;/code&gt; parameter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is an example of how we can use this within a build script (in &lt;a href="https://ci.appveyor.com" rel="noopener noreferrer"&gt;Appveyor&lt;/a&gt;, in this case) :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ScriptAnalyzerRules = Get-ScriptAnalyzerRule -Severity Warning
$ScriptAnalyzerResult = Invoke-ScriptAnalyzer -Path '.\CustomPSScriptAnalyzerRules\Example.ps1' -IncludeRule $ScriptAnalyzerRules
If ( $ScriptAnalyzerResult ) {  
    $ScriptAnalyzerResultString = $ScriptAnalyzerResult | Out-String
    Write-Warning $ScriptAnalyzerResultString
}
Import-Module '.\Export-NUnitXml\Export-NUnitXml.psm1' -Force
Export-NUnitXml -ScriptAnalyzerResult $ScriptAnalyzerResult -Path '.\ScriptAnalyzerResult.xml'

(New-Object 'System.Net.WebClient').UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", '.\ScriptAnalyzerResult.xml')
If ( $ScriptAnalyzerResult ) {        
    # Failing the build
    Throw 'There was PSScriptAnalyzer violation(s). See test results for more information.'
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the result in Appveyor :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmathieubuisson.github.io%2Fimages%2F2016-11-30-psscriptanalyzer-first-class-citizen-overview.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmathieubuisson.github.io%2Fimages%2F2016-11-30-psscriptanalyzer-first-class-citizen-overview.png" alt="Appveyor Overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just by reading the name of the test case, we get the essential information : the rule name, the file name and even the line number.&lt;/p&gt;

&lt;p&gt;Also, we can expand any failed test to get additional information.&lt;br&gt;&lt;br&gt;
For example, the last 2 tests are expanded below :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmathieubuisson.github.io%2Fimages%2F2016-11-30-psscriptanalyzer-first-class-citizen-details.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmathieubuisson.github.io%2Fimages%2F2016-11-30-psscriptanalyzer-first-class-citizen-details.png" alt="Appveyor test details"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The “ &lt;strong&gt;Stacktrace&lt;/strong&gt; ” section provides additional details, like the rule severity and the actual offending code. Another nice touch is that the “ &lt;strong&gt;Error message&lt;/strong&gt; ” section gives us the rule message, which normally provides an actionable recommendation to remediate the problem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;But, what if &lt;code&gt;PSScriptAnalyzer&lt;/code&gt; returns nothing ?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;Export-NUnitXml&lt;/code&gt; does handle this scenario gracefully because its &lt;code&gt;ScriptAnalyzerResult&lt;/code&gt; parameter accepts &lt;code&gt;$Null&lt;/code&gt;. In this case, the test result file will contain only 1 test case and this test will pass.&lt;/p&gt;

&lt;p&gt;So now, we not only have quick feedback on our adherence to coding standards, but we also get actionable guidance on how to improve.&lt;br&gt;&lt;br&gt;
And remember, this NUnit &lt;code&gt;XML&lt;/code&gt; format is widely supported in the CI/CD tooling world, so this would work similarly in TeamCity, Microsoft VSTS, and others…&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was originally published on &lt;a href="https://mathieubuisson.github.io/psscriptanalyzer-first-class-citizen/" rel="noopener noreferrer"&gt;mathieubuisson.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>devops</category>
      <category>pester</category>
      <category>linting</category>
    </item>
  </channel>
</rss>
