Skip to main content

A function that writes files

Every function so far has returned a value. Testing those is easy: call it, look at what came back. This page adds a function whose entire purpose is a side effect — it writes a file and returns nothing useful.

Export-PlanetReport

Planetarium/Public/Export-PlanetReport.ps1
function Export-PlanetReport {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string] $Path,

[string] $Name = '*'
)

$planets = @(Get-Planet -Name $Name)

if ($planets.Count -eq 0) {
throw "No planets matched '$Name'."
}

$report = foreach ($planet in $planets) {
'{0,-8} {1} AU' -f $planet.Name, (ConvertTo-AstronomicalUnit -Kilometre $planet.DistanceFromSunKm)
}

Set-Content -Path $Path -Value $report
}

It leans on almost everything built so far: Get-Planet for the data, the private ConvertTo-AstronomicalUnit for the conversion, and the module loader to wire them together. It goes in Public/, so it is exported automatically.

(The @( ) around Get-Planet is a PowerShell detail, not a Pester one: it guarantees .Count works even when a single planet comes back.)

Try it:

Import-Module ./Planetarium/Planetarium.psd1 -Force
Export-PlanetReport -Path ./report.txt
Get-Content ./report.txt
Mercury 0.387 AU
Venus 0.723 AU
Earth 1 AU
Mars 1.524 AU
Jupiter 5.204 AU
Saturn 9.583 AU
Uranus 19.201 AU
Neptune 30.048 AU

The problem with testing this

You have just created a file in your source folder. Delete it:

Remove-Item ./report.txt

Now think about what a test for this function has to do. It needs a path to write to, and afterwards that file should not still be there. Doing this by hand goes wrong quickly:

  • Writing into your source folder leaves junk next to your code, and one forgotten cleanup means a test that passes only because a previous run left the file behind.
  • Writing to a fixed temp path breaks the moment two tests use the same name, and breaks harder when two Pester runs happen at once.
  • Cleaning up in AfterEach is bookkeeping you have to maintain — every file every test creates must be remembered and removed, and an interrupted run still leaves debris for the next one.

Every one of these produces the same nasty class of bug: tests whose result depends on what previous runs left lying around. They pass on your machine, fail in CI, and pass again after you delete something by hand.

Could you just mock Set-Content?

You could — Mock Set-Content and Should-Invoke would tell you the function tried to write. But it would not tell you the file has eight lines, or that Earth's line reads Earth 1 AU. You would be asserting that your code called a command, not that it produced a correct report.

Mock the filesystem when the write itself is the behaviour. When the content matters, write real files somewhere disposable — which is exactly what the next page is about.

Before you move on

0/3

Next: the disposable filesystem Pester gives you for free.