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
name: Test
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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@v4
with:
name: test-results
path: |
testResults.xml
coverage.xml
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
Your module claims to run on Windows, Linux and macOS. A matrix runs the same job once per operating system:
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- 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@v4
with:
name: test-results-${{ matrix.os }}
path: |
testResults.xml
coverage.xml
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 has to include ${{ matrix.os }} — three jobs uploading to the same artifact name is an error.
shell: pwsh is PowerShell 7 on every runner, Windows included. So three operating systems, one PowerShell version.
Your manifest says PowerShellVersion = '5.1', and nothing above has tested that claim. Windows PowerShell 5.1 is a genuinely different engine, and it is where the surprises live. To cover it, add a second dimension and let the Windows job run under powershell — the built-in 5.1 shell:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
shell: [pwsh]
include:
- os: windows-latest
shell: powershell
Then use shell: ${{ matrix.shell }} on the PowerShell steps, and add ${{ matrix.shell }} to the artifact name so the two Windows jobs do not collide. If you would rather not support 5.1, the honest alternative is to raise PowerShellVersion in the manifest.
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.xmlto 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
CommandsMissedrather than chasing a number - A test script that fails correctly, running on three operating systems 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.