如果你發現自己花更多時間在維護 Pester 測試上,而不是實際創建新測試,那麼這篇文章就是為你準備的。在這篇文章中,我將分享我與 Devolutions 合作的一個項目。
背景故事
我們在 Devolutions 遇到了一個問題。旗艦的 PowerShell 模組 Devolutions.PowerShell 缺少 Pester 測試。我知道,我知道。他們有 C# 測試,但那是另一回事。
作為一名專注於 PowerShell 的 Devolutions 顧問,我被要求創建一套 Pester 測試,以便在 CI/CD 管道中使用並在生產部署之前執行。我想,這沒問題。我使用過這個模組幾次,應該不會太困難。結果我錯了。

在沒有任何具體要求的情況下,只是“建立一些測試”,我開始為整個模組構建測試,結果發現它有將近 500 個命令!這將需要一段時間。
因為我一直想要構建不僅僅是為了今天而是為了長期的解決方案,我不想將一堆測試放入一個腳本中就算了。那個腳本會非常龐大!
相反,在寫第一個測試之前,我想設計一個可以使用的框架,它應該:
- 可擴展 – 我自己或其他人可以輕鬆地將測試添加到框架中,而不需要太多考慮。
- 為了防止代碼重複,這個項目需要遵循 DRY 原則(不要重複自己)。我不想重複任何代碼,因此我使用了共享腳本、函數等。
- 要使用數據驅動測試:數據驅動測試通過在數據結構中定義測試來促進代碼重用,然後讓測試參考該結構,而不是為每個測試重複測試代碼。
- 模塊化 – 所有測試必須分拆成某種結構,以防止需要滾動過多不必要的代碼來找到所需的內容。
- 支持環境設置和拆解場景 – 這些測試是端到端(E2E)/集成/驗收測試,無論你想稱之為什麼。它們測試最終用戶的使用情況;沒有單元測試,因此必須在環境中設置各種內容才能運行。
- 支持無需一次性運行所有測試的測試組運行。
這些要求導致了Devolutions的Pester測試框架。
先決條件
如果你想使用這個框架,我不能保證它100%有效,除非你在與我相同的環境中工作。要使用這個框架,我使用了:
- Windows上的PowerShell v7.4.5
- Pester 5.6.1
這仍然可以在Windows PowerShell和早期版本的Pester v5上運行,但不能保證!
框架概述
這個框架由四個組件組成:一個調用腳本、各種測試定義腳本、一個可選的存儲輔助函數的腳本,以及可選的before all
/ before each
腳本。這些組件在文件系統中的結構如下:
📁 root/ 📄 caller.tests.ps1 📄 helper_functions.ps1 📁 test_definitions/ 📁 group1/ 📄 beforeall.tests.ps1 📄 beforeeach.tests.ps1 📄 core.tests.ps1 📄 subgroup.tests.ps1
當你調用調用腳本時:
Invoke-Pester -Path root/caller.tests.ps1
調用腳本:
- 在每個組中查找所有測試定義腳本。
- 查找每個組的所有
beforeeach
和beforeall
腳本。 - 為每個組和子組創建 Pester 上下文。
上下文 'group' { 上下文 'subgroup' { } }
- 在組中任何測試運行之前調用一次
beforeall
腳本。 - 在組中的每個測試之前調用
beforeeach
腳本。 - 最後,它調用每個測試定義中定義的測試斷言。
調用測試腳本
調用腳本是 Pester 的調用點。這是 Invoke-Pester
在需要調用框架中的任何測試時所調用的腳本。
調用腳本分為 X 個區域:
BeforeDiscovery 區塊
調用腳本的一個任務是找到所有測試定義以及 Pester 的發現階段的目的。在腳本的 BeforeDiscovery 區塊中,收集測試定義,以及任何 before each
或 before all
腳本。
BeforeDiscovery { # Initialize hashtable to store all test definitions $tests = @{} # Iterate through test group directories (e.g. datasources, entries) Get-ChildItem -Path "$PSScriptRoot\test_definitions" -Directory -PipelineVariable testGroupDir | ForEach-Object { $testGroup = $testGroupDir.BaseName $tests[$testGroup] = @{} # Load beforeall script for test group if it exists # This script runs once before all tests in the group $testGroupBeforeAllScriptPath = Join-Path -Path $testGroupDir.FullName -ChildPath 'beforeall.ps1' if (Test-Path -Path $testGroupBeforeAllScriptPath) { $tests[$testGroup]['beforeall'] += . $testGroupBeforeAllScriptPath } # Load beforeeach script for test group if it exists # This script runs before each individual test in the group $testGroupBeforeEachScriptPath = Join-Path -Path $testGroupDir.FullName -ChildPath 'beforeeach.ps1' if (Test-Path -Path $testGroupBeforeEachScriptPath) { $tests[$testGroup]['beforeeach'] += . $testGroupBeforeEachScriptPath } # Load all test definition files in the group directory Get-ChildItem -Path $testGroupDir.FullName -Filter '*.ps1' -PipelineVariable testDefinitionFile | ForEach-Object { $tests[$testGroup][$testDefinitionFile.BaseName] = @() $tests[$testGroup][$testDefinitionFile.BaseName] += . $testDefinitionFile.FullName } } }
BeforeAll
區塊
BeforeAll
區塊運行以使任何輔助函數可用於調用腳本或測試定義。在 Pester v5 中,這個任務不能在 BeforeDiscovery
中;否則,它將無法對測試可用。
# Load helper functions used across all tests BeforeAll { . (Join-Path $PSScriptRoot -ChildPath "_helper_functions.ps1") }
Describe
區塊和 Contexts
您需要運行測試的每個環境配置被拆分為上下文,並在每個上下文下有測試組。
對於 Devolutions PowerShell 模組,由於我們需要針對許多不同類型的數據源進行測試,我按數據源創建了上下文,但您可以在這裡使用任何內容。然後,使用 Pester 的 ForEach
參數,每個測試定義文件夾都是一個上下文,每個子組也是一個上下文。然後,每個測試定義在其中作為 it
塊。
請注意 it
塊上的 where()
方法。 Where({ $_.environments -contains 'xxx' -or $_.environments.count -eq 0})
。這是每個定義結構發揮作用的地方。這部分是我們指定哪些測試在哪個環境運行的地方。
# Main test container for all tests Describe 'RDM' { # Tests that run against an environment Context 'Environment1' -Tag 'Environment1' { BeforeEach { ## Do stuff to execute before each test in this content. For example, this is used to set up a specific data source for Devolutions Remote Desktop Manager } # Clean up AfterAll { } # Run each test group against the environment Context '<_>' -ForEach $tests.Keys -Tag ($_) { $testGroup = $_ # Run each test subgroup (core, properties, etc) Context '<_>' -ForEach $tests[$testGroup].Keys -Tag ($_) { $testSubGroup = $_ # Run tests marked for this environment or all It '<name>' -ForEach ($tests[$testGroup][$testSubGroup]).Where({ $_.environments -contains 'xxx' -or $_.environments.count -eq 0}) { & $_.assertion } } } } ## Other environments. If you have specific configurations some tests must have, you can define them here by creating more context blocks. }
測試定義
接下來,您將看到最重要的部分:測試!測試是在每個組文件夾內部的子組(subgroup.tests.ps1
)中創建的,必須按以下結構創建為哈希表數組:
@( @{ 'name' = 'creates a thing' 'environments' = @() ## Nothing means all environments 'assertion' = { ## Teating something $true | should -Be $true } } )
在這裡,您可以在調用腳本中定義環境以執行腳本。例如,如果您為 Devolutions RDM 使用的每個數據源設置一個環境,我的環境是 xml
、sqllite
等。
輔助函數
最後,我們有輔助函數腳本。此腳本包含我們在調用腳本中的 BeforeAll
塊中點源的函數。這是您可以放置任何要重複使用的函數的地方。在我的示例中,我有一些設置數據源和刪除所有數據源的函數。
# Helper function to remove all entries from the current data source # Used for cleanup in test scenarios function Remove-AllEntries { try { # Get all entries and their IDs # Using ErrorAction SilentlyContinue to handle case where no entries exist $entries = @(Get-RDMEntry -ErrorAction SilentlyContinue) # If no entries found, just return silently # No cleanup needed in this case if ($entries.Count -eq 0) { return } # Delete entries one at a time # Using foreach loop to handle errors for individual entries foreach ($entry in $entries) { try { # Remove entry and refresh to ensure UI is updated Remove-RDMEntry -ID $entry.ID -Refresh -ErrorAction Stop } catch [System.Management.Automation.ItemNotFoundException] { # Silently ignore if entry is already gone # This can happen if entry was deleted by another process continue } } } catch { # Only warn about unexpected errors # Ignore "Connection not found" as this is expected in some cases if ($_.Exception.Message -ne 'Connection not found.') { Write-Warning "Error during cleanup: $_" } } } # Helper function to remove all data sources and their files # Used for cleanup in test scenarios function Remove-AllDatasources { # Remove any data sources currently loaded in memory # This ensures clean state for tests Get-RDMDataSource | ForEach-Object { Remove-RDMDataSource -DataSource $_ } # Delete any existing data source folders on disk # These are identified by GUIDs in the RDM application data folder Get-ChildItem $env:LOCALAPPDATA\Devolutions\RemoteDesktopManager | Where-Object { $_.Name -match '^[0-9a-fA-F\-]{36} }
如何構建您的框架
這個框架看起來實用嗎?你認為它對你的組織會有幫助嗎?如果是,這裡有幾個提示和問題可以思考。
- 你預計需要多少測試?你的使用案例足以支持這個框架,而不僅僅是幾個測試腳本嗎?
- 在編寫任何 PowerShell 之前,描述一下你預計如何定義群組和子群組。讓它們成為你正在測試的任何元素的邏輯抽象。
- 考慮一下每組測試所需的環境和配置;將這些作為上下文創建在調用腳本中。
- 在構建測試定義時,如果你發現自己重複代碼,為它創建一個函數並將其放入 helper_functions.ps1。
構建一個可擴展的 Pester 測試框架需要仔細的規劃和組織,但這些好處是值得付出的努力。通過遵循本文所述的結構——其模組化設計、可重用組件和靈活的環境處理——你可以創建一個隨著需求增長而增強的堅固測試解決方案。無論你是在測試 PowerShell 模組、基礎設施配置,還是複雜應用程序,這個框架都為維護整個組織的代碼質量和可靠性提供了堅實的基礎。
Source:
https://adamtheautomator.com/powershell-pester-testing-framework/