Skip to content
Updated 6 min read

Safe File and Registry Tests with TestDrive and TestRegistry

  • powershell
  • pester
  • testing
  • testdrive
  • testregistry
  • filesystem

📦 Run it yourself: download the companion code pack; this part's examples are in the 04-testing-real-code/ folder (Pester 5-verified).

Plenty of useful PowerShell touches the world: it writes a config file, drops a log, sets a registry value. You want to test that behavior, but you don't want a test run littering your disk or, worse, clobbering a real file. Pester gives you a disposable sandbox so your side-effecting code can run safely and clean up after itself.

What you'll learn

  • What TestDrive: is and how it auto-cleans
  • How to write and read files under the sandbox
  • Using the $TestDrive variable to build paths
  • TestRegistry: for code that touches the Windows registry
  • Why the sandbox beats temp folders you manage yourself

Meet TestDrive:

TestDrive: is a PSDrive that Pester creates for you, backed by a fresh temporary folder. You read and write under it exactly like any other path: TestDrive:\config.json, TestDrive:/logs/app.log, and so on. The magic is the lifecycle: Pester makes it before your tests run and deletes it automatically afterward. You never write cleanup code, and nothing leaks onto your real disk.

# TestDriveBasics.Tests.ps1
Describe 'TestDrive basics' {
    It 'starts empty and accepts a file' {
        $path = 'TestDrive:\hello.txt'
        Set-Content -Path $path -Value 'hi'
        Get-Content -Path $path | Should -Be 'hi'
    }
}

Files you write stay put for the rest of the Describe (or Context) they were written in, so a later It in the same block can still see them. When that block ends, Pester removes them, the next Describe starts with an empty drive, and the whole drive is gone once the run ends.

Testing a function that writes a file

Here is a small function that saves settings to disk. Notice it takes the target path as a parameter. That one design choice is what makes it testable.

function Save-AppConfig {
    param(
        [Parameter(Mandatory)][string]$Path,
        [Parameter(Mandatory)][hashtable]$Settings
    )
    $Settings | ConvertTo-Json | Set-Content -Path $Path -Encoding UTF8
}

The test points it straight at the sandbox:

# Save-AppConfig.Tests.ps1
Describe 'Save-AppConfig' {
    BeforeAll {
        function Save-AppConfig {
            param(
                [Parameter(Mandatory)][string]$Path,
                [Parameter(Mandatory)][hashtable]$Settings
            )
            $Settings | ConvertTo-Json | Set-Content -Path $Path -Encoding UTF8
        }
    }

    It 'writes the settings as JSON' {
        $configPath = 'TestDrive:\config.json'

        Save-AppConfig -Path $configPath -Settings @{ Theme = 'Dark'; Retries = 3 }

        Test-Path $configPath | Should -BeTrue
        $loaded = Get-Content $configPath -Raw | ConvertFrom-Json
        $loaded.Theme   | Should -Be 'Dark'
        $loaded.Retries | Should -Be 3
    }
}

The file is created, read back, asserted on, and then it vanishes when the run finishes. Run it twice in a row and the second run starts just as clean as the first.

The $TestDrive variable

The TestDrive: drive string is great for PowerShell-native cmdlets like Get-Content and Test-Path. But some commands (and most .NET methods) do not understand PSDrive paths. For those you need a real filesystem path, and that is what the $TestDrive variable gives you: the actual folder on disk behind the drive.

# TestDrivePath.Tests.ps1
Describe 'Using $TestDrive for a real path' {
    It 'builds a nested path that .NET can use' {
        $folder = Join-Path $TestDrive 'logs'
        New-Item -ItemType Directory -Path $folder | Out-Null

        $file = Join-Path $folder 'app.log'
        [System.IO.File]::WriteAllText($file, 'started')

        [System.IO.File]::ReadAllText($file) | Should -Be 'started'
    }
}

Quick guide: use the TestDrive: string with PowerShell cmdlets, and the $TestDrive variable with Join-Path, .NET methods, or anything that needs a plain path.

TestRegistry: for registry code (Windows)

If your code reads or writes the registry, TestRegistry: is the same idea applied to a throwaway registry key. Pester creates a temporary key, exposes it as the TestRegistry: drive, and removes it when the run ends, so you can test registry logic without poisoning HKCU or HKLM. This is Windows-only; the drive does not exist on Linux or macOS.

# RegistryWrite.Tests.ps1
Describe 'Registry write' -Skip:($IsWindows -eq $false) {
    It 'stores and reads back a value' {
        New-Item -Path 'TestRegistry:\MyApp' | Out-Null
        Set-ItemProperty -Path 'TestRegistry:\MyApp' -Name 'Version' -Value '2.0'

        $value = Get-ItemProperty -Path 'TestRegistry:\MyApp' -Name 'Version'
        $value.Version | Should -Be '2.0'
    }
}

The -Skip:($IsWindows -eq $false) guard skips the test only on Linux and macOS. PowerShell 7 sets $IsWindows to $true or $false. Windows PowerShell 5.1 doesn't define it at all, and $null -eq $false is $false, so on 5.1 (which only runs on Windows) the test runs. Avoid the shorter -Skip:(-not $IsWindows): on 5.1, -not $null is $true, so it skips the registry test on the one platform that always has a registry.

Why not just use a temp folder?

You could write to $env:TEMP\my-test-$(New-Guid) and delete it in AfterAll. People do, and it goes wrong in familiar ways: a failing test skips the cleanup and leaves junk behind; two parallel runs collide; the cleanup code itself has a bug. TestDrive: and TestRegistry: hand all of that to Pester: fresh sandbox, guaranteed teardown, even when a test blows up.

Try it yourself

Write a function Write-Log that takes a -Path and a -Message and appends a timestamped line to the file. Then write a test that calls it with a TestDrive: path, asserts the file exists with Test-Path ... | Should -BeTrue, reads it back with Get-Content, and asserts the message is present with Should -Match. Confirm nothing lands in your repo or $env:TEMP.

Common mistakes

Writing artifacts to the repo or $env:TEMP. Hard-coded real paths pollute your disk and make tests order-dependent. Send all test output to TestDrive: and let Pester clean up.

Assuming TestDrive persists across files. Files written inside a Describe or Context are removed when that block ends, and the whole drive is deleted after the run. Never use it to pass data from one test file to another.

Forgetting TestRegistry: is Windows-only. It does not exist on Linux or macOS. Guard registry tests with -Skip:($IsWindows -eq $false) so cross-platform runs stay green and Windows PowerShell 5.1 still runs them.

Recap

TestDrive: and $TestDrive let you exercise file-writing code in a self-cleaning sandbox; TestRegistry: does the same for Windows registry code. Use the drive string with cmdlets and the variable for real paths, and let Pester handle teardown so failures never leave a mess. Next we tackle the copy-paste smell: five nearly identical tests that differ only in their numbers.

Next up: Part 15, Data-Driven Tests with -ForEach and -TestCases.

Posts here are written with AI assistance and tested against real code before publishing.

Comments