This guide explains how to write, run, and debug tests for JiraPS.
JiraPS uses Pester v5.7+ for unit testing. All test files follow one of two standardized templates based on the function type being tested.
Tests are organized to mirror the module structure:
Tests/Functions/Public/ - Tests for public (exported) functions that make API calls
Public/README.md for detailsAdd-JiraFilterPermission.Unit.Tests.ps1Tests/Functions/Private/ - Tests for private (internal) converter/formatter functions
Private/README.md for detailsConvertTo-JiraAttachment.Unit.Tests.ps1$script: prefix.$script: prefix.JiraPS uses two primary test templates based on function type:
Location: Tests/Functions/Public/
Template File: Tests/Functions/Public/.template.ps1
Reference File: Tests/Functions/Public/Add-JiraFilterPermission.Unit.Tests.ps1
Use For: Functions that make API calls - Get-, Set-, Remove-, New-, Add-* functions
Structure:
#requires -modules @{ ModuleName = "Pester"; ModuleVersion = "5.7"; MaximumVersion = "5.999" }
BeforeDiscovery {
. "$PSScriptRoot/../../Helpers/TestTools.ps1"
Initialize-TestEnvironment
$script:moduleToTest = Resolve-ModuleSource
Import-Module $script:moduleToTest -Force -ErrorAction Stop
}
InModuleScope JiraPS {
Describe "Add-JiraFilterPermission" -Tag 'Unit' {
BeforeAll {
. "$PSScriptRoot/../../Helpers/TestTools.ps1"
# $VerbosePreference = 'Continue' # Uncomment for mock debugging
#region Definitions
$script:jiraServer = "https://jira.example.com"
# Test data and variables
#endregion
#region Mocks
Mock Get-JiraConfigServer -ModuleName JiraPS {
Write-MockDebugInfo 'Get-JiraConfigServer'
$jiraServer
}
Mock Invoke-JiraMethod -ModuleName JiraPS {
Write-MockDebugInfo 'Invoke-JiraMethod' 'Method', 'Uri', 'Body'
# Mock implementation
}
#endregion
}
Describe "Signature" {
BeforeAll {
$script:command = Get-Command -Name $ThisTest
}
Context "Parameter Types" {
It "has a parameter '<parameter>' of type '<type>'" -TestCases @(...) {
$command | Should -HaveParameter $parameter
}
}
Context "Mandatory Parameters" {
It "parameter '<parameter>' is mandatory" -TestCases @(...) {
$command | Should -HaveParameter $parameter -Mandatory
}
}
}
Describe "Behavior" {
Context "Feature Description" {
It "performs expected action" {
# Test implementation
}
}
}
Describe "Input Validation" {
Context "Type Validation - Negative Cases" {
It "rejects invalid type '<description>'" -TestCases @(...) {
{ ... } | Should -Throw -ExpectedMessage "*Filter*"
}
}
Context "Type Validation - Positive Cases" {
It "accepts valid input" {
{ ... } | Should -Not -Throw
}
}
}
}
}
Key Features:
Write-MockDebugInfo for debugging-ExpectedMessageLocation: Tests/Functions/Private/
Template File: Tests/Functions/Private/.template.ps1
Reference File: Tests/Functions/Private/ConvertTo-JiraAttachment.Unit.Tests.ps1
Use For: Functions that transform JSON to PowerShell objects - ConvertTo-, ConvertFrom- functions
Structure:
#requires -modules @{ ModuleName = "Pester"; ModuleVersion = "5.7"; MaximumVersion = "5.999" }
BeforeDiscovery {
. "$PSScriptRoot/../../Helpers/TestTools.ps1"
Initialize-TestEnvironment
$script:moduleToTest = Resolve-ModuleSource
Import-Module $script:moduleToTest -Force -ErrorAction Stop
}
InModuleScope JiraPS {
Describe "ConvertTo-JiraAttachment" -Tag 'Unit' {
BeforeAll {
. "$PSScriptRoot/../../Helpers/TestTools.ps1"
#region Definitions
$script:jiraServer = 'http://jiraserver.example.com'
# Sample JSON fixture (can be large - keep it organized)
$script:sampleJson = @"
{
"id": "123",
"name": "Example",
"created": "2025-01-01T00:00:00.000Z"
}
"@
$script:sampleObject = ConvertFrom-Json -InputObject $sampleJson
#endregion
#region Mocks
# Converter functions typically don't need mocks
#endregion
}
Describe "Behavior" {
Context "Object Conversion" {
BeforeAll {
$script:result = ConvertTo-YourType -InputObject $sampleObject
}
It "creates a PSObject out of JSON input" {
$result | Should -Not -BeNullOrEmpty
}
It "adds the custom type name 'JiraPS.YourType'" {
$result.PSObject.TypeNames[0] | Should -Be 'JiraPS.YourType'
}
}
Context "Property Mapping" {
BeforeAll {
$script:result = ConvertTo-YourType -InputObject $sampleObject
}
It "defines the 'Id' property with correct value" {
$result.Id | Should -Be "123"
}
It "defines required properties" {
$result.Name | Should -Not -BeNullOrEmpty
}
}
Context "Type Conversion" {
BeforeAll {
$script:result = ConvertTo-YourType -InputObject $sampleObject
}
It "converts 'created' field to DateTime object" {
$result.created | Should -Not -BeNullOrEmpty
$result.created | Should -BeOfType [System.DateTime]
}
It "converts nested objects to proper types" {
$result.author.PSObject.TypeNames[0] | Should -Be 'JiraPS.User'
}
}
Context "Pipeline Support" {
It "accepts input from pipeline" {
$result = $sampleObject | ConvertTo-YourType
$result | Should -Not -BeNullOrEmpty
}
It "handles array input" {
$multipleObjects = @($sampleObject, $sampleObject)
$result = $multipleObjects | ConvertTo-YourType
@($result).Count | Should -Be 2
}
}
}
}
}
Key Features:
#region DefinitionsBeforeAll in contexts to avoid redundant conversions| Aspect | Public CRUD Functions | Private Converter Functions |
|---|---|---|
| Mocks | Required (API calls) | Usually not needed |
| Focus | API interaction | Data transformation |
| Contexts | Signature, Behavior, Input Validation | Object Conversion, Property Mapping, Type Conversion, Pipeline Support |
| Test Data | Minimal (IDs, strings) | Large JSON fixtures |
| Complexity | Error handling, edge cases | Type checking, nested objects |
The Tests/Helpers/TestTools.ps1 module provides reusable functions for test setup:
Initialize-TestEnvironment
BeforeDiscovery block before importing the moduleResolve-ModuleSource
.psd1)$script:moduleToTestResolve-ProjectRoot
Resolve-ModuleSource internallyWrite-MockDebugInfo
$VerbosePreference = 'Continue'FunctionName (string), Params (string array of parameter names)BeforeDiscovery {
. "$PSScriptRoot/../../Helpers/TestTools.ps1"
Initialize-TestEnvironment # Clean up modules
$script:moduleToTest = Resolve-ModuleSource # Get module path
Import-Module $script:moduleToTest -Force -ErrorAction Stop
}
InModuleScope JiraPS {
Describe "YourFunction" -Tag 'Unit' {
BeforeAll {
. "$PSScriptRoot/../../Helpers/TestTools.ps1"
# $VerbosePreference = 'Continue' # Uncomment to debug mocks
Mock Get-JiraFilter -ModuleName JiraPS {
Write-MockDebugInfo 'Get-JiraFilter' 'Id' # Shows: Id = 123
# mock logic
}
}
}
}
# From repository root
Invoke-Build -Task Test
Invoke-Pester ./Tests/Functions/Get-JiraIssue.Unit.Tests.ps1
Invoke-Pester ./Tests/Functions/Get-JiraIssue.Unit.Tests.ps1 -Output Detailed
Invoke-Pester -Path ./Tests -Tag Unit
FunctionName.Unit.Tests.ps1Use -TestCases for testing multiple scenarios:
It "rejects invalid type '<description>'" -TestCases @(
@{ description = "string"; value = "invalid" }
@{ description = "number"; value = 123 }
@{ description = "null"; value = $null }
) -Test {
param($value)
{ Your-Function -Parameter $value } | Should -Throw -ExpectedMessage "*expected*"
}
Always validate error messages for robust testing:
Context "Type Validation - Negative Cases" {
It "rejects invalid input with meaningful error" {
{ Your-Function -Parameter "bad" } | Should -Throw -ExpectedMessage "*Filter*"
}
}
Mock external dependencies in BeforeAll:
Mock Invoke-JiraMethod -ModuleName JiraPS {
Write-MockDebugInfo 'Invoke-JiraMethod' 'Method', 'Uri', 'Body'
ConvertFrom-Json @'
{
"id": "123",
"name": "Test"
}
'@
}
The Write-MockDebugInfo.ps1 helper provides easy mock debugging through verbose output.
In your test file’s BeforeAll block, uncomment the debug line:
BeforeAll {
. "$PSScriptRoot/../Helpers/Write-MockDebugInfo.ps1"
# Uncomment to enable mock debug output:
$VerbosePreference = 'Continue' # <-- Uncomment this line
# ... rest of BeforeAll
}
In your mock definitions, call Write-MockDebugInfo with the function name and parameter names:
Mock Invoke-JiraMethod -ModuleName JiraPS {
Write-MockDebugInfo 'Invoke-JiraMethod' 'Method', 'Uri', 'Body'
# ... mock implementation
}
Mock Get-JiraFilter -ModuleName JiraPS {
Write-MockDebugInfo 'Get-JiraFilter' 'Id', 'Name'
# ... mock implementation
}
Invoke-Pester ./Tests/Functions/Your-Test.Unit.Tests.ps1 -Output Detailed
Important: The -Verbose parameter on Invoke-Pester does NOT enable mock debug output. It only shows Pester’s internal verbose messages. The debug output will appear automatically once you set $VerbosePreference = 'Continue' in the test file’s BeforeAll block.
You’ll see output like:
🔷 Mock: Invoke-JiraMethod
[Method] = "GET"
[Uri] = "https://jira.example.com/rest/api/2/issue/TEST-123"
[Body] = <null>
🔷 Mock: Get-JiraFilter
[Id] = 12345
[Name] = <null>
<null> for null/empty valuesWhen finished debugging, re-comment the line:
# $VerbosePreference = 'Continue'
The -Verbose parameter on Invoke-Pester only affects Pester’s own internal logging, not the test script’s $VerbosePreference. Due to how Pester v5 isolates test execution contexts, command-line parameters don’t propagate into the test script scope. This is why you must set $VerbosePreference directly inside the test file.
If you need interactive debugging, use breakpoints:
Mock Invoke-JiraMethod -ModuleName JiraPS {
$Method | Set-PSBreakpoint # Set breakpoint
# ... mock implementation
}
Then run in VS Code or with pwsh -Debug.
#region Definitions and #region Mocks[ValidateNotNullOrEmpty()], [ValidatePattern()]-TestCases with pipeline inputShould -Invoke to verify mock behavior-ExpectedMessage in error testsBeforeAll instead of BeforeEach where possibleAdd-JiraFilterPermission.Unit.Tests.ps1 as referenceSee Tests/Functions/Add-JiraFilterPermission.Unit.Tests.ps1 for a complete, well-structured test file that demonstrates all best practices.
Install-Module Pester -MinimumVersion 5.7 -ForceInModuleScope JiraPS wraps Describe block-ModuleName JiraPS is specified in MockJiraPS.psd1)$VerbosePreference = 'Continue' is uncommented in BeforeAllWrite-MockDebugInfo.ps1 is loaded before mocks are definedWrite-MockDebugInfo is called inside mock definition$script: prefix in BeforeDiscovery and BeforeAll$script:variableName in It blocks if needed