plugins/languages/powershell/skills/testing/SKILL.md
PowerShell testing with Pester 5.x (Describe/Context/It, BeforeAll/AfterAll/BeforeEach, Should -Be / -Throw / -Match / -BeOfType, Mock with parameter filter, InModuleScope, TestDrive/TestRegistry, tags & filters, code coverage, NUnit XML output) and static analysis with PSScriptAnalyzer (custom rules, Invoke-ScriptAnalyzer -EnableExit, Settings.psd1). Covers CI integration (-CI flag), test discovery layout, fixtures, and migration notes from Pester 4 to 5. Use proactively when the user asks "写 PowerShell 测试 / pester 用例 / mock cmdlet / 静态分析 / PSScriptAnalyzer". Also triggers on "Pester", "Should", "Mock", "InModuleScope", "TestDrive", "Invoke-ScriptAnalyzer", "PSSA".
npx skillsauth add lazygophers/ccplugin powershell-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| 工具 | 角色 | 安装 |
|------|------|------|
| Pester 5.x | BDD 测试框架 | Install-PSResource Pester |
| PSScriptAnalyzer | 静态分析 | Install-PSResource PSScriptAnalyzer |
Pester 5 是 2026 主流,与 Pester 4 不向后兼容(行为差异详见迁移指南)。
Tests/MyModule.Tests.ps1:
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.5.0' }
BeforeAll {
$modulePath = Join-Path $PSScriptRoot '..' 'MyModule.psd1'
Import-Module $modulePath -Force
}
AfterAll {
Remove-Module MyModule -ErrorAction SilentlyContinue
}
Describe 'Get-Foo' -Tag 'Unit' {
Context 'when input is valid' {
BeforeEach {
$script:fixture = @{ Name = 'alpha' }
}
It 'returns the expected object' {
$result = Get-Foo -Name $fixture.Name
$result | Should -Not -BeNullOrEmpty
$result.Name | Should -Be 'alpha'
}
It 'has correct type' {
Get-Foo -Name $fixture.Name | Should -BeOfType [pscustomobject]
}
}
Context 'when input is invalid' {
It 'throws ArgumentException' {
{ Get-Foo -Name '' } | Should -Throw -ExceptionType ([System.ArgumentException])
}
}
}
| 断言 | 含义 |
|------|------|
| Should -Be 5 | 严格相等 |
| Should -BeExactly 'Foo' | 区分大小写 |
| Should -Not -BeNullOrEmpty | 非空 |
| Should -BeOfType [int] | 类型 |
| Should -Match 'pattern' | 正则 |
| Should -Contain 'item' | 集合包含 |
| Should -HaveCount 3 | 集合长度 |
| Should -Throw | 抛异常 |
| Should -Throw -ExceptionType ([IOException]) | 抛指定类型 |
| Should -Invoke -CommandName X -Times 1 | mock 调用次数 |
| Should -InvokeVerifiable | 全部 verifiable mock 都被调 |
Describe 'Send-Notification' {
BeforeAll {
Mock -CommandName Invoke-RestMethod -ModuleName MyModule -MockWith {
return @{ ok = $true }
}
}
It 'calls REST endpoint once' {
Send-Notification -Message 'hi'
Should -Invoke Invoke-RestMethod -Times 1 -Exactly -ModuleName MyModule
}
It 'mocks with parameter filter' {
Mock Invoke-RestMethod -ParameterFilter { $Uri -like '*api/v1/*' } -MockWith {
return @{ filtered = $true }
}
# ...
}
}
关键点:
-ModuleName,否则 mock 不生效。-ParameterFilter 用脚本块匹配特定参数组合。-Verifiable 标记后用 Should -InvokeVerifiable 一次性验证。InModuleScope MyModule {
Describe 'Internal helpers' {
It 'normalizes path' {
Get-NormalizedPath '/tmp//foo/' | Should -Be '/tmp/foo'
}
}
}
It 'writes to disk' {
$path = Join-Path $TestDrive 'out.txt'
Set-Content -Path $path -Value 'hi'
Get-Content $path | Should -Be 'hi'
# 测试结束自动清理 TestDrive
}
TestRegistry 同理但作用 Windows 注册表。
Describe 'integration' -Tag 'Integration', 'Slow' { ... }
# 跑特定标签
Invoke-Pester -Tag Unit
Invoke-Pester -ExcludeTag Slow
$config = New-PesterConfiguration
$config.Run.Path = './Tests'
$config.Run.Exit = $true # 失败时进程非零退出
$config.Output.Verbosity = 'Detailed'
$config.TestResult.Enabled = $true
$config.TestResult.OutputFormat = 'NUnitXml'
$config.TestResult.OutputPath = 'testResults.xml'
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = './Public', './Private'
$config.CodeCoverage.OutputFormat = 'JaCoCo'
$config.CodeCoverage.OutputPath = 'coverage.xml'
Invoke-Pester -Configuration $config
CI 一行:
Invoke-Pester -CI # 等价启用 Exit / NUnitXml / 覆盖率
PSScriptAnalyzerSettings.psd1@{
Severity = @('Error', 'Warning')
IncludeRules = @('PS*')
ExcludeRules = @(
'PSUseShouldProcessForStateChangingFunctions' # 视项目放宽
)
Rules = @{
PSAvoidUsingCmdletAliases = @{ Whitelist = @('cd', 'ls') }
PSPlaceOpenBrace = @{
Enable = $true
OnSameLine = $true
NewLineAfter = $true
IgnoreOneLineBlock = $true
}
PSPlaceCloseBrace = @{
Enable = $true
NewLineAfter = $true
IgnoreOneLineBlock = $true
NoEmptyLineBefore = $false
}
PSUseConsistentIndentation = @{
Enable = $true
IndentationSize = 4
Kind = 'space'
}
}
}
Invoke-ScriptAnalyzer -Path . -Recurse `
-Settings ./PSScriptAnalyzerSettings.psd1 `
-Severity Warning `
-EnableExit # CI:发现告警即非零退出
| 规则 | 含义 | |------|------| | PSAvoidUsingWriteHost | 数据走 Write-Output | | PSUseShouldProcessForStateChangingFunctions | 修改状态需 -WhatIf 支持 | | PSUseApprovedVerbs | 函数动词在 Get-Verb 列表 | | PSAvoidUsingPositionalParameters | 显式参数名 | | PSAvoidUsingPlainTextForPassword | 用 SecureString | | PSUseDeclaredVarsMoreThanAssignments | 死代码 | | PSAvoidGlobalVars | 不污染 $global: |
MyModule/
├── Public/Get-Foo.ps1
├── Private/Get-Internal.ps1
├── Tests/
│ ├── MyModule.Tests.ps1
│ ├── Get-Foo.Tests.ps1
│ └── Helpers/
│ └── TestHelpers.psm1
└── PSScriptAnalyzerSettings.psd1
约定:每个 Public 函数对应一个 *.Tests.ps1,或汇总到 MyModule.Tests.ps1。
- name: Lint
shell: pwsh
run: |
Install-PSResource PSScriptAnalyzer -TrustRepository
Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 -EnableExit
- name: Test
shell: pwsh
run: |
Install-PSResource Pester -TrustRepository
Invoke-Pester -CI
- uses: actions/upload-artifact@v4
with: { name: test-results, path: testResults.xml }
Describe/Context/It 语法保留,但内部块(BeforeAll 等)作用域更严。It 内只跑断言;fixtures 全挪到 BeforeAll/BeforeEach。Describe/Context 范围。New-PesterConfiguration,不再传 hashtable 给 Invoke-Pester。BeforeAll/BeforeEach 而非 It 内做 setup-ModuleNameInModuleScope 测$TestDrivePSScriptAnalyzerSettings.psd1 入版本控Invoke-Pester -CI + Invoke-ScriptAnalyzer -EnableExittools
--- name: trellisx-workspace description: 维护 `.trellis/task.md` 任务看板 —— trellis 缺的跨任务总览。**一个表格, 一行一个任务**, 列为 id/名称/描述/状态/阶段/进度/worktree (状态/阶段中文显示)。在 task create/start/阶段切换/archive 后**及时更新**对应行; 并**自动清理超 7 天的已完成行**防膨胀。保持看板与 task.json 实时一致。 when_to_use: 维护 / 创建 / 更新 `.trellis/task.md` 任务看板时; task 生命周期任一节点 (create/start/阶段推进/archive) 之后同步看板时; 用户问"当前有哪些任务 / 任务进度 / 任务看板"时。被 trellisx-flow 与 trellisx-apply 注入的流程引用。 user-invocable: true argument-hint: [show|update|sync|cleanup ...] [task id] arguments:
testing
强制以 Trellis task 闭环处理用户指定的请求 (自判新建/并入 → plan→exec→check→finish 全程不跳步)。**仅用户显式主动调用** (/trellisx-flow 或明确要求"强制走 task 处理这个"); **禁止自动 / 被动 / 推断式调用** —— 不要因为某个请求"看起来该建 task"就自动触发本 skill, 那是 apply 注入的 no_task 倾向的职责。
testing
把 强推task + subtask拆分 + worktree隔离 + 闭环收尾 四维度增量注入当前项目 .trellis/ (workflow.md 的 no_task/planning/in_progress 块 + spec 背书文档 + trellis 生命周期 hook worktree 自动化)。强推 task 与闭环为纯 prompt 软约束 (非平台 hook 硬拦截)。**纯增量追加, 绝不替换 trellis 原生文本** (no_task 分类+征同意/check/finish/前缀全保留)。幂等 (marker 包裹)。
development
Claude Code 会话历史整理 — 扫 ~/.claude/projects/**/*.jsonl 全部 session transcripts, 提取学习增量 (用户校正/决策/踩坑/L0 规则) → 全局记忆库 ~/.cortex/.wiki/memory/. 默认 --apply 落盘 (--dry-run opt-in 仅出 JSON plan 预览). 与 cortex-extract (L4-inbox 内部) 互补.