Chapter 18Lesson 02~225 minutes

Mocks, TestDrive, Fixtures, and Testing Side Effects

Isolate PowerShell side effects with Pester 6 mocks, TestDrive, fixtures, deterministic dependencies, platform-conditional tests, and meaningful interaction verification.

MocksTestDriveFixturesDeterminism

Learning objectives

  • Explain dependency isolation and why production changes do not belong in unit tests.
  • Use TestDrive for disposable filesystem behavior.
  • Use Pester 6 Mock and Should-Invoke for controlled command dependencies.
  • Account for the Pester 6 no-fall-through mocking change.
  • Create deterministic fixtures and inject clocks or clients.
  • Test a file/API wrapper without contacting a real production service.

1. A side effect is a change or dependency outside the function result

Reading or writing a file, calling an HTTP service, querying the clock, generating randomness, changing an environment variable, or restarting a service all cross a boundary outside pure computation. These side effects are often necessary in DevOps, but unit tests should not casually exercise the real production boundary.

Dependency isolation means replacing a real dependency with a controlled test substitute so the test can focus on your code's behavior. Integration tests can exercise the real dependency later in a disposable environment.

A side effect is a change or dependency outside the function result
flowchart LR T[Test] --> L[Your logic] L --> B{Dependency boundary} B -->|Unit test| F[Fake / Mock / TestDrive] B -->|Integration test| R[Real disposable dependency]

2. TestDrive gives file tests an isolated filesystem location

Pester's TestDrive: is a temporary PowerShell drive intended for test file activity. Use it when the behavior under test genuinely needs filesystem semantics. The test can create, read, and remove files without choosing a production path.

Describe 'file behavior with TestDrive' {
    It 'writes a report only inside the test drive' {
        $path = Join-Path 'TestDrive:' 'inventory.json'
        '{"status":"healthy"}' | Set-Content -LiteralPath $path

        (Test-Path -LiteralPath $path) | Should-BeTrue
        (Get-Content -LiteralPath $path -Raw) | Should-MatchString 'healthy'
    }
}

Use TestDrive for real file semantics. Do not mock every filesystem call automatically; a mock is useful when the file operation itself is not what you are trying to test.

3. A mock replaces a command implementation inside the test

Pester's Mock intercepts a PowerShell command and supplies controlled behavior. A mock can return deterministic data, throw a chosen error, or do nothing. This prevents a unit test from contacting a real API or changing a real resource.

BeforeAll {
    function Get-ApplicationStatus {
        param([Parameter(Mandatory)][uri]$Uri)
        Invoke-RestMethod -Uri $Uri -Method Get
    }
}

Describe 'Get-ApplicationStatus' {
    It 'returns the API response as an object' {
        Mock Invoke-RestMethod {
            [pscustomobject]@{ name='api'; healthy=$true }
        }

        $result = Get-ApplicationStatus -Uri 'https://unit.test/status'

        $result.name | Should-Be 'api'
        $result.healthy | Should-BeTrue
    }
}

The URL is deliberately non-production and the mock prevents the network command from running. The test validates your wrapper's contract, not the availability of the internet.

4. Should-Invoke verifies meaningful interaction with a mocked dependency

Sometimes the behavior under test is not only the returned value but also how a dependency was called. Pester 6 provides Should-Invoke to assert call count and parameter filters. Use this sparingly: tests that specify every internal call become brittle implementation mirrors.

Describe 'Get-ApplicationStatus interaction' {
    It 'requests the expected URI exactly once' {
        Mock Invoke-RestMethod { [pscustomobject]@{ healthy=$true } }

        $null = Get-ApplicationStatus -Uri 'https://unit.test/status'

        Should-Invoke Invoke-RestMethod -Times 1 -Exactly -ParameterFilter {
            $Uri -eq 'https://unit.test/status' -and $Method -eq 'Get'
        }
    }
}

Prefer verifying externally meaningful interactions—such as “one publish request with the expected target”—rather than asserting incidental helper calls that could change during a harmless refactor.

5. Pester 6 does not silently fall through unmatched parameterized mocks

This is an important Pester 6 change. If you define only parameter-filtered mocks and a call matches none of them, Pester 6 does not silently execute the real command. The call fails unless a default unfiltered mock exists. This reduces the risk that a test accidentally reaches production because a filter was incomplete.

BeforeAll {
    function Get-Node { param([int]$Id) "real:$Id" }
}

Describe 'Pester 6 mock selection' {
    It 'uses a specific mock and a safe default' {
        Mock Get-Node { 'default' }
        Mock Get-Node { 'one' } -ParameterFilter { $Id -eq 1 }

        (Get-Node -Id 1) | Should-Be 'one'
        (Get-Node -Id 2) | Should-Be 'default'
    }
}
Safety consequence: for dangerous dependencies, a default mock that throws can make unexpected calls fail loudly instead of performing real work.

6. Fixtures are known test data, not mysterious shared state

A fixture is controlled input or setup data used by tests. Good fixtures are small, readable, deterministic, and close enough to production shape to be meaningful. Avoid giant copied production dumps containing secrets or unstable fields.

