This guide covers the complete development workflow including creating functions, building, testing, and generating documentation.
- Public functions (
src/Public/) - Exported to users, become module commands - Private functions (
src/Private/) - Internal helpers, not exported
Follow PowerShell naming conventions: Verb-Noun.ps1
# src/Public/Get-Something.ps1
function Get-Something {
<#
.SYNOPSIS
Brief one-line description of what the function does
.DESCRIPTION
Detailed explanation of the function's purpose and behavior.
Can span multiple lines for complex functions.
.PARAMETER Name
Description of what this parameter does
.PARAMETER Force
Description of Force parameter (optional switches)
.EXAMPLE
Get-Something -Name 'Example'
Description of what this example demonstrates and expected output.
.EXAMPLE
Get-Something -Name 'Test' -Force
Another example showing different usage pattern.
.INPUTS
System.String
You can pipe string values to this function
.OUTPUTS
System.Object
Returns custom object with properties
.NOTES
Additional information about requirements, limitations, or notes
.LINK
https://github.com/YourUsername/YourModule
#>
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(
Mandatory,
ValueFromPipeline,
ValueFromPipelineByPropertyName
)]
[ValidateNotNullOrEmpty()]
[string]
$Name,
[Parameter()]
[switch]
$Force
)
begin {
Write-Verbose "Starting $($MyInvocation.MyCommand)"
$results = [System.Collections.Generic.List[object]]::new()
}
process {
try {
# Should Process for destructive operations
if ($PSCmdlet.ShouldProcess($Name, 'Process item')) {
Write-Verbose "Processing: $Name"
# Your implementation here
$result = [PSCustomObject]@{
Name = $Name
Processed = $true
Timestamp = Get-Date
}
$results.Add($result)
}
}
catch {
$errorMessage = "Failed to process '$Name': $_"
Write-Error $errorMessage
throw
}
}
end {
Write-Verbose "Completed $($MyInvocation.MyCommand). Processed $($results.Count) items."
return $results
}
}Create FunctionName.Tests.ps1 alongside the function:
# src/Public/Get-Something.Tests.ps1
BeforeAll {
# Import the function being tested
. $PSCommandPath.Replace('.Tests.ps1', '.ps1')
}
Describe 'Get-Something' {
Context 'Parameter Validation' {
It 'Should require Name parameter' {
{ Get-Something } | Should -Throw
}
It 'Should not accept null or empty Name' {
{ Get-Something -Name '' } | Should -Throw
}
It 'Should accept Name from pipeline' {
{ 'TestValue' | Get-Something } | Should -Not -Throw
}
}
Context 'Basic Functionality' {
It 'Should return object with expected properties' {
$result = Get-Something -Name 'Test'
$result | Should -Not -BeNullOrEmpty
$result.Name | Should -Be 'Test'
$result.Processed | Should -Be $true
$result.Timestamp | Should -BeOfType [DateTime]
}
It 'Should process multiple items from pipeline' {
$results = 'Item1', 'Item2', 'Item3' | Get-Something
$results.Count | Should -Be 3
}
}
Context 'Error Handling' {
It 'Should throw on invalid operation' {
# Mock any external dependencies
Mock Write-Verbose {}
{ Get-Something -Name 'InvalidValue' } | Should -Throw
}
It 'Should write error on failure' {
# Test error handling behavior
{ Get-Something -Name 'ErrorCase' -ErrorAction Stop } | Should -Throw
}
}
Context 'WhatIf and Confirm' {
It 'Should support WhatIf' {
{ Get-Something -Name 'Test' -WhatIf } | Should -Not -Throw
}
It 'Should not process when WhatIf is specified' {
$result = Get-Something -Name 'Test' -WhatIf
# Verify no changes were made
}
}
Context 'Verbose Output' {
It 'Should write verbose messages' {
$verboseOutput = Get-Something -Name 'Test' -Verbose 4>&1
$verboseOutput | Should -Not -BeNullOrEmpty
}
}
}Note: Public functions are automatically exported from the module. No need to manually update
FunctionsToExportin the module manifest.
# Run all tests
Invoke-Build Test
# Run specific test file
Invoke-Pester -Path ./src/Public/Get-Something.Tests.ps1
# Run with detailed output
Invoke-Pester -Path ./src/Public/Get-Something.Tests.ps1 -Output Detailed# View all available tasks
Invoke-Build ?
# Common tasks
Invoke-Build # Default: Clean + Build
Invoke-Build Clean # Remove build artifacts
Invoke-Build Build # Compile module
Invoke-Build Test # Run all tests
# Testing tasks
Invoke-Build UnitTests # Run Pester tests with coverage
Invoke-Build PSScriptAnalyzer # Run static code analysis
Invoke-Build InjectionHunter # Run security scans
# Documentation tasks
Invoke-Build Export-CommandHelp # Generate help files
# Publishing task
Invoke-Build Publish # Publish to PowerShell Gallery# Development build (default)
Invoke-Build -ReleaseType Debug
# Production release build
Invoke-Build -ReleaseType Release
# Pre-release build
Invoke-Build -ReleaseType PrereleaseAfter building, find your module in:
build/
├── src/ # Copied source files
├── out/ # Compiled module (ready to use)
│ └── YourModuleName/
│ ├── YourModuleName.psd1 # Module manifest
│ ├── YourModuleName.psm1 # Compiled module file
│ └── en-US/ # Help files
└── help/ # Generated help documentation
# Import the built module
Import-Module ./build/out/YourModuleName/YourModuleName.psd1 -Force
# Test your functions
Get-Command -Module YourModuleName
Get-Help Get-Something -Full
# Remove when done
Remove-Module YourModuleName# Run all tests (unit, analysis, security)
Invoke-Build Test
# Run specific test types
Invoke-Build UnitTests # Unit tests only
Invoke-Build PSScriptAnalyzer # Code analysis only
Invoke-Build InjectionHunter # Security scans only
# Run specific test file
Invoke-Pester -Path ./src/Public/Get-Something.Tests.ps1
# Run with code coverage
Invoke-Pester -Configuration @{
Run = @{
Path = './src'
}
CodeCoverage = @{
Enabled = $true
Path = './src/**/*.ps1'
OutputPath = './test-results/code-coverage.xml'
}
TestResult = @{
Enabled = $true
OutputPath = './test-results/unit-tests.xml'
}
}All test results are saved to test-results/:
test-results/
├── unit-tests.xml # Pester test results (NUnit XML)
├── code-coverage.xml # Code coverage report (Cobertura)
├── static-code-analysis.xml # PSScriptAnalyzer results
└── code-injection.xml # InjectionHunter security scan
Use Pester's Describe, Context, It blocks:
Describe 'Module-Level Tests' {
Context 'Specific Feature or Scenario' {
It 'Should behave in expected way' {
# Arrange
$input = 'test'
# Act
$result = Get-Something -Name $input
# Assert
$result | Should -Not -BeNullOrEmpty
}
}
}# Equality
$result | Should -Be 'expected'
$result | Should -Not -Be 'unexpected'
# Type checking
$result | Should -BeOfType [string]
$result | Should -BeOfType [System.Collections.Hashtable]
# Null/Empty checks
$result | Should -Not -BeNullOrEmpty
$result | Should -BeNullOrEmpty
# Exceptions
{ Get-Something } | Should -Throw
{ Get-Something } | Should -Throw '*required*'
{ Get-Something } | Should -Not -Throw
# Collection checks
$array | Should -HaveCount 3
$array | Should -Contain 'item'
# Boolean checks
$result | Should -BeTrue
$result | Should -BeFalseBeforeAll {
# Mock external commands
Mock Invoke-RestMethod {
return @{
Status = 'Success'
Data = 'Mocked'
}
}
}
It 'Should use mocked API call' {
$result = Get-Something -Name 'Test'
Should -Invoke Invoke-RestMethod -Exactly 1
}- Target: 80% or higher code coverage
- Critical paths: 100% coverage for error handling
- Exported functions: Should have comprehensive test coverage
# Check coverage
Invoke-Build UnitTests
# Review coverage report
# Open test-results/code-coverage.xml in coverage viewerThis project uses PlatyPS to generate documentation in two formats:
- Markdown (
.md) - For GitHub/web viewing indocs/help/ - MAML (
.xml) - For PowerShell'sGet-Helpcommand
# Generate all help documentation
Invoke-Build Export-CommandHelpThis creates:
docs/help/
├── Get-PSScriptModuleInfo.md # Markdown help
└── Get-Something.md # One file per function
build/out/YourModuleName/en-US/
└── YourModuleName.dll-Help.xml # MAML help for Get-Help
# Import your module
Import-Module ./build/out/YourModuleName/YourModuleName.psd1
# View help
Get-Help Get-Something
Get-Help Get-Something -Full
Get-Help Get-Something -Examples
Get-Help Get-Something -Parameter Name-
Update comment-based help in your function
-
Regenerate help files:
Invoke-Build Export-CommandHelp
-
Review generated markdown in
docs/help/ -
Commit updated documentation
You can manually edit markdown files in docs/help/ to add:
- Additional examples
- More detailed descriptions
- Related links
- Notes and warnings
After editing, regenerate MAML:
Invoke-Build Export-CommandHelpAll code must pass PSScriptAnalyzer rules:
# Run analysis
Invoke-Build PSScriptAnalyzer
# Or directly
Invoke-ScriptAnalyzer -Path ./src -Recurse -Settings ./tests/PSScriptAnalyzer/PSScriptAnalyzerSettings.psd1Sometimes you need to suppress specific rules:
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSAvoidUsingWriteHost',
'',
Justification = 'Interactive prompt requires Write-Host'
)]
param()
Write-Host "User prompt message" -ForegroundColor Cyan# Always work on feature branches
git checkout -b feature/my-feature
# Keep main branch clean
# Only merge through Pull RequestsUse semantic versioning keywords:
# New feature (minor version bump)
git commit -m "Add Get-Something function +semver: minor"
# Bug fix (patch version bump)
git commit -m "Fix parameter validation in Get-Something +semver: patch"
# Breaking change (major version bump)
git commit -m "Remove deprecated Get-OldFunction +semver: major"
# Documentation only (no version bump)
git commit -m "Update README examples +semver: none"-
Create feature branch
-
Make changes and commit
-
Push to GitHub:
git push origin feature/my-feature
-
Create Pull Request on GitHub
-
Wait for CI checks to pass:
- Unit tests
- Code analysis
- Security scans
-
Merge after approval
-
Automatic release triggered on merge to main
✅ Do:
- Use approved PowerShell verbs (
Get-Verb) - Include comprehensive help documentation
- Add parameter validation
- Support pipeline input where appropriate
- Implement proper error handling
- Add verbose output for troubleshooting
- Support
-WhatIffor destructive operations
❌ Don't:
- Use aliases in production code
- Hard-code paths or credentials
- Suppress errors without good reason
- Mix output types from same function
- Use positional parameters (always name them)
✅ Do:
- Test both success and failure scenarios
- Test parameter validation
- Test pipeline input
- Mock external dependencies
- Test edge cases (null, empty, invalid input)
- Use descriptive test names
❌ Don't:
- Depend on external services in tests
- Share state between tests
- Skip tests (fix or remove them)
- Test implementation details (test behavior)
✅ Do:
- Include meaningful examples
- Describe all parameters
- Document outputs
- Add notes for special requirements
- Link to related functions
❌ Don't:
- Copy-paste example output (show commands only)
- Leave default example text
- Skip parameter descriptions
- 📖 Pester Documentation
- 📖 PSScriptAnalyzer Rules
- 📖 PlatyPS Documentation
- 📖 PowerShell Best Practices
- 📖 About Comment-Based Help
Happy coding! Build great PowerShell modules! 🚀