Если вы обнаружили, что тратите больше времени на обслуживание своих тестов Pester, чем на создание новых, этот пост для вас. В этом посте я поделюсь проектом, над которым работал с Devolutions.
Предыстория
У нас была проблема в Devolutions. Флагманский модуль PowerShell Devolutions.PowerShell не имел тестов Pester. Знаю, знаю. У них были тесты на C#, но это уже другая история.
В качестве консультанта для Devolutions, сосредоточенного на PowerShell, меня попросили создать набор тестов Pester, которые будут использоваться в CI/CD конвейере и запускаться перед развертыванием в продуктивной среде. Никаких проблем, подумал я. Я несколько раз использовал этот модуль, и это не может быть так сложно. Я ошибался.

Без каких-либо конкретных запросов, просто “создайте несколько тестов”, я начал создавать тесты для всего модуля, только чтобы обнаружить, что в нем почти 500 команд! Это займет некоторое время.
Поскольку я всегда хотел создавать решения не только на сегодня, но и на долгосрочную перспективу, я не хотел просто бросать кучу тестов в один скрипт и на этом заканчивать. Этот скрипт был бы огромным!
Вместо этого, прежде чем написать хоть один тест, я хотел разработать структуру, которую мог бы использовать, которая бы:
- Была масштабируемой – Я или другие могли бы легко добавлять тесты в структуру без особых усилий.
- Чтобы предотвратить дублирование кода, этот проект должен быть DRY (не повторяйте себя). Я не хотел дублировать никакой код, поэтому вместо этого использовал общие скрипты, функции и т.д.
- Чтобы использовать тесты на основе данных: Тесты на основе данных способствуют повторному использованию кода, определяя тесты в структуре данных, а затем позволяя тестам ссылаться на эту структуру, вместо того чтобы дублировать тестовый код для каждого теста.
- Будьте модульными – Все тесты должны быть разбиты на какую-то структуру, которая предотвратит необходимость прокручивать ненужный код, чтобы найти то, что вам нужно.
- Чтобы поддерживать сценарии настройки и завершения окружения – Эти тесты являются тестами “конец-к-концу” (E2E)/интеграционными/приемочными, как вам угодно. Они тестируют использование конечным пользователем; нет юнит-тестов, поэтому в окружении должно быть предусмотрено множество вещей для запуска.
- Чтобы поддерживать выполнение групп тестов без необходимости запускать их все сразу.
Эти требования приводят к фреймворку тестирования Pester для Devolutions.
Предварительные условия
Если вы хотите использовать этот фреймворк, я не могу гарантировать, что он будет работать на 100%, если вы не работаете в том же окружении, что и я. Для использования этого фреймворка я использовал:
- PowerShell v7.4.5 на Windows
- 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 для каждой группы и подгруппы.
контекст 'группа' { контекст 'подгруппа' { } }
- Вызывает скрипт
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
Каждая конфигурация окружения, с которой вам нужно запускать тесты, делится на контексты с тестовыми группами под каждой из них.
Для модуля PowerShell Devolutions, поскольку нам нужно тестировать командлеты против множества различных типов источников данных, я создал контексты по источникам данных, но вы можете использовать здесь что угодно. Затем, используя параметр ForEach
в Pester, каждая папка с определениями тестов является контекстом, как и каждая подгруппа. Затем каждый тест определяется в блоках it
.
Обратите внимание на метод where()
в блоке it
. 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/