Unit Testing Bicep Logic with BicepConsoleTTK

Problem Space

In most Infrastructure as Code teams, Bicep quality checks start to look mature as soon as linting and deployment validation are in place. In practice, there is still a blind spot: logic-level testing of exported functions, types, and variables.

Most teams validate by deploying to a subscription and checking outcomes afterwards. That is useful, but it is also slow and expensive when what you actually want to verify is pure logic. A shared Bicep library changes, everything still compiles, and then a downstream module fails later in a deployment pipeline because a naming function or constructor behaviour subtly changed. That is exactly the type of issue we normally catch early in application development with unit tests.

If you maintain shared Bicep libraries, this gap hurts quickly:

  • Naming functions drift without anyone noticing
  • Type constructors change and break downstream modules
  • Refactors feel risky because feedback loops are too long

I wanted a way to unit test Bicep logic directly, without deploying anything.

Introducing BicepConsoleTTK

To solve that problem, I created BicepConsoleTTK (Bicep Console Test Tool Kit).

It is a Pester-based framework that executes Bicep expressions through the Bicep console REPL, so you can assert outputs in fast, repeatable unit tests.

Repository:

GitHub Repository PowerShell Gallery

At a high level, the toolkit gives you two commands:

  • Import-Bicep: reads one or more Bicep files and extracts the exports you ask for
  • Invoke-BicepExpression: runs those declarations plus your expression in bicep console and returns the result

Why This Approach Works

This lets you test Bicep logic in isolation, before template deployment.

Practical outcomes:

  • Faster feedback during development
  • More confidence when refactoring shared functions
  • Cleaner CI pipelines for template libraries
  • Better separation between unit tests (logic) and integration tests (deployment)

Features

  • Familiar import syntax (named imports and wildcard imports)
  • Multi-file import composition with preserved order
  • Deduplication when the same member is imported multiple ways
  • Setup declarations for multi-step scenarios
  • Pipeline input support
  • Cleaner Bicep console error reporting
  • CI/CD-friendly, non-interactive execution

Prerequisites

  • PowerShell 5.1 (Desktop) or 7+
  • Pester 5.x
  • Bicep CLI 0.42.1+
  • bicep available on your PATH

Installation

PowerShell Gallery

Install-Module -Name BicepConsoleTTK -Repository PSGallery

In your test file:

BeforeAll {
	Import-Module BicepConsoleTTK -Force
}

From Source

git clone https://github.com/Andrew-D-Wilson/bicep-console-test-framework.git
Import-Module "$PSScriptRoot/../src/BicepConsoleTTK" -Force

AI-Led Test Authoring with Instructions

One thing that has worked well for me is combining BicepConsoleTTK with AI-assisted test generation in VS Code.

I maintain a dedicated instructions file:

This gives Copilot a clear contract for how tests should be authored for this toolkit, including:

  • Correct BeforeAll module import patterns
  • Import-Bicep usage for named, wildcard, and multi-file imports
  • Invoke-BicepExpression patterns with aliases and setup declarations
  • Expected result formatting for strings, objects, arrays, and error assertions
  • REPL limitations to avoid (for example deployment-time functions)

To use it in your own repository:

  1. Copy the file to .github/instructions/bicepconsolettk.instructions.md
  2. Keep applyTo: "**/*.Tests.ps1" so it is scoped to Pester test files
  3. Ask Copilot to generate or refactor tests in your *.Tests.ps1 files

The result is faster test authoring while still staying aligned to the conventions and edge cases that matter for BicepConsoleTTK.

Usage

1. Import exports from Bicep files

$imports = Import-Bicep @(
	"import {coreParams, newCoreParams} from '$PSScriptRoot/../shared/Types.bicep'",
	"import {basicResource}             from '$PSScriptRoot/../shared/NamingFunctions.bicep'"
)

2. Evaluate an expression

$result = Invoke-BicepExpression -b $imports -e "newCoreParams('uksouth', 'uks', 'prod', 'myapp')"

3. Use setup declarations when needed

$setupDeclarations = @(
	"var projectNameStart = 'hello'",
	"var projectNameComplete = '`${projectNameStart}world'",
	"var coreParameters coreParams = newCoreParams('uksouth', 'uks', 'dev', projectNameComplete)"
)

$result = Invoke-BicepExpression -b $imports -s $setupDeclarations -e "basicResource('aks', coreParameters)"

Example Pester Test

BeforeAll {
	Import-Module BicepConsoleTTK -Force
}

Describe "Naming Functions" {
	BeforeAll {
		$script:imports = Import-Bicep @(
			"import {coreParams, newCoreParams} from '$PSScriptRoot/../shared/Types.bicep'",
			"import {basicResource, csResource} from '$PSScriptRoot/../shared/NamingFunctions.bicep'"
		)
	}

	It "basicResource includes abbreviation, project, environment and location" {
		$result = Invoke-BicepExpression -b $script:imports `
			-e "basicResource('aks', newCoreParams('uksouth', 'uks', 'dev', 'myapp'))"

		$result | Should -Be "'aks-myapp-dev-uksouth'"
	}
}

Run tests with:

Invoke-Pester -Path ./tests

How It Works (Short Version)

Import-Bicep parses import strings, resolves file paths, and extracts exported declarations from source files.

Invoke-BicepExpression sends those declarations and your expression into bicep console, captures output, and translates console noise into readable exceptions when failures occur.

That gives you deterministic, fast unit tests focused on logic instead of deployment orchestration.

Where This Fits in Your Testing Strategy

Use BicepConsoleTTK for:

  • Function output verification
  • Naming convention enforcement
  • Shared type constructor validation
  • Regression checks during refactoring

Still keep deployment/integration tests for:

  • Resource runtime behavior
  • Policy and RBAC interactions
  • End-to-end environment validation

Both layers matter. This tool improves the unit-testing layer.

In Short

BicepConsoleTTK helps you test Bicep exports the same way you test application code: quickly, repeatedly, and early.

If your team relies on shared Bicep libraries, this can remove a lot of friction from your delivery pipeline while increasing confidence in every change.

For the original version of this post see Andrew Wilson's personal blog at Unit Testing Bicep Logic with BicepConsoleTTK