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:
At a high level, the toolkit gives you two commands:
Import-Bicep: reads one or more Bicep files and extracts the exports you ask forInvoke-BicepExpression: runs those declarations plus your expression inbicep consoleand 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+
bicepavailable on yourPATH
Installation
From 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
BeforeAllmodule import patterns Import-Bicepusage for named, wildcard, and multi-file importsInvoke-BicepExpressionpatterns 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:
- Copy the file to
.github/instructions/bicepconsolettk.instructions.md - Keep
applyTo: "**/*.Tests.ps1"so it is scoped to Pester test files - Ask Copilot to generate or refactor tests in your
*.Tests.ps1files
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.
