Si te encuentras pasando más tiempo manteniendo tus pruebas Pester que creando nuevas, este post es para ti. En esta publicación, compartiré un proyecto en el que he estado trabajando con Devolutions.
Antecedentes
Teníamos un problema en Devolutions. El módulo principal de PowerShell, Devolutions.PowerShell, carecía de pruebas Pester. Lo sé, lo sé. Tenían pruebas en C#, pero esa es otra historia.
Como consultor de Devolutions enfocado en PowerShell, se me pidió crear un conjunto de pruebas Pester para ser utilizado en el pipeline de CI/CD y ejecutarse antes de la implementación en producción. No había problema, pensé. Había usado el módulo algunas veces y no debería ser tan difícil. Estaba equivocado.

Sin ninguna solicitud específica, solo “crear algunas pruebas”, me dispuse a construir pruebas para todo el módulo, ¡solo para descubrir que tenía casi 500 comandos! Esto iba a llevar un tiempo.
Dado que siempre quise construir soluciones no solo para hoy, sino también para el largo plazo, no quería simplemente lanzar un montón de pruebas en un solo script y dar por terminado. ¡Ese script sería enorme!
En cambio, antes de escribir una sola prueba, quería diseñar un marco que pudiera utilizar y que:
- Fuera escalable: yo u otros pudiéramos agregar pruebas al marco sin pensarlo mucho.
- Para evitar la duplicación de código, este proyecto necesita ser DRY (no te repitas). No quería duplicar ningún código, así que en su lugar utilicé scripts compartidos, funciones, etc.
- Para usar pruebas basadas en datos: Las pruebas basadas en datos promueven la reutilización de código al definir pruebas en una estructura de datos y luego hacer que las pruebas hagan referencia a esa estructura en lugar de duplicar el código de prueba para cada prueba.
- Ser modular: Todas las pruebas deben dividirse en alguna estructura que evite tener que desplazarse por un código innecesario para encontrar lo que estás buscando.
- Para admitir escenarios de configuración y limpieza de entorno: Estas pruebas son de extremo a extremo (E2E)/integración/aceptación, lo que desees. Están probando el uso del usuario final; no hay pruebas unitarias, por lo que deben tener varias cosas en su lugar en el entorno para ejecutarse.
- Para admitir la ejecución de grupos de pruebas sin ejecutarlas todas al mismo tiempo.
Estos requisitos conducen al marco de pruebas Pester para Devolutions.
Prerrequisitos
Si deseas utilizar este marco, no puedo garantizar que funcione al 100% a menos que estés trabajando en el mismo entorno que yo. Para usar este marco, utilicé:
- PowerShell v7.4.5 en Windows
- Pester 5.6.1
Esto seguirá funcionando en Windows PowerShell y versiones anteriores de Pester v5, ¡pero no hay garantías!
Descripción general del marco
Este marco consta de cuatro componentes: un script de llamada, varios scripts de definición de pruebas, un script opcional para almacenar funciones auxiliares y scripts opcionales before all
/ before each
. Cada uno de estos componentes está estructurado de la siguiente manera en el sistema de archivos:
📁 root/ 📄 caller.tests.ps1 📄 helper_functions.ps1 📁 test_definitions/ 📁 group1/ 📄 beforeall.tests.ps1 📄 beforeeach.tests.ps1 📄 core.tests.ps1 📄 subgroup.tests.ps1
Cuando invocas el script de llamada:
Invoke-Pester -Path root/caller.tests.ps1
El script de llamada:
- Encuentra todos los scripts de definición de pruebas en cada grupo.
- Encuentra todos los scripts
beforeeach
ybeforeall
para cada grupo. - Crea contextos de Pester para cada grupo y subgrupo.
contexto 'grupo' { contexto 'subgrupo' { } }
- Invoca el script
beforeall
una vez antes de que se ejecuten las pruebas en un grupo. - Invoca el script
beforeeach
antes de cada prueba en el grupo. - Finalmente, invoca las aserciones de prueba definidas en cada definición de prueba.
El Script de Prueba del Llamador
El script del llamador es el punto de invocación de Pester. Es el script que llama Invoke-Pester
cuando necesitas invocar cualquier prueba dentro del marco de trabajo.
El script del llamador se divide en X áreas:
El Bloque BeforeDiscovery
Una tarea del script del llamador es encontrar todas las definiciones de pruebas y para qué es la fase de descubrimiento de Pester. En el bloque BeforeDiscovery del script, se recopilan las definiciones de pruebas, así como cualquier script before each
o 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 } } }
El Bloque BeforeAll
El bloque BeforeAll
se ejecuta para hacer que las funciones auxiliares estén disponibles para el script del llamador o las definiciones de pruebas. En Pester v5, esta tarea no puede estar en BeforeDiscovery
; de lo contrario, no estaría disponible para las pruebas.
# Load helper functions used across all tests BeforeAll { . (Join-Path $PSScriptRoot -ChildPath "_helper_functions.ps1") }
El Bloque Describe
y Contextos
Cada configuración de entorno que necesitas para ejecutar pruebas se divide en contextos con grupos de pruebas debajo de cada uno de ellos.
Para el módulo de PowerShell de Devolutions, dado que necesitamos probar cmdlets contra muchos tipos diferentes de fuentes de datos, creé contextos por fuente de datos, pero puedes usar cualquier cosa aquí. Luego, usando el parámetro ForEach
de Pester, cada carpeta de definición de prueba es un contexto, al igual que cada subgrupo. Luego, cada prueba se define debajo como bloques it
.
Nota el método where()
en el bloque it
. Where({ $_.environments -contains 'xxx' -or $_.environments.count -eq 0})
. Aquí es donde la estructura de cada definición entra en juego. Esta parte es donde designamos qué pruebas se ejecutan en qué entorno.
# 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. }
Definiciones de Prueba
A continuación, tienes la parte más esencial: ¡las pruebas! Las pruebas se crean en subgrupos (subgroup.tests.ps1
) dentro de cada carpeta de grupo y deben crearse como un array de hashtables con la siguiente estructura:
@( @{ 'name' = 'creates a thing' 'environments' = @() ## Nothing means all environments 'assertion' = { ## Teating something $true | should -Be $true } } )
Aquí, puedes definir los entornos en el script llamador para ejecutar los scripts. Por ejemplo, si tienes un entorno para cada fuente de datos que utilicé para Devolutions RDM, mis entornos son xml
, sqllite
, etc.
Funciones Auxiliares
Finalmente, tenemos el script de funciones auxiliares. Este script contiene funciones que incluimos en el bloque BeforeAll
en el script llamador. Aquí es donde puedes poner cualquier función que vayas a reutilizar. En mi ejemplo, tengo funciones para configurar fuentes de datos y eliminar todas ellas.
# 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} }
Cómo Construir Tu Marco
¿Consideras útil este marco de trabajo? ¿Crees que sería beneficioso para tu organización? Si es así, aquí tienes algunos consejos y preguntas en las que pensar.
- ¿Cuántas pruebas anticipas que necesitarás? ¿Es tu caso de uso suficiente para respaldar este marco en lugar de solo un par de scripts de prueba?
- Antes de escribir cualquier PowerShell, describe cómo anticipas definir grupos y subgrupos. Haz que sean una abstracción lógica de los elementos que estás probando.
- Considera los entornos y la configuración que necesitarías para cada conjunto de pruebas; crea esos como contextos en el script del llamador.
- Al construir definiciones de pruebas, si te encuentras repitiendo código, crea una función para ello y colócala en helper_functions.ps1.
Crear un marco de pruebas Pester escalable requiere una planificación y organización cuidadosas, pero los beneficios valen el esfuerzo. Siguiendo la estructura descrita en este artículo, con su diseño modular, componentes reutilizables y manejo flexible de entornos, puedes crear una solución de pruebas sólida que crece con tus necesidades. Ya sea que estés probando módulos de PowerShell, configuraciones de infraestructura o aplicaciones complejas, este marco proporciona una base sólida para mantener la calidad y confiabilidad del código en toda tu organización.
Source:
https://adamtheautomator.com/powershell-pester-testing-framework/