$fixture = @(
    [pscustomobject]@{ Name='api';    Environment='prod'; Replicas=3 },
    [pscustomobject]@{ Name='worker'; Environment='prod'; Replicas=2 }
)

Describe 'inventory fixture' {
    It 'contains two production applications' {
        $fixture | Should-BeCollection -Count 2
        ($fixture.Environment | Select-Object -Unique) | Should-Be 'prod'
    }
}

For larger fixtures, store sanitized JSON/CSV under a tests/fixtures directory and parse it exactly as production code would.

7. Inject clocks, randomness, and network clients when time or chance matters

A test becomes flaky when it can pass or fail without a code change. Current time, randomness, and network availability are common causes. Instead of hiding these dependencies inside the function, pass them in so the test can supply deterministic behavior.

function New-AuditRecord {
    param(
        [string]$Action,
        [scriptblock]$Clock = { [datetime]::UtcNow }
    )
    [pscustomobject]@{
        Action = $Action
        Time   = & $Clock
    }
}

Describe 'New-AuditRecord' {
    It 'uses the injected clock' {
        $fixedClock = { [datetime]'2026-08-11T12:00:00Z' }
        $record = New-AuditRecord -Action 'deploy' -Clock $fixedClock

        $record.Time | Should-Be ([datetime]'2026-08-11T12:00:00Z')
    }
}

The same pattern works for HTTP clients, GUID factories, random-number sources, and path providers. This is a first taste of dependency injection; Lesson 5 develops it into an architecture technique.

8. Platform-specific behavior should be labeled, not accidentally executed everywhere

If a function intentionally has a Windows-only branch, a cross-platform suite should make that condition explicit. A skipped test documents unsupported context; it is not the same as silently omitting the test.

Describe 'platform-specific adapter' {
    It 'runs this Windows-only assertion only on Windows' -Skip:(-not $IsWindows) {
        $IsWindows | Should-BeTrue
    }

    It 'keeps the shared transformation cross-platform' {
        ("API".ToLowerInvariant()) | Should-Be 'api'
    }
}

Where practical, keep most logic platform-neutral and isolate OS-specific commands behind small adapters. Then the majority of behavior can be unit-tested on every supported runner.

9. Lab — test a file/API wrapper with local and fake dependencies

The function below obtains inventory data, serializes it, and writes a report. The unit test mocks the network dependency and writes only to TestDrive:. The production endpoint is never contacted.

BeforeAll {
    function Save-InventoryReport {
        param([uri]$Uri,[string]$Path)
        $data = Invoke-RestMethod -Uri $Uri -Method Get
        $data | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $Path
        Get-Item -LiteralPath $Path
    }
}

Describe 'Save-InventoryReport' {
    It 'writes mocked inventory into the isolated test drive' {
        Mock Invoke-RestMethod {
            @(
                [pscustomobject]@{name='api';healthy=$true},
                [pscustomobject]@{name='worker';healthy=$true}
            )
        }

        $path = Join-Path 'TestDrive:' 'inventory.json'
        $file = Save-InventoryReport -Uri 'https://unit.test/inventory' -Path $path

        $file.Exists | Should-BeTrue
        $parsed = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json
        $parsed | Should-BeCollection -Count 2
        Should-Invoke Invoke-RestMethod -Times 1 -Exactly
    }
}

This test allows the filesystem boundary because TestDrive makes it disposable, while mocking the network because a real remote call would make a unit test slow and nondeterministic.

10. Common isolation mistakes

MistakeRiskBetter pattern
Mock every commandTests verify implementation plumbing instead of behavior.Mock only true external boundaries.
Use production credentials in testsSecrets leak and tests can modify real systems.Use fakes, TestDrive, local emulators, or dedicated disposable accounts.
Rely on current time/random valuesFlaky assertions.Inject a clock/random source.
Use only parameter-filtered mocks in v6Unexpected calls fail without a default; old fall-through assumptions break.Provide an intentional default mock and specific overrides.
Ignore OS differencesCross-platform runners fail mysteriously.Isolate adapters and use explicit conditional tests.

11. Verification checklist

  • You can explain why unit tests isolate production side effects.
  • You can use TestDrive for disposable filesystem behavior.
  • You can use Mock and Should-Invoke without overcoupling tests to internal details.
  • You understand Pester 6's no-fall-through behavior for unmatched parameterized mocks.
  • You can inject deterministic clocks or clients.
  • You can label platform-specific tests explicitly.

12. Knowledge check

Question 1. When should you prefer TestDrive over mocking Set-Content?

Question 2. What does a Pester Mock do?

Question 3. What important mock behavior changed in Pester 6?

Question 4. Why inject a clock into time-sensitive code?

Question 5. Should a unit test use a real production API token?

13. Summary and next bridge

Side-effect testing is about choosing the boundary deliberately. Use TestDrive when you want real file semantics, mocks when you want to isolate a command, fixtures for known data, and injected dependencies for clocks or clients. The next lesson adds a second quality channel: static analysis, which can flag risky patterns without executing every possible path.

14. Authoritative references

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.