Skip to content

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-Pester on 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/ or scratch/ if they exist at all