适用于 PowerShell 5.1 及以上版本,建议安装 Pester 5.x
基础设施即代码(IaC)和自动化脚本的普及使得”测试”不再只是开发者的专利。Pester 是 PowerShell 社区最流行的测试框架,它使用 BDD(行为驱动开发)风格的语法编写测试——描述期望行为、执行代码、验证结果。无论是验证函数逻辑、测试配置是否符合预期,还是构建运维合规检查,Pester 都是不可或缺的工具。
本文将讲解 Pester 的核心语法、常用模式,以及如何构建运维合规测试。
安装与基础语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| Install-Module -Name Pester -Scope CurrentUser -Force -SkipPublisherCheck
Get-Module Pester -ListAvailable | Select-Object Name, Version
$testScript = @' Describe "数学运算测试" { Context "加法" { It "1 + 1 应该等于 2" { (1 + 1) | Should -Be 2 }
It "负数加法应该正确" { (-3) + 5 | Should -Be 2 } }
Context "字符串操作" { It "字符串应该包含子串" { "Hello World" | Should -BeLike "*World*" }
It "字符串长度应该正确" { "PowerShell".Length | Should -Be 10 } } } '@
Invoke-Pester -ScriptBlock ([scriptblock]::Create($testScript)) -Output Detailed
|
执行结果示例:
1 2 3 4 5 6 7 8 9
| Describing 数学运算测试 Context 加法 [+] 1 + 1 应该等于 2 15ms [+] 负数加法应该正确 3ms Context 字符串操作 [+] 字符串应该包含子串 8ms [+] 字符串长度应该正确 2ms
Tests passed: 4, Failed: 0, Skipped: 0
|
测试自定义函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| function Get-FileSizeCategory { param([long]$SizeBytes)
if ($SizeBytes -lt 1KB) { return 'Tiny' } if ($SizeBytes -lt 1MB) { return 'Small' } if ($SizeBytes -lt 1GB) { return 'Medium' } if ($SizeBytes -lt 1TB) { return 'Large' } return 'Huge' }
Describe "Get-FileSizeCategory" { Context "小文件" { It "0 字节应该返回 Tiny" { Get-FileSizeCategory -SizeBytes 0 | Should -Be 'Tiny' }
It "1023 字节应该返回 Tiny" { Get-FileSizeCategory -SizeBytes 1023 | Should -Be 'Tiny' } }
Context "边界值测试" { It "1KB 恰好应该是 Small" { Get-FileSizeCategory -SizeBytes 1KB | Should -Be 'Small' }
It "1MB - 1 应该是 Small" { Get-FileSizeCategory -SizeBytes (1MB - 1) | Should -Be 'Small' }
It "1MB 应该是 Medium" { Get-FileSizeCategory -SizeBytes 1MB | Should -Be 'Medium' } }
Context "异常输入" { It "负数应该抛出异常" { { Get-FileSizeCategory -SizeBytes -1 } | Should -Throw } } }
Invoke-Pester -Output Detailed
|
执行结果示例:
1 2 3 4 5 6 7 8 9 10
| Describing Get-FileSizeCategory Context 小文件 [+] 0 字节应该返回 Tiny 12ms [+] 1023 字节应该返回 Tiny 3ms Context 边界值测试 [+] 1KB 恰好应该是 Small 2ms [+] 1MB - 1 应该是 Small 2ms [+] 1MB 应该是 Medium 2ms
Tests passed: 5, Failed: 0
|
运维合规测试
Pester 最强大的运维场景是基础设施合规检查——验证服务器配置是否符合安全基线:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| Describe "Windows 安全基线检查" { Context "执行策略" { It "执行策略不应为 Unrestricted" { $policy = Get-ExecutionPolicy $policy | Should -Not -Be 'Unrestricted' }
It "应使用 RemoteSigned 或更严格的策略" { $policy = Get-ExecutionPolicy @('RemoteSigned', 'AllSigned', 'Restricted') | Should -Contain $policy } }
Context "防火墙状态" { It "域配置文件防火墙应启用" { $fw = Get-NetFirewallProfile -Profile Domain $fw.Enabled | Should -BeTrue }
It "专用配置文件防火墙应启用" { $fw = Get-NetFirewallProfile -Profile Private $fw.Enabled | Should -BeTrue }
It "公用配置文件防火墙应启用" { $fw = Get-NetFirewallProfile -Profile Public $fw.Enabled | Should -BeTrue } }
Context "磁盘空间" { It "系统盘可用空间应大于 10GB" { $disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'" $freeGB = [math]::Round($disk.FreeSpace / 1GB, 2) $freeGB | Should -BeGreaterThan 10 } }
Context "安全更新" { It "应在最近 30 天内安装过更新" { $latestPatch = Get-CimInstance Win32_QuickFixEngineering | Sort-Object InstalledOn -Descending | Select-Object -First 1 ($latestPatch.InstalledOn) | Should -BeGreaterThan (Get-Date).AddDays(-30) } }
Context "密码策略" { It "最小密码长度应至少 8 位" { $policy = Get-ADDefaultDomainPasswordPolicy -ErrorAction SilentlyContinue if ($policy) { $policy.MinPasswordLength | Should -BeGreaterOrEqual 8 } } } }
$config = New-PesterConfiguration $config.Output.Verbosity = 'Detailed' $config.TestResult.Enabled = $true $config.TestResult.OutputPath = "C:\Reports\compliance-results.xml"
Invoke-Pester -Configuration $config
|
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12
| Describing Windows 安全基线检查 Context 执行政策 [+] 执行政策不应为 Unrestricted 15ms Context 防火墙状态 [+] 域配置文件防火墙应启用 8ms [+] 专用配置文件防火墙应启用 5ms [-] 公用配置文件防火墙应启用 45ms Expected $true, but got $false. Context 磁盘空间 [+] 系统盘可用空间应大于 10GB 12ms
Tests passed: 6, Failed: 1, Skipped: 0
|
Mock 和 Stub
Pester 支持 Mock 功能,可以替换外部依赖进行隔离测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| function Get-ServerStatus { param([string]$ServerName)
try { $response = Invoke-RestMethod -Uri "https://$ServerName/health" -TimeoutSec 5 return $response.status } catch { return "Unreachable" } }
Describe "Get-ServerStatus" { BeforeAll { Mock Invoke-RestMethod { return @{ status = "healthy" } } -ParameterFilter { $Uri -match 'web-01' }
Mock Invoke-RestMethod { return @{ status = "degraded" } } -ParameterFilter { $Uri -match 'db-01' }
Mock Invoke-RestMethod { throw "Connection timeout" } -ParameterFilter { $Uri -match 'legacy' } }
It "正常服务器应返回 healthy" { Get-ServerStatus -ServerName "web-01" | Should -Be "healthy" }
It "降级服务器应返回 degraded" { Get-ServerStatus -ServerName "db-01" | Should -Be "degraded" }
It "不可达服务器应返回 Unreachable" { Get-ServerStatus -ServerName "legacy-server" | Should -Be "Unreachable" }
It "应该调用 Invoke-RestMethod" { Get-ServerStatus -ServerName "web-01" | Out-Null Should -Invoke Invoke-RestMethod -Times 1 -Exactly } }
|
执行结果示例:
1 2 3 4 5 6 7
| Describing Get-ServerStatus [+] 正常服务器应返回 healthy 8ms [+] 降级服务器应返回 degraded 3ms [+] 不可达服务器应返回 Unreachable 2ms [+] 应该调用 Invoke-RestMethod 15ms
Tests passed: 4, Failed: 0
|
注意事项
- Pester 5.x 语法:Pester 5 有重大语法变更(如
Context 内不支持直接写 It 外的代码),从 Pester 4 升级需注意
- 测试隔离:每个
It 块应该是独立的,不依赖其他测试的执行顺序。使用 BeforeAll/BeforeEach 初始化状态
- Mock 作用域:Mock 只在当前
Describe/Context 块内生效,不会影响外部代码
- CI/CD 集成:Pester 的 NUnit XML 输出可以直接被 Azure DevOps、GitHub Actions 等解析展示
- 测试命名:
It 的描述字符串应该清晰表达期望行为,失败时可直接从报告看出问题
- 覆盖率:Pester 不直接提供代码覆盖率,但可以结合
Invoke-CoverageAnalysis 等工具实现