Automation,  Azure,  Powershell

Test Azure custom modules with Pester

Before I go too far along with building my LSECosmos module I must add proper tests. Just as a quick refresher (or to get some context if you’re not familiar with the concept), here are some pointers about Test Driven Development and Unit Testing:

While it is relatively straightforward to test simple scripts (we would likely manually run the script testing a 2-3 core scenarios to make sure nothing terrible happens), things can get complicated fairly quickly with longer scripts or modules, especially when they are using a variety of cmdlets to take actions (think about Azure resources for example, or any other system-wide on-prem operation), need to pass data and objects back and forth between calls and so on. If you have written enough lines of code (no matter the language/tool you use), I bet you can remember at least one occasion where you decided to make an apparently small and innocent change to a well working piece of software an all hell broke loose 😵. I recently came across this meme on Facebook, it sums it up nicely 😅 (thanks to CodeChef for sharing):

https://www.facebook.com/CodeChef/photos/a.10150302285647799/10156486095592799/?type=3&__tn__=-R

At its core proper testing (even for Powershell scripts and modules) helps makes sure that future changes do not break existing functionality; of course, that implies we write proper tests for each new feature (and who tests the tests…? 🤯)

Speaking of Powershell you may be familiar with Pester: I won’t go here into too much detail about how to write test with Pester here, but here are a few pointers if you need a refresher or even get started from scratch:

It’s important to get some basics right: Pester tests are contained in *.Tests.ps1files; also, the code to be tested should be contained un one or more functions so simple scripts may need to be refactored to account for this requirement. In the case of my LSECosmos module I am already using one .ps1 file for each function I want the module to export, so I just need to build the proper folder structure to organize my files. This is what I came up with:

PS /Users/carlo/Git/LSECosmos> dir -rec

    Directory: /Users/carlo/Git/LSECosmos

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----            5/5/19 11:35 AM                _private
d-----            5/6/19  6:37 PM                LSECosmos
d-----            5/7/19  6:51 PM                Tests
------            5/4/19  9:35 PM            944 README.md

    Directory: /Users/carlo/Git/LSECosmos/Tests

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
------            5/7/19  6:59 PM           2385 Get-AzCosmosDbAccount.Tests.ps1
------            5/7/19  6:59 PM           2411 Get-AzCosmosDbAccountKey.Tests.ps1
------            5/7/19  6:59 PM           1471 New-AzCosmosDbAccount.Tests.ps1
------            5/7/19  6:59 PM            777 Remove-AzCosmosDbAccount.Tests.ps1

    Directory: /Users/carlo/Git/LSECosmos/_private

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
------            5/5/19 11:35 AM            488 Add-AzCosmosDbDatabase.ps1
------            5/5/19 11:35 AM            132 Update-AzCosmosDbDatabaseThroughput.ps1

    Directory: /Users/carlo/Git/LSECosmos/LSECosmos

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
------            5/5/19 11:50 AM           1007 Get-AzAccessToken.ps1
------            5/6/19  8:06 PM           2030 Get-AzCosmosDbAccount.ps1
------            5/6/19  8:17 PM           1970 Get-AzCosmosDbAccountKey.ps1
------            5/4/19  9:53 PM            651 Get-AzCosmosDbDatabase.ps1
------            5/4/19  7:33 PM           4169 LSECosmos.psd1
------            5/5/19 11:50 AM             70 LSECosmos.psm1
------            5/7/19  6:58 PM           1872 New-AzCosmosDbAccount.ps1
------            5/7/19  6:58 PM           1297 Remove-AzCosmosDbAccount.ps1

The LSECosmos folder contains the actual module, while the Tests folder is where I am storing my test files; I keep the tests in a separate folder because I don’t want to distribute them along with the module.

Let’s take a quick look at one of the test files:

Remove-Module 'LSECosmos' -Force -ErrorAction 'SilentlyContinue'
Import-Module $PSScriptRoot/../LSECosmos/LSECosmos.psm1 -Force -ErrorAction 'Stop'

