Se você se encontra gastando mais tempo mantendo seus testes Pester do que realmente criando novos, este post é para você. Neste post, vou compartilhar um projeto com o qual tenho trabalhado com a Devolutions.
Contexto
Tivemos um problema na Devolutions. O módulo PowerShell principal Devolutions.PowerShell estava sem testes Pester. Eu sei, eu sei. Eles tinham testes em C#, mas essa é outra história.
Como consultor da Devolutions focado em PowerShell, fui solicitado a criar um conjunto de testes Pester para ser usado no pipeline CI/CD e executado antes da implantação em produção. Sem problemas, pensei. Eu usei o módulo algumas vezes e não deveria ser tão difícil. Eu estava errado.

Sem nenhum pedido específico, apenas “crie alguns testes”, comecei a criar testes para o módulo inteiro, apenas para descobrir que ele tinha quase 500 comandos! Isso ia levar um tempo.
Como sempre quis construir soluções não apenas para o presente, mas também para o longo prazo, não queria simplesmente jogar um monte de testes em um único script e dar por encerrado. Esse script seria enorme!
Em vez disso, antes de escrever um único teste, eu queria projetar um framework que pudesse usar que:
- Seja escalável – Eu ou outros poderíamos facilmente adicionar testes ao framework sem muita complicação.
- Para evitar a duplicação de código, este projeto precisa ser DRY (não se repita). Eu não queria duplicar nenhum código, então em vez disso usei scripts compartilhados, funções, etc.
- Para usar testes orientados por dados: Os testes orientados por dados promovem a reutilização de código, definindo testes em uma estrutura de dados e então tendo os testes referenciarem essa estrutura em vez de duplicar o código de teste para cada teste.
- Ser modular – Todos os testes devem ser divididos em alguma estrutura que evite ter que rolar por código desnecessário para encontrar o que você está procurando.
- Para apoiar cenários de configuração e limpeza de ambiente – Esses testes são de ponta a ponta (E2E)/integração/aceitação, o que você preferir. Eles estão testando o uso do usuário final; não há testes unitários, então eles devem ter várias coisas no ambiente para serem executados.
- Para suportar a execução de grupos de testes sem executá-los todos de uma vez.
Esses requisitos levam ao framework de teste Pester para a Devolutions.
Pré-requisitos
Se você quiser usar este framework, não posso garantir que funcionará 100% a menos que você esteja trabalhando no mesmo ambiente que eu. Para usar este framework, utilizei:
- PowerShell v7.4.5 no Windows
- Pester 5.6.1
Isso ainda funcionará no Windows PowerShell e em versões anteriores do Pester v5, mas sem garantias!
Visão geral do framework
Este framework consiste em quatro componentes: um script de chamada, vários scripts de definição de teste, um script opcional para armazenar funções auxiliares e scripts opcionais before all
/ before each
. Cada um desses componentes é estruturado assim no sistema de arquivos:
📁 root/ 📄 caller.tests.ps1 📄 helper_functions.ps1 📁 test_definitions/ 📁 group1/ 📄 beforeall.tests.ps1 📄 beforeeach.tests.ps1 📄 core.tests.ps1 📄 subgroup.tests.ps1
Quando você invoca o script de chamada:
Invoke-Pester -Path root/caller.tests.ps1
O script de chamada:
- Encontra todos os scripts de definição de teste em cada grupo.
- Encontra todos os scripts
beforeeach
ebeforeall
para cada grupo. - Cria contextos do Pester para cada grupo e subgrupo.
contexto 'grupo' { contexto 'subgrupo' { } }
- Invoca o script
beforeall
uma vez antes de qualquer execução de teste em um grupo. - Invoca o script
beforeeach
antes de cada teste no grupo. - Finalmente, invoca as asserções de teste definidas em cada definição de teste.
O Script de Teste Chamador
O script chamador é o ponto de invocação do Pester. É o script que o Invoke-Pester
chama quando é necessário invocar quaisquer testes dentro do framework.
O script chamador é dividido em X áreas:
O Bloco BeforeDiscovery
Uma tarefa do script chamador é encontrar todas as definições de teste e o que é a fase de descoberta do Pester. Dentro do bloco BeforeDiscovery do script, as definições de teste são reunidas, assim como quaisquer scripts before each
ou 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 } } }
O Bloco BeforeAll
O bloco BeforeAll
é executado para disponibilizar quaisquer funções auxiliares para o script chamador ou definições de teste. No Pester v5, essa tarefa não pode estar em BeforeDiscovery
; caso contrário, não estaria disponível para os testes.
# Load helper functions used across all tests BeforeAll { . (Join-Path $PSScriptRoot -ChildPath "_helper_functions.ps1") }
O Bloco Describe
e Contexts
Cada configuração de ambiente que você precisa executar testes é dividida em contextos com grupos de teste abaixo de cada um deles.
Para o módulo Devolutions PowerShell, uma vez que precisamos testar cmdlets contra muitos tipos diferentes de fontes de dados, criei contextos por fonte de dados, mas você pode usar qualquer coisa aqui. Em seguida, usando o parâmetro ForEach
do Pester, cada pasta de definição de teste é um contexto, assim como cada subgrupo. Em seguida, cada teste é definido abaixo como blocos it
.
Observe o método where()
no bloco it
. Where({ $_.ambientes -contém 'xxx' -ou $_.ambientes.count -eq 0})
. Aqui é onde a estrutura de cada definição entra em jogo. Esta parte é onde designamos quais testes são executados em qual ambiente.
# 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. }
Definições de Teste
Em seguida, temos a parte mais essencial: os testes! Os testes são criados em subgrupos (subgrupo.tests.ps1
) dentro de cada pasta de grupo e devem ser criados como um array de hashtables com a seguinte estrutura:
@( @{ 'name' = 'creates a thing' 'environments' = @() ## Nothing means all environments 'assertion' = { ## Teating something $true | should -Be $true } } )
Aqui, você pode definir os ambientes no script de chamada para executar os scripts. Por exemplo, se você tiver um ambiente para cada fonte de dados que eu usei para o Devolutions RDM, meus ambientes são xml
, sqllite
, etc.
Funções Auxiliares
Por fim, temos o script de funções auxiliares. Este script contém funções que nós carregamos no bloco BeforeAll
no script de chamada. Aqui é onde você pode colocar quaisquer funções que você estará reutilizando. No meu exemplo, tenho funções para configurar fontes de dados e removê-las.
# 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} }
Como Construir Seu Framework
Este framework parece útil? Você acha que seria benéfico para a sua organização? Se sim, aqui estão algumas dicas e perguntas para pensar.
- Quantos testes você prevê que serão necessários? Seu caso de uso é suficiente para suportar este framework em vez de apenas alguns scripts de teste?
- Antes de escrever qualquer PowerShell, descreva como você prevê a definição de grupos e subgrupos. Faça deles uma abstração lógica dos elementos que você está testando.
- Pense sobre os ambientes e as configurações necessárias para cada conjunto de testes; crie-os como contextos no script chamador.
- Ao construir definições de testes, se você se pegar repetindo código, crie uma função para ele e coloque em funções_auxiliares.ps1.
Construir um framework de teste Pester escalável requer planejamento e organização cuidadosos, mas os benefícios valem o esforço. Seguindo a estrutura delineada neste artigo – com seu design modular, componentes reutilizáveis e tratamento flexível de ambiente – você pode criar uma solução de teste robusta que cresce com suas necessidades. Seja testando módulos PowerShell, configurações de infraestrutura ou aplicações complexas, este framework fornece uma base sólida para manter a qualidade e a confiabilidade do código em toda a sua organização.
Source:
https://adamtheautomator.com/powershell-pester-testing-framework/