Pester unit tests for Azure Bicep modules

Photo of author

Dan Rios

5 min read

Introduction

I’ve been dabbling into the world of Pester as of late to beef some unit tests with Azure Bicep modules. I thought it would be great if I can enhance the continuous integration pipeline in Azure DevOps for my Azure Bicep repository.

With this, I thought I’d share this solution I came up with which may be of use to others in the community who could benefit or take inspiration from.

As part of a previous blog, I detailed how to reliably scan against your Azure Bicep templates with PSRule here.

This blog in particular takes this concept further, but instead applies Pester unit tests with Azure Bicep modules to make sure you have .tests.bicep files for each Azure Bicep module that exists in the repository structure.

What is Pester? 🐭

Pester is a testing and mocking framework for PowerShell. It provides us a way to write and run unit tests, which means we can utilise this framework to create some cool unit tests as part of CI/CD pipelines.

It is a very popular and powerful toolset within the DevOps ecosystem to harness for unit testing.

Benefits of incorporating unit tests for your Azure Bicep modules ✅

Having robust unit tests as part of your build validation & continuous integration pipelines is crucial for ensuring reliable, consistent, clean and secure Infrastructure as Code.

It is very easy to adopt Azure Bicep as your infrastructure as code tooling of choice due to its ease of readability, growing popularity and native Azure support. However, this potentially also plays against it if you don’t have the proper governance in place.

This is because a lot of cloud teams may have the tendency copy modules and code from GitHub repos dotted around the web that may not fully align to your cloud governance and best practices. (after all, no one wants to reinvent the wheel!)

Therefore, incorporating a strong CI pipeline with robust unit tests will ensure the code being merged into the main branch is up to these standards. The benefits of creating some unit tests for your Azure Bicep modules are:

• Help align your Azure Bicep modules to the Well-Architected & Cloud Adoption Frameworks

• Consistent patterns. Azure Bicep modules have the same unit tests across the board

• Reliable modules. Each module gets a unit test to enable reliable scanning with PSRule to align to Azure best practices

The problem 🧐

Okay, so you might be wondering what issue am I trying to solve here by adding Pester into the mix with Azure Bicep modules. Let’s break it down.

A lot of repositories (GitHub, Azure DevOps, etc.) will have a structure for their Azure Bicep modules that are similar to this:

bicep/
├── modules/
│   ├── resourceGroup.bicep
│   ├── vNetPeering.bicep
│   ├── hubNetworking.bicep
│   ├── networkSecurityGroup.bicep
│   └── etc
├── main.bicep
└── main.bicepparam
Markdown

However, as I’ve detailed in a previous blog here, when running Infrastructure as Code scanning tooling like PSRule, if you are scanning a main.bicep file then you’re likely to encounter lots of param errors. This is mostly because some parameters aren’t populated or can’t be determined before runtime.

Therefore, by adding .tests.bicep files for each Bicep module we’re able to specify the required parameters to scan against with PSRule for best practice and cloud governance.

The repository structure would look similar to this:

bicep/
├── modules/
│   ├── .tests/
│   │   ├── resourceGroup.tests.bicep
│   │   ├── vNetPeering.tests.bicep
│   │   ├── hubNetworking.tests.bicep
│   │   ├── networkSecurityGroup.tests.bicep
│   │   └── etc
│   ├── resourceGroup.bicep
│   ├── vNetPeering.bicep
│   ├── hubNetworking.bicep
│   ├── networkSecurityGroup.bicep
│   └── etc
├── main.bicep
└── main.bicepparam
Markdown

With a .tests.bicep file example that looks like:

// Test with only required parameters
targetScope = 'subscription'
module resourceGroups_test_required_params '../resourceGroup.bicep' = {
  name: 'resourceGroups_test_required_params'
  params: {
    parLocation: 'uksouth'
    parResourceGroupName: 'rg-test'
  }
}
Markdown

Where does Pester fit into this then? ⚗️

Hopefully that made sense so far. Let’s move onto the really juicy stuff now.

