Testing standard¶
All HCS projects that ship executable code or automation scripts require tests. This document defines the test classes, required files, frameworks, and enforcement model.
Test classification¶
Every test falls into exactly one of these classes. The class determines where it lives, what runs it, and what a failure means.
| Class | Purpose | Location | Framework |
|---|---|---|---|
| unit | Exercise a single function or cmdlet in isolation. No external state. | tests/unit/ |
Pester 5 (PowerShell) |
| contract | Assert the shape of a deployed resource or environment against a fixture. Fixture is authoritative — code conforms to it. | tests/contract/ |
Pester 5 + custom fixture schema |
| integration | Exercise module + live dependency (real Azure tenant, real AD, real cluster). Requires a provisioned lab. | tests/integration/ |
Pester 5 with lab config |
| scenario | Scripted end-to-end walkthrough of a user journey with pass/fail gates at each step. | tests/scenario/ |
PowerShell with structured assertion output |
| drift-audit | Assert a live environment still matches its contract fixture after time passes. Runs on a schedule. | tests/drift/ |
Pester 5 + scheduled pipeline |
Contract testing¶
Contract tests assert that a deployed environment (Azure resources, cluster configuration, network topology) matches its expected shape as defined in a fixture file. The fixture is the authoritative definition — the deployment must conform to it, not the other way around.
Required for every project that provisions or manages infrastructure.
Fixture files¶
One JSON fixture per logical target. The fixture describes expected state:
tests/contract/
├── fixtures/
│ ├── cluster-01.json # HCI cluster shape
│ ├── network-topology.json # VNets, subnets, NSGs
│ └── identity.json # AD structure, SPNs, managed identities
└── cluster-01.Tests.ps1 # Pester test that loads fixture and asserts conformance
Fixture structure¶
{
"$schema": "./schema/fixture.schema.json",
"target": "hcs-cluster-01",
"environment": "prd",
"assertions": {
"cluster_nodes": 3,
"cluster_name": "hcs-cluster-01",
"domain": "corp.hybridsolutions.cloud",
"dns_servers": ["10.0.0.10", "10.0.0.11"],
"ntp_server": "0.pool.ntp.org"
}
}
Example contract test¶
Describe 'hcs-cluster-01 contract' {
BeforeAll {
$fixture = Get-Content "$PSScriptRoot/fixtures/cluster-01.json" | ConvertFrom-Json
$actual = Get-HCSClusterState -ClusterName $fixture.target
}
It 'has the expected number of nodes' {
$actual.NodeCount | Should -Be $fixture.assertions.cluster_nodes
}
It 'is joined to the correct domain' {
$actual.Domain | Should -Be $fixture.assertions.domain
}
It 'has correct DNS servers configured' {
$actual.DnsServers | Should -Be $fixture.assertions.dns_servers
}
}
Scenario testing¶
Scenario tests are executable walkthroughs of a user journey end-to-end. Each step has an expected outcome. The scenario passes if every step's gate passes.
Required for every project that ships a user-facing workflow — provisioning, migration, deployment, handover.
Scenario file structure¶
tests/scenario/
├── deploy-cluster.scenario.ps1
├── configure-network.scenario.ps1
└── expected/
├── deploy-cluster.expected.json
└── configure-network.expected.json
Scenario naming¶
Scenario filenames describe the user journey, not the implementation:
deploy-cluster.scenario.ps1 # Good — describes what the user does
test-deploy-cluster.ps1 # Bad — generic test name
Example scenario structure¶
<#
.SYNOPSIS
Scenario: deploy a new HCI cluster from config.
.DESCRIPTION
Steps: load config → validate prerequisites → deploy nodes → configure OS → validate cluster.
Each step has a named gate. Failure stops the scenario and reports which gate failed.
#>
$scenarioName = 'deploy-cluster'
$gates = @()
# Step 1 — load config
$step = 'load-config'
try {
$cfg = Get-Content 'config/variables.yml' -Raw | ConvertFrom-Yaml
$gates += [PSCustomObject]@{ Step = $step; Result = 'PASS'; Detail = "Loaded $($cfg.compute.azure_local.cluster_name)" }
} catch {
$gates += [PSCustomObject]@{ Step = $step; Result = 'FAIL'; Detail = $_.Exception.Message }
$gates | ConvertTo-Json | Out-File "tests/scenario/results/$scenarioName.results.json"
throw "Scenario stopped at gate: $step"
}
# ... additional steps ...
$gates | ConvertTo-Json -Depth 5 | Out-File "tests/scenario/results/$scenarioName.results.json"
Write-Host "Scenario '$scenarioName' completed. Results: tests/scenario/results/$scenarioName.results.json" -ForegroundColor Green
Unit tests¶
All PowerShell modules and shared functions must have unit tests using Pester 5.
tests/unit/
├── Get-HCSVaultSecret.Tests.ps1
├── Resolve-KeyVaultRef.Tests.ps1
└── Test-HCSPrerequisites.Tests.ps1
Minimum requirements:
- One test file per module function or cmdlet
- Test the happy path and at least one failure path
- Tests must run without Azure credentials — mock external calls with
Mock - Tests must complete in under 5 seconds per file
Describe 'Resolve-KeyVaultRef' {
BeforeAll {
. "$PSScriptRoot/../../scripts/helpers.ps1"
}
It 'returns null for non-URI input' {
Resolve-KeyVaultRef -KvUri 'plain-string' | Should -BeNullOrEmpty
}
It 'parses vault name and secret name from URI' {
Mock -CommandName 'az' -MockWith { return 'mock-secret-value' }
$result = Resolve-KeyVaultRef -KvUri 'keyvault://kv-hcs-vault-01/my-secret'
$result | Should -Be 'mock-secret-value'
}
}
What every repo must have¶
A conforming HCS repo has, at minimum:
- [ ]
tests/directory at repo root - [ ]
tests/unit/with at least one Pester test per shipped module function - [ ]
tests/contract/fixtures/with at least one fixture if the repo provisions infrastructure - [ ]
tests/scenario/with at least one scenario if the repo ships a user-facing workflow - [ ] ADO pipeline stage that runs
Invoke-Pesteron every PR - [ ] Scheduled ADO pipeline that re-runs contract tests against live environments weekly
Repos that are purely documentation are exempt from contract and scenario requirements but must have unit tests for any embedded scripts.
CI enforcement¶
Add a test stage to every ADO pipeline that runs on PRs:
- stage: Test
jobs:
- job: UnitTests
pool:
vmImage: windows-latest
steps:
- task: PowerShell@2
displayName: Run Pester tests
inputs:
targetType: inline
script: |
Install-Module Pester -Force -SkipPublisherCheck
$results = Invoke-Pester -Path tests/unit/ -PassThru -Output Detailed
if ($results.FailedCount -gt 0) { exit 1 }
What does not belong in tests/¶
- Copies of shared assertion helpers — if two repos need the same helper, extract it into a shared module in this (platform) repo
- Live credentials or environment-specific connection strings — these live in ADO Variable Groups and are injected by the pipeline
- Exploratory test scripts not tied to a specific assertion — those belong in
scripts/orscratch/if they exist at all