Describe 'Get-AzCosmosDbAccount' {
    InModuleScope 'LSECosmos' {
        Mock -CommandName 'Get-AzResource' -MockWith {
            # make sure to return the proper object type
            $generic = [Microsoft.Azure.Management.ResourceManager.Models.GenericResource]::new()
            $resource = [Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSResource]::new($generic)
            $resource.Name = 'mycosmosaccount'
            $resource.ResourceGroupName = 'mycosmosaccountRG'
            $resource.ResourceType = 'Microsoft.DocumentDb/databaseAccounts'
            $resource.Location = 'northcentralus'
            $resource.ResourceId = '/subscriptions/f897c2fa-a735-4e03-b019-890cd2f7109e/resourceGroups/mycosmosaccountRG/providers/Microsoft.DocumentDb/databaseAccounts/mycosmosaccount'

            return $resource
        }

        Context "Without input parameters" {
            It "Should return a 'Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSResource' object type" {
                LSECosmos\Get-AzCosmosDbAccount | Should -BeOfType 'Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSResource'
            }

            it "Should return exactly one object" {
                LSECosmos\Get-AzCosmosDbAccount | Should -HaveCount 1
            }
        }

        Context "Filter by AccountName" {
            It "Given valid AccountName <Filter> it returns <Expected>" -TestCases @(
                @{'Filter' = 'mycosmosaccount'; 'Expected' = 'mycosmosaccount' }
                @{'Filter' = 'mycosmosac*'; 'Expected' = 'mycosmosaccount' }
                @{'Filter' = '*cosmosac*'; 'Expected' = 'mycosmosaccount' }
                @{'Filter' = '*osaccount'; 'Expected' = 'mycosmosaccount' }
            ) -Test {
                param ($Filter, $Expected)

                $account = LSECosmos\Get-AzCosmosDbAccount -AccountName $Filter
                $account.AccountName | Should -Be 'mycosmosaccount'
            }

            It 'Given invalid AccountName "nonexisting" it returns NULL' {
                $account = LSECosmos\Get-AzCosmosDbAccount -AccountName 'nonexisting'
                $account.AccountName | Should -BeNullOrEmpty
            }
        }
    }
}

This is a very basic test but I am still trying to cover some basic scenarios, here are a few important notes:

  • Since I am testing a function exported by a Module, I must use InModuleScope(I forgot to use this block in my first test iteration, I stared at misbehaving tests for almost 10 minutes before I realized my mistake 🙄)
  • Note the Mock block: of course I don’t want to create a real CosmosDb account every time I run the test but I also want the test to be as accurate as possible, so I am mocking the proper PSResource object type as I would get in a real function run
  • Finally I run a few tests passing various input combinations

Now for the actual test run (this is where all tests pass):

successful test

Now to show how a failed test looks like, let’s mess up with it a bit: line 7 will not return anything but the test is expecting a value

Context "Filter by AccountName" {
  It "Given valid AccountName <Filter> it returns <Expected>" -TestCases @(
    @{'Filter' = 'mycosmosaccount'; 'Expected' = 'mycosmosaccount' }
    @{'Filter' = 'mycosmosac*'; 'Expected' = 'mycosmosaccount' }
    @{'Filter' = '*cosmosac*'; 'Expected' = 'mycosmosaccount' }
    @{'Filter' = '*osaccount'; 'Expected' = 'mycosmosaccount' }
    @{'Filter' = 'osaccount'; 'Expected' = 'mycosmosaccount' }
  ) -Test {
    param ($Filter, $Expected)

    $account = LSECosmos\Get-AzCosmosDbAccount -AccountName $Filter
    $account.AccountName | Should -Be 'mycosmosaccount'
}
failed test

If I want to run all tests at once (it will be useful later) I just just pass the folder name as Path parameter to Pester: Invoke-Pester -Path ./Tests/

run all tests

This is useful to make sure the entire module is tested and working properly before we share or publish online. Pester integrates well with CI (Continuous Integration) services such as TFS, Jenkins, Azure Pipelines etc…: so if I decide to publish my module to the Powershell Gallery I could build an integration between Github and Azure Pipelines (or any other supported CI service for that matter): when I merge a new change in my master branch I could run all Pester test automatically (and other validation tools such as ScriptAnalyzer) and if all goes well, automatically publish the module to the Powershell Gallery. If something fails the pipeline would stop and no harm is done. 🤓


The weak can never forgive. Forgiveness is the attribute of the strong. – Mahatma Gandhi 

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.