With Pester we can now write a unit test to make sure that if an Azure Bicep module exists in the modules/ folder that a corresponding .tests.bicep file must also exist within the .tests/ folder location. If it does not then it will fail our Azure DevOps build validation pipeline.

Lets check out the Pester test:

# Create Pester container https://pester.dev/docs/commands/New-PesterContainer
# Location of the Pester Unit test script location (example)
$container = New-PesterContainer -Path './.scripts/bicep-module-tests.ps1'
# Set config values and results
$config = New-PesterConfiguration
$config.TestResult.OutputFormat = "NUnitXML"
$config.TestResult.OutputPath = "test-Pester.xml"
$config.TestResult.Enabled = $True
$config.Run.Container = $container
# Invoke Pester run
Invoke-Pester -Configuration $config
BeforeDiscovery {
# Change the current directory to the Bicep module folder (example)
Set-Location -Path "bicep\modules"
# Define the module and test paths
$testPath = "/home/vsts/work/1/s/bicep/modules/.tests"
$moduleFiles = Get-ChildItem -Filter *.bicep
$testFiles = foreach ($moduleFile in $moduleFiles) {
Join-Path $testPath ($moduleFile.BaseName + '.tests.bicep')
}
}
Describe "test file" -ForEach $testFiles {
# Check if a corresponding .tests.bicep file exists for each .bicep module file
It "$_ should exist" {
$_ | Should -Exist
}
}

The top PowerShell script is creating the Pester container to run our configuration values from, including calling the unit test which is the second PowerShell script. In the second script, the actual Pester unit test we are:

  • Piping the test module folder to a variable
  • Collecting all available .bicep module files within the modules folder
  • Looping through each module file name and joining the paths so we get a list of module.tests.bicep file names based on the modules folder as our source of truth
  • Lastly, using Pester to confirm if the file path returns a value of true

Amend any file paths and/or file extension names as you see fit for your use case.

Azure DevOps pipeline 🪄

Incorporating this into your Azure DevOps build validation pipeline is simple and you can also publish the unit tests as an .XML to review the results directly in the tests tab within the ADO GUI which lends to a nice reporting output.

Here’s an example job that would call the Pester test files to run and publish the results as part of your build validation:

trigger: none
variables:
vmImageName: ubuntu-latest
azureServiceConnection: YOUR_ARM_CONNECTION
pool:
vmImage: $(vmImageName)
stages:
– stage: Validation
displayName: Validation ✅
jobs:
– job: 'PesterTests'
displayName: 'Pester Tests 🧪'
steps:
– task: PowerShell@2
displayName: "Azure Bicep module Pester tests"
inputs:
filePath: './.scripts/bicep-module-pester.ps1'
– task: PublishTestResults@2
displayName: 'Publish Pester results'
condition: always()
inputs:
testResultsFormat: "NUnit"
testResultsFiles: "**/test-Pester.xml"
failTaskOnFailedTests: true
testRunTitle: "Validate Bicep module tests file"

In my example pipeline run, we can see it found 25 modules and tested to make sure the .tests.bicep file path returned true for the .tests folder.

unit tests with Azure Bicep module output
Pipeline overview unit tests with Azure Bicep modules

If the test had failed we would get a test result published to the tests tab in the pipeline overview in Azure DevOps telling us what files should exist, for example:

unit tests with Azure Bicep module  pipeline Published result overview

This result means the Pester unit test found a module called roleAssignmentResourceGroupMany.bicep but could not find a corresponding roleAssignmentResourceGroupMany.tests.bicep file in the .tests folder, therefore it failed the Bicep unit test.

Conclusion 🏁

Thanks if you made it this far!

This post was just a reference/example solution that I hope the Azure Bicep community can take inspiration from and build on. The scripts and YAML examples are just that, so feel free to amend / customise / enhance as you see fit for your specific use cases.

For me, this really adds a nice layer to the CI pipeline process to enforce the standards around having unit tests with Azure Bicep module files for unit testing with PSRule and Pester.

If you thought this was useful, or have implemented something similar I’d love to hear in the comments.

Leave a comment


Skip to content