Skip to main content

Running on every push

The test script is the hard part and it is already done. A CI workflow is mostly plumbing: check out the code, install Pester, run ./test.ps1, keep the artifacts.

The workflow

.github/workflows/test.yml
name: Test

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- name: Install Pester
shell: pwsh
run: Install-Module Pester -MinimumVersion 6.0.0 -Force -SkipPublisherCheck -Scope CurrentUser

- name: Run tests
shell: pwsh
run: ./test.ps1

- name: Upload results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results
path: |
testResults.xml
coverage.xml
This part requires git and GitHub

Everything so far ran locally; running in CI means publishing the code to GitHub. If your working folder is not a git repository yet, turn it into one and commit your files:

git init
git add .
git commit -m "Planetarium module with tests"

Then create a GitHub repository and push to it. If you have never used git, that guide covers everything this module needs.

Commit that and push. Every push to main and every pull request now runs your 26 tests.

Three details are doing real work.

shell: pwsh on every PowerShell step. The default shell on ubuntu-latest is bash, and PowerShell 7 is preinstalled but not the default. Omit this and the step tries to run PowerShell as bash. On Windows runners the default is powershell — Windows PowerShell 5.1 — which is a different shell from pwsh, so being explicit avoids a second, subtler version of the same problem.

-Scope CurrentUser on the install. The runner user is not an administrator, and an all-users install needs elevation. -SkipPublisherCheck is there for the Windows runner, which ships the Microsoft-signed Pester 3.4.0 you met in the prerequisites — without it, installing over a module signed by a different publisher is refused.

if: always() on the upload. Without it, the artifact step is skipped whenever a previous step fails — which is precisely when you most want the test results. This one line is the difference between a red build you can diagnose from the artifact and one you have to reproduce locally.

Testing on more than one platform

The single job above tests less than the module claims to support. The tutorial has worked on Windows, Linux and macOS all along, and the manifest says PowerShellVersion = '5.1' — Windows PowerShell 5.1 is a genuinely different engine from PowerShell 7, and it is where the surprises live. A matrix runs the same job once per combination, so extend the workflow to test all of it:

.github/workflows/test.yml
jobs:
test:
name: ${{ matrix.name }} on ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
shell: [pwsh]
name: [PowerShell 7]
include:
- os: windows-latest
shell: powershell
name: Windows PowerShell 5.1

runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v6

- name: Install Pester
shell: pwsh
shell: ${{ matrix.shell }}
run: Install-Module Pester -MinimumVersion 6.0.0 -Force -SkipPublisherCheck -Scope CurrentUser

- name: Run tests
shell: pwsh
shell: ${{ matrix.shell }}
run: ./test.ps1

- name: Upload results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results
name: test-results-${{ matrix.os }}-${{ matrix.shell }}
path: |
testResults.xml
coverage.xml

This produces four jobs. The os list crossed with shell: [pwsh] gives three — PowerShell 7 on each operating system. The include adds a fourth: windows-latest running under powershell, the built-in Windows PowerShell 5.1 shell. That fourth job is the one that tests what the manifest promises. (For a module of your own that does not support 5.1, drop the include and raise PowerShellVersion in the manifest instead.)

The job name is customized to identify all combinations in the CI report, e.g. PowerShell 7 on ubuntu-latest and Windows PowerShell 5.1 on windows-latest.

fail-fast: false matters here. The default cancels every other job the moment one fails, so a Windows-only bug would abort the Linux and macOS runs and hide whether the failure is platform-specific. Turning it off costs a few runner minutes and tells you far more.

The artifact name now includes both matrix values. Jobs uploading to the same artifact name is an error, and ${{ matrix.os }} alone would still collide for the two Windows jobs.

This is where the cross-platform bugs show up

Join-Path and $TestDrive have been quietly protecting you. Hard-coded \ separators, case-sensitivity assumptions, and C:\temp paths all work on your machine and fail on Linux. A matrix is how you find them, and it is the main reason this module is worth doing even for a small module.

Where to go from here

The workflow above is a complete, working setup. Natural next steps, in rough order of value:

  • Publish the test results as a check run so failures annotate the pull request directly, using something like dorny/test-reporter, which reads the NUnit file you are already producing.
  • Send coverage.xml to a coverage service — the JaCoCo format is widely supported.
  • Require the check in branch protection, so a red build actually blocks the merge. Until you do this, CI is advisory.
  • Cache the Pester install if the install step becomes a meaningful share of the run time.

You are done

Starting from an empty folder, you have built a PowerShell module with a manifest, a loader, public and private functions, an external data file and a function that writes reports — and tested all of it:

  • 26 tests across six test files
  • Public functions tested through the module's real front door, which proves they are exported
  • Private helpers reached with InModuleScope, sparingly
  • A data source replaced by mocks so tests own their data
  • File operations isolated in TestDrive, leaving nothing behind
  • 100% coverage, arrived at by reading CommandsMissed rather than chasing a number
  • A test script that fails correctly, running on three operating systems — and on Windows PowerShell 5.1 — on every push

The reference documentation goes deeper on everything here: mocking, TestDrive, code coverage, configuration and the result object. If you write assertions you wish existed, custom assertions is the next thing worth reading.

Before you move on

0/5