Skip to content

Latest commit

Β 

History

History
614 lines (444 loc) Β· 20.2 KB

File metadata and controls

614 lines (444 loc) Β· 20.2 KB

JiraPS Testing Guide

This guide explains how to write, run, and debug tests for JiraPS.

Table of Contents

Test Structure

JiraPS uses Pester 6.2.0 through 6.x for unit testing. All test files follow one of two standardized templates based on the function type being tested.

Directory Organization

Tests are organized to mirror the module structure:

Key Structure Elements

  • BeforeDiscovery: Loads the module before test discovery. Variables need $script: prefix.
  • InModuleScope: Required wrapper to access module internals. Must be top-level.
  • Describe Blocks: Organize tests by category (Signature, Behavior, Input Validation).
  • Context Blocks: Group related tests within a Describe block.
  • BeforeAll: Set up test data and mocks. Variables need $script: prefix.

Test Templates

JiraPS uses two primary test templates based on function type:

CRUD Functions (Get/Set/Remove/New/Add)

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 = "6.2.0"; MaximumVersion = "6.999" }

BeforeDiscovery {
    . "$PSScriptRoot/../../Helpers/TestTools.ps1"

    $script:moduleToTest = Initialize-TestEnvironment
}

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:

  • Mock API calls with Write-MockDebugInfo for debugging
  • Separate Describe blocks for Signature, Behavior, and Input Validation
  • Positive/Negative test separation
  • Error message validation with -ExpectedMessage

Converter Functions (ConvertTo-/ConvertFrom-)

Location: 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 = "6.2.0"; MaximumVersion = "6.999" }

BeforeDiscovery {
    . "$PSScriptRoot/../../Helpers/TestTools.ps1"

    $script:moduleToTest = Initialize-TestEnvironment
}

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 'AtlassianPS.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:

  • No mocks needed typically (pure transformation functions)
  • Large JSON fixtures kept organized in #region Definitions
  • Focus on type conversion and property mapping
  • Organized Context blocks:
    • Object Conversion: Basic transformation and type name tests
    • Property Mapping: Field presence and values
    • Type Conversion: DateTime, nested objects, type checking
    • Pipeline Support: Pipeline behavior and array handling
  • Use BeforeAll in contexts to avoid redundant conversions

Template Comparison

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

Test Helpers (TestTools.ps1)

The Tests/Helpers/TestTools.ps1 module provides reusable functions for test setup:

Functions

Initialize-TestEnvironment

  • Ensures the JiraPS module is loaded at the current on-disk version and returns the manifest path used.
  • Must be called in BeforeDiscovery; assign its return value to $script:moduleToTest.
  • Idempotent: short-circuits when the loaded module already matches the on-disk source. The next test file in a Pester run skips the Remove-Module / Import-Module churn entirely; the cache invalidates automatically when a source file (or Release/JiraPS/JiraPS.psm1) is touched.

Resolve-ModuleSource

  • Returns the path to the JiraPS module manifest (.psd1).
  • Automatically detects source vs. Release build.
  • You normally don't call this directly β€” Initialize-TestEnvironment already returns it.

Resolve-ProjectRoot

  • Returns the path to the project root directory
  • Used by Resolve-ModuleSource internally
  • Can be used in project-level tests (Build.Tests.ps1, etc.)

Write-MockDebugInfo

  • Displays mock call information when $VerbosePreference = 'Continue'
  • Call at the start of mock script blocks
  • Parameters: FunctionName (string), Params (string array of parameter names)

Usage Pattern

BeforeDiscovery {
    . "$PSScriptRoot/../../Helpers/TestTools.ps1"

    $script:moduleToTest = Initialize-TestEnvironment   # imports + returns manifest path
}

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
            }
        }
    }
}

Running Tests

Run All Tests

# From repository root
Invoke-Build -Task Test

Run Specific Test File

Invoke-Pester ./Tests/Functions/Get-JiraIssue.Unit.Tests.ps1

Run with Detailed Output

Invoke-Pester ./Tests/Functions/Get-JiraIssue.Unit.Tests.ps1 -Output Detailed

Run Tests by Tag

Invoke-Pester -Path ./Tests -Tag Unit

Writing Tests

Test Naming Convention

  • Test files: FunctionName.Unit.Tests.ps1
  • Describe blocks: Use function name
  • It blocks: Descriptive, starting with verb (e.g., "accepts valid filter object")

Parameterized Tests

Use -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*"
}

Error Testing

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*"
    }
}

Mocking

Mock external dependencies in BeforeAll:

Mock Invoke-JiraMethod -ModuleName JiraPS {
    Write-MockDebugInfo 'Invoke-JiraMethod' 'Method', 'Uri', 'Body'
    ConvertFrom-Json @'
    {
        "id": "123",
        "name": "Test"
    }
'@
}

Debugging Mocks

Using Write-MockDebugInfo Helper

The Write-MockDebugInfo.ps1 helper provides easy mock debugging through verbose output.

Enable Debug 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
}

Add Debug Calls to Mocks

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
}

Run Tests and View Output

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>

Debug Output Features

  • Color coding: Function names in Cyan, values in Yellow, nulls in DarkGray
  • Type awareness: Arrays show item count and contents (up to 5 items)
  • Hashtable display: Shows key/value pairs
  • String truncation: Long strings truncated to 97 characters with "..."
  • Null indication: Shows <null> for null/empty values

Disable Debug Output

When finished debugging, re-comment the line:

# $VerbosePreference = 'Continue'

Why Doesn't -Verbose Work?

The -Verbose parameter on Invoke-Pester only affects Pester's own internal logging, not the test script's $VerbosePreference. Due to how Pester v6 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.

Alternative: Manual Debugging

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.

Best Practices

Test Organization

  1. Group by purpose: Use Context blocks to separate positive/negative tests
  2. Clear naming: Describe what's being tested, not how
  3. One assertion per test: Makes failures easy to identify
  4. Test data in regions: Use #region Definitions and #region Mocks

Code Quality

  1. Use validation attributes: [ValidateNotNullOrEmpty()], [ValidatePattern()]
  2. Test pipeline support: Use -TestCases with pipeline input
  3. Verify mock calls: Use Should -Invoke to verify mock behavior
  4. Error messages: Always include -ExpectedMessage in error tests

Performance

  1. Mock external calls: Never make real API calls in unit tests
  2. Minimize setup: Use BeforeAll instead of BeforeEach where possible
  3. Parameterized tests: Reduce duplicate test code

Maintainability

  1. Follow template: Use Add-JiraFilterPermission.Unit.Tests.ps1 as reference
  2. American English: Use "Behavior" not "Behaviour"
  3. Consistent styling: Follow existing patterns in the codebase
  4. Document complex tests: Add comments for non-obvious test logic

Example Test File

See Tests/Functions/Add-JiraFilterPermission.Unit.Tests.ps1 for a complete, well-structured test file that demonstrates all best practices.

Troubleshooting

Tests Not Running

  • Ensure Pester 6.2.0 through 6.x is installed: Install-Module Pester -MinimumVersion 6.2.0 -MaximumVersion 6.999 -Force
  • Check for syntax errors in test file
  • Verify module imports correctly in BeforeDiscovery

Mocks Not Working

  • Confirm InModuleScope JiraPS wraps Describe block
  • Check -ModuleName JiraPS is specified in Mock
  • Verify function is exported from module (check JiraPS.psd1)

Debug Output Not Showing

  • Verify $VerbosePreference = 'Continue' is uncommented in BeforeAll
  • Check Write-MockDebugInfo.ps1 is loaded before mocks are defined
  • Ensure Write-MockDebugInfo is called inside mock definition

Variable Scope Issues

  • Use $script: prefix in BeforeDiscovery and BeforeAll
  • Use plain variable names in Describe blocks
  • Reference $script:variableName in It blocks if needed

Integration Tests

In addition to unit tests, JiraPS includes integration tests that run against a live Jira Cloud instance.

Overview

  • Location: Tests/Integration/
  • Tags: Integration, Smoke
  • Documentation: See Integration/README.md for full setup instructions

Quick Start

  1. Copy .env.example to .env in the project root
  2. Configure your Jira Cloud credentials and test fixtures
  3. Run integration tests (no build required):
# Run all integration tests in parallel (recommended)
Invoke-Build -Task TestIntegration

# Run smoke tests only (fast, critical tests)
Invoke-Build -Task TestIntegration -Tag 'Smoke'

# With custom parallelism
Invoke-Build -Task TestIntegration -ThrottleLimit 8

# Or use the script directly for more control
./Tests/Invoke-ParallelPester.ps1 -Tag 'Integration' -Output Detailed

Test Filtering

Integration tests are excluded by default from Invoke-Build -Task Test to prevent failures when credentials aren't configured.

# Run only unit tests (default, excludes Integration)
Invoke-Build -Task Build, Test

# Run integration tests (no build needed)
Invoke-Build -Task TestIntegration

CI/CD

Cloud integration coverage is split across two workflows:

  • Smoke tests (ci.yml, smoke_tests job): Run on every first-party PR + every push to master. Skipped on fork / Dependabot PRs (no secrets). Gates continuous delivery via the CI Result sentinel.
  • Full integration tests (integration_tests.yml): two parallel jobs β€” cloud_integration_tests and server_integration_tests (Dockerized Jira DC). Run on:
    • Weekly schedule (Sunday at 5 AM UTC) β€” both jobs.
    • Manual trigger (workflow_dispatch) β€” use this to run the full suite against an in-flight PR branch; pass track=cloud, track=server, or track=both (default both).

Required GitHub Secrets:

  • JIRA_CLOUD_URL
  • JIRA_CLOUD_USERNAME
  • JIRA_CLOUD_PASSWORD
  • JIRA_TEST_PROJECT
  • JIRA_TEST_ISSUE

When the workflow runs, the TestIntegration task in JiraPS.build.ps1 validates that all required environment variables are set and fails early with a clear error if any are missing β€” so a misconfigured secret surfaces as a red CI run rather than a green run with all tests skipped.

Resources