Mantenimiento de pruebas de PowerShell con Pester — un patrón práctico para que los scripts operativos sean más resistentes a la rotura
· Actualizado el: · Go Komura · PowerShell, Pester, Windows, Pruebas, Automatización, CI, Aprovechamiento de activos existentes
1. Lo primero que conviene tener claro
Los scripts de PowerShell empiezan casi siempre como la automatización de una tarea pequeña.
Reunir archivos. Buscar en los registros. Generar un CSV. Mover archivos antiguos. Comprobar el estado de un servicio.
Si cada uno tiene unas pocas decenas de líneas, basta con revisarlo a simple vista para confirmar que funciona. Sin embargo, a medida que se sigue usando en el trabajo diario, van llegando cambios como estos.
- Aumentar las carpetas objetivo
- Añadir condiciones de exclusión
- Cambiar las columnas del CSV
- Archivar antes de eliminar
- Ejecutarlo desde el Programador de tareas o desde CI
- Notificar cuando se produce un error
Llegados a este punto, ya no basta con que «haya funcionado una vez en el equipo local».
El peligro de PowerShell es inseparable de su comodidad. Las operaciones de solo lectura se pueden probar con tranquilidad, pero procesos como eliminar, mover, sobrescribir, reiniciar un servicio o cambiar permisos convierten un pequeño error de condición en un incidente.
Ahí es donde entra Pester, el framework de pruebas para PowerShell. Este artículo no pretende cubrir todas las funciones de Pester, sino ordenar cómo avanzar en el «mantenimiento de pruebas» para que los scripts de PowerShell ya existentes sean más resistentes a la rotura en el uso diario.
Las pruebas de PowerShell no sirven solo para escribir código limpio. Son una herramienta para reducir la incertidumbre antes de un cambio y para confirmarlo con fundamento después.
El código que aparece en este artículo está publicado en GitHub como un conjunto de muestra ejecutable con Invoke-Pester (el script a probar, las pruebas de Pester y el script de ejecución para CI).
pester-powershell-test-maintenance - komurasoft-blog-samples (GitHub)
2. Qué proteger con Pester
Instalar Pester no hace que todo se vuelva seguro automáticamente. Lo primero que hay que decidir es «qué se va a proteger con las pruebas».
En los scripts operativos de PowerShell, suele dar buen resultado priorizar estos cuatro puntos.
| Qué proteger | Qué comprobar en la prueba |
|---|---|
| Evaluación de condiciones | Qué archivos, filas, usuarios o servicios son el objetivo |
| Forma de la salida | Nombres de columna del CSV, propiedades del valor de retorno, número de elementos |
| Paso previo a la operación peligrosa | Si el objetivo de eliminar, mover o detener es el previsto |
| Dependencias externas | El manejo del sistema de archivos, la API, la ejecución de comandos, la fecha y hora, y las variables de entorno |
Lo primero que conviene probar no es el propio proceso de eliminación, sino el proceso que selecciona qué eliminar.
Por ejemplo, en un script que elimina registros antiguos, en lugar de probar directamente Remove-Item, conviene probar antes «qué registros se seleccionan como objetivo».
Separarlo de esta manera facilita las pruebas.
Función que reúne los objetivos
↓
Proceso que verifica y registra los objetivos
↓
Proceso de cambio: mover, eliminar, notificar, etc.
El mantenimiento de pruebas de PowerShell no consiste en rediseñar de golpe un script existente. Primero se extrae como función la parte de decisión que precede a la operación peligrosa, y se comprueba su valor de retorno con Pester.
3. Unificar la versión
Este artículo parte de la base de Pester v5.
En entornos Windows antiguos, es posible que Pester ya venga instalado, pero en la serie v3. En lugar de usar tal cual lo que ya está en el entorno, conviene comprobar primero la versión.
Get-Module Pester -ListAvailable |
Sort-Object Version -Descending |
Select-Object Name, Version, Path
Si va a instalarlo de nuevo, hágalo desde PowerShell Gallery.
Install-Module -Name Pester -Scope CurrentUser -Force -SkipPublisherCheck
Import-Module Pester
Get-Module Pester
Se añade -SkipPublisherCheck porque el Pester antiguo que viene incluido con Windows está firmado por Microsoft. Sin esta opción, la instalación se detiene porque el editor es distinto.
Tenga en cuenta que, en equipos corporativos, este comando puede no funcionar tal cual. El proxy, TLS 1.2 y el proveedor de NuGet son las causas típicas. El tratamiento de estos casos se resume en el capítulo 17.
Si trabaja en equipo, compruebe que la versión de Pester no difiera entre el equipo de desarrollo local, el servidor de compilación y el entorno de ejecución de tareas.
Una confusión habitual en las pruebas de PowerShell no proviene de un problema del código, sino de la diferencia de versión del ejecutor de pruebas.
En particular, artículos antiguos o notas internas pueden conservar la forma de escritura de Pester v4 o anterior. Si va a organizar las pruebas de nuevo, acercarse a la forma de escritura de v5 facilita la lectura más adelante.
4. Decidir dónde colocar los archivos
En Pester, lo habitual es nombrar los archivos de prueba con el patrón *.Tests.ps1.
La estructura mínima sería así.
scripts/
Get-OldLogFile.ps1
Get-OldLogFile.Tests.ps1
Si el proyecto es algo más grande, se separan src y tests.
src/
public/
Get-OldLogFile.ps1
Remove-OldLogFile.ps1
tests/
public/
Get-OldLogFile.Tests.ps1
Remove-OldLogFile.Tests.ps1
Cualquiera de las dos opciones sirve. Lo importante es fijar una convención.
- Colocar un archivo de prueba por cada función
- Añadir
.Tests.ps1al nombre del archivo de prueba - Unificar la forma de cargar el objeto a probar
- No mezclar demasiado las pruebas unitarias con las de integración
Al principio basta con colocar el .ps1 del objeto y el .Tests.ps1 de la prueba uno junto al otro.
5. Ejecutar la prueba mínima
Primero, se prepara una función sencilla, Get-OldLogFile.ps1.
function Get-OldLogFile {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $Path,
[int] $Days = 30,
[string] $Filter = '*.log',
[datetime] $Now = (Get-Date)
)
if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
throw "Folder not found: $Path"
}
$limit = $Now.AddDays(-1 * $Days)
Get-ChildItem -LiteralPath $Path -Filter $Filter -File |
Where-Object { $_.LastWriteTime -lt $limit } |
Sort-Object -Property LastWriteTime |
Select-Object FullName, Name, Length, LastWriteTime
}
Aquí, para facilitar las pruebas, se permite recibir $Now como argumento.
Si dentro de la función se usa directamente Get-Date cada vez, el resultado cambia según el día en que se ejecute la prueba. Si la fecha se recibe como argumento, se puede probar con una condición fija, como «los archivos más antiguos de 30 días a fecha del 1 de junio de 2026».
A continuación, se escribe la prueba en Get-OldLogFile.Tests.ps1.
BeforeAll {
. $PSScriptRoot\Get-OldLogFile.ps1
}
Describe 'Get-OldLogFile' {
BeforeEach {
$script:Root = Join-Path $TestDrive 'logs'
New-Item -ItemType Directory -Path $script:Root -Force | Out-Null
$oldLog = Join-Path $script:Root 'old.log'
$newLog = Join-Path $script:Root 'new.log'
$oldTxt = Join-Path $script:Root 'old.txt'
Set-Content -LiteralPath $oldLog -Value 'old log' -Encoding UTF8
Set-Content -LiteralPath $newLog -Value 'new log' -Encoding UTF8
Set-Content -LiteralPath $oldTxt -Value 'old text' -Encoding UTF8
(Get-Item -LiteralPath $oldLog).LastWriteTime = [datetime]'2026-05-01T00:00:00'
(Get-Item -LiteralPath $newLog).LastWriteTime = [datetime]'2026-05-31T00:00:00'
(Get-Item -LiteralPath $oldTxt).LastWriteTime = [datetime]'2026-05-01T00:00:00'
}
It 'devuelve solo los archivos .log más antiguos que el número de días indicado' {
$result = Get-OldLogFile `
-Path $script:Root `
-Days 30 `
-Now ([datetime]'2026-06-01T00:00:00')
$result | Should -HaveCount 1
$result[0].Name | Should -Be 'old.log'
}
It 'falla cuando la carpeta no existe' {
{ Get-OldLogFile -Path (Join-Path $TestDrive 'missing') } |
Should -Throw
}
}
Se ejecuta.
Invoke-Pester -Output Detailed .\Get-OldLogFile.Tests.ps1
$TestDrive, que se usa aquí, es un área temporal para pruebas que proporciona Pester. Permite usar archivos creados solo dentro de la prueba, sin tocar la ruta real C:\Logs ni una carpeta compartida. En los scripts de PowerShell que implican operaciones de archivo, resulta más seguro acostumbrarse a usar primero $TestDrive.
6. Escribir el nombre de la prueba como especificación
La cadena de texto que se escribe en el It de Pester no es una simple descripción: para quien la lea más adelante, se convierte en una pequeña especificación.
Por ejemplo, un nombre como este resulta un poco débil.
It 'works' {
# ...
}
No queda claro qué es lo que debe funcionar.
En la práctica, resulta más legible incluir en el nombre la condición y el resultado esperado.
It 'devuelve solo los archivos .log más antiguos que el número de días indicado' {
# ...
}
It 'no incluye el archivo justo en la fecha límite' {
# ...
}
It 'falla cuando la carpeta no existe' {
# ...
}
Un buen nombre de prueba resulta útil cuando algo falla. Cuando el registro de CI muestra esto, se entiende de inmediato qué se ha roto.
[-] Get-OldLogFile.no incluye el archivo justo en la fecha límite
El nombre de la prueba es una nota para el yo del futuro.
7. Añadir una condición límite
El Get-OldLogFile visto antes decide qué archivos son antiguos con esta condición.
$_.LastWriteTime -lt $limit
Como usa -lt, un archivo con la misma fecha y hora que el límite queda excluido.
Esta decisión es pequeña, pero importante en la práctica, porque el número de elementos objetivo cambia según se interprete como «más antiguo que 30 días» o «incluyendo los 30 días exactos».
Se añade esta condición límite a la prueba.
It 'no incluye el archivo justo en la fecha límite' {
$border = Join-Path $script:Root 'border.log'
Set-Content -LiteralPath $border -Value 'border log' -Encoding UTF8
(Get-Item -LiteralPath $border).LastWriteTime = [datetime]'2026-05-02T00:00:00'
$result = Get-OldLogFile `
-Path $script:Root `
-Days 30 `
-Now ([datetime]'2026-06-01T00:00:00')
$result.Name | Should -Not -Contain 'border.log'
}
No se trata de escribir muchas pruebas por escribirlas. Sin embargo, los procesos que tienen un límite —como fechas, números, cantidades, permisos o patrones de nombre de archivo— tienen un valor alto para las pruebas.
8. Fijar la forma del valor de retorno
En los scripts de PowerShell, la forma del valor de retorno puede cambiar sin que uno se dé cuenta.
Al principio devolvía directamente un FileInfo.
Más adelante se introdujo Select-Object.
Después se cambiaron los nombres de columna para el CSV.
Este tipo de cambios afecta al proceso posterior, así que probar las propiedades del valor de retorno permite detectar cambios no previstos.
It 'devuelve las propiedades que usa el proceso posterior' {
$result = Get-OldLogFile `
-Path $script:Root `
-Days 30 `
-Now ([datetime]'2026-06-01T00:00:00')
$propertyNames = $result[0].PSObject.Properties.Name
$propertyNames | Should -Contain 'FullName'
$propertyNames | Should -Contain 'Name'
$propertyNames | Should -Contain 'Length'
$propertyNames | Should -Contain 'LastWriteTime'
}
En las funciones que alimentan la salida a CSV o a la generación de informes, no solo el valor, también el nombre de columna forma parte de la especificación.
No basta con confirmar que «funcionó»: hay que verificar que «devuelve la forma que espera el siguiente proceso».
9. Separar el proceso de eliminación de la selección del objetivo
A continuación, veamos el proceso de eliminación. Empecemos por un mal ejemplo.
Get-ChildItem C:\Logs -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
Remove-Item -Force
Es corto y práctico, pero difícil de probar. Como la selección del objetivo y la eliminación están unidas en una sola canalización (pipeline), no queda claro qué parte hay que verificar.
En la práctica, se separan así.
function Remove-OldLogFile {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[string] $Path,
[int] $Days = 30,
[datetime] $Now = (Get-Date)
)
$targets = Get-OldLogFile -Path $Path -Days $Days -Now $Now
foreach ($target in $targets) {
if ($PSCmdlet.ShouldProcess($target.FullName, 'Remove old log file')) {
Remove-Item -LiteralPath $target.FullName -Force
}
}
}
Aquí se añade SupportsShouldProcess para que la propia función pueda recibir -WhatIf.
Remove-OldLogFile -Path C:\Logs -Days 30 -WhatIf
En las funciones de PowerShell relacionadas con la eliminación, siempre que sea posible es más seguro dejarlas en una forma que permita ensayarlas con -WhatIf.
10. Sustituir operaciones peligrosas con Mock
En Pester, Mock permite sustituir la ejecución real de un comando.
No hace falta ejecutar realmente Remove-Item para probar el proceso de eliminación.
Si se llamó cuando debía llamarse. Si no se llamó cuando no debía llamarse.
Con comprobar eso basta.
Un ejemplo de Remove-OldLogFile.Tests.ps1.
BeforeAll {
. $PSScriptRoot\Get-OldLogFile.ps1
. $PSScriptRoot\Remove-OldLogFile.ps1
}
Describe 'Remove-OldLogFile' {
It 'llama a Remove-Item para el archivo de registro antiguo' {
Mock Get-OldLogFile {
[pscustomobject]@{
FullName = 'C:\Logs\old.log'
Name = 'old.log'
Length = 10
LastWriteTime = [datetime]'2026-05-01'
}
}
Mock Remove-Item {}
Remove-OldLogFile `
-Path 'C:\Logs' `
-Days 30 `
-Now ([datetime]'2026-06-01')
Should -Invoke Remove-Item `
-Times 1 `
-Exactly `
-ParameterFilter { $LiteralPath -eq 'C:\Logs\old.log' }
}
It 'no llama a Remove-Item con WhatIf' {
Mock Get-OldLogFile {
[pscustomobject]@{
FullName = 'C:\Logs\old.log'
Name = 'old.log'
Length = 10
LastWriteTime = [datetime]'2026-05-01'
}
}
Mock Remove-Item {}
Remove-OldLogFile `
-Path 'C:\Logs' `
-Days 30 `
-Now ([datetime]'2026-06-01') `
-WhatIf
Should -Invoke Remove-Item -Times 0
}
}
En esta prueba se simulan tanto Get-OldLogFile como Remove-Item, por lo que no hace falta que exista realmente C:\Logs\old.log. Lo que se observa es la decisión de Remove-OldLogFile.
- Llama a
Remove-Itemsi hay un objetivo - No llama a
Remove-Itemcuando se usa-WhatIf - Al llamarlo, pasa la ruta prevista
Cuanto más peligroso es un proceso, más seguro resulta probar las condiciones de la llamada en lugar de la ejecución en sí.
11. No abusar de Mock
Mock es útil, pero abusar de él reduce el valor de la prueba, porque simular todo aleja demasiado la prueba del comportamiento real de PowerShell.
Como referencia, se puede seguir esto.
| Proceso | Recomendación |
|---|---|
| Fecha | Fijarla con un argumento |
| Creación de archivos | Usar $TestDrive |
| Eliminación / movimiento | Verificar con Mock y -WhatIf |
| Llamada a una API web | Aplicar Mock a Invoke-RestMethod, etc. |
| Envío de correo / notificaciones | Aplicar Mock al comando de envío |
| Lectura y escritura de CSV | Crear archivos reales pequeños en $TestDrive |
Si se simula incluso la lectura y escritura de archivos, se puede pasar por alto problemas reales de codificación de caracteres, saltos de línea o nombres de columna.
Por otro lado, procesos como eliminar, notificar, llamar a una API externa o detener un servicio es mejor no ejecutarlos de verdad.
Se separa «dónde usar lo real» de «dónde usar Mock».
12. Adaptar scripts existentes para que sean más fáciles de probar
Al introducir Pester, la forma de escribir los scripts existentes cambia un poco. Sin embargo, no hace falta un cambio de diseño grande desde el principio: para empezar basta con un ajuste de este nivel.
Antes del ajuste
$limit = (Get-Date).AddDays(-30)
Get-ChildItem C:\Logs -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt $limit } |
Remove-Item -Force
Después del ajuste
function Get-OldLogFile {
param(
[string] $Path,
[int] $Days = 30,
[datetime] $Now = (Get-Date)
)
$limit = $Now.AddDays(-1 * $Days)
Get-ChildItem -LiteralPath $Path -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt $limit }
}
function Remove-OldLogFile {
[CmdletBinding(SupportsShouldProcess)]
param(
[string] $Path,
[int] $Days = 30,
[datetime] $Now = (Get-Date)
)
Get-OldLogFile -Path $Path -Days $Days -Now $Now |
ForEach-Object {
if ($PSCmdlet.ShouldProcess($_.FullName, 'Remove old log file')) {
Remove-Item -LiteralPath $_.FullName -Force
}
}
}
Los cambios no son grandes.
- Se convirtió la fecha en un argumento
- Se convirtió la selección del objetivo en una función
- Se separó el proceso de eliminación en otra función
- Se añadió
SupportsShouldProcess
Con esto ya resulta más fácil de probar.
En el mantenimiento de pruebas de PowerShell, en lugar de partir de una discusión de diseño, resulta más eficaz hacer que «la fecha», «la ruta», «los comandos externos» y «las operaciones de cambio» se puedan sustituir desde fuera.
13. Definir la clasificación de las pruebas
En Pester se pueden añadir etiquetas a Describe, Context e It.
Por ejemplo, se pueden separar las pruebas unitarias rápidas de las pruebas de integración que tocan el entorno real.
Describe 'Get-OldLogFile' -Tag 'Unit' {
It 'devuelve solo los archivos .log más antiguos que el número de días indicado' {
# Prueba rápida que usa TestDrive
}
}
Describe 'Log maintenance smoke test' -Tag 'Smoke' {
It 'puede leer la carpeta de registros real' {
Test-Path -LiteralPath 'C:\Logs' | Should -BeTrue
}
}
Se ejecutan solo las pruebas unitarias.
Invoke-Pester -TagFilter Unit
Se excluyen las pruebas lentas o dependientes del entorno.
Invoke-Pester -ExcludeTagFilter Slow, RequiresAdmin, Network
En el trabajo diario, intentar ejecutar todas las pruebas cada vez puede resultar insostenible.
Conviene tomar como estándar las pruebas rápidas y sin efectos secundarios, y separar con etiquetas las pruebas dependientes del entorno para ejecutarlas cuando haga falta.
Tabla de correspondencia por tipo de script
Hasta dónde llega la prueba unitaria de un script y desde dónde empieza la prueba de integración, así como qué usar en cada caso, depende en gran medida del tipo de script.
| Tipo de script | Qué observar en la prueba unitaria | Mecanismo que se usa | Qué observar en la prueba de integración |
|---|---|---|---|
| Recopilación de archivos / selección de objetivo | El límite de la condición: hasta cuántos días atrás se incluye, si se filtra bien por extensión | Crear archivos reales pequeños en $TestDrive |
Si el número de elementos es el esperado en una carpeta real |
| Entrada y salida de CSV / JSON | Nombres de columna, orden de las columnas, codificación de caracteres, saltos de línea | Leer y escribir archivos reales en $TestDrive |
Si el sistema de destino puede abrirlo realmente |
| Eliminación / movimiento / sobrescritura | El resultado de la selección de objetivos a eliminar y el ensayo con -WhatIf |
Mock Remove-Item / Mock Move-Item |
Ejecutarlo una sola vez en el entorno de verificación y comprobar el resultado |
| Operaciones sobre servicios y procesos | La lógica que decide la siguiente operación a partir del estado | Crear el estado con Mock Get-Service, etc. |
Iniciar y detener realmente en una máquina de verificación |
| API externa / notificaciones / envío de correo | El contenido de la solicitud o del cuerpo construido | Mock Invoke-RestMethod / Mock Send-MailMessage |
Enviarlo una sola vez a un entorno de staging |
| Procesos que deciden según fecha o periodo | La evaluación del día límite: el mismo día, el día anterior, un año bisiesto | Fijar la fecha con un argumento (no usar Mock) |
— |
| Lectura del registro de Windows o de configuración del sistema | Cómo se interpreta el valor leído | Mock Get-ItemProperty |
Comprobar el valor real en la máquina |
Hay dos formas de verlo.
- La prueba unitaria observa «nuestro propio juicio». No se comprueba que los comandos estándar funcionen correctamente (capítulo 19).
- Las operaciones peligrosas no se ejecutan en la prueba unitaria. La eliminación, el movimiento, el envío y la notificación se sustituyen con
Mock, y la ejecución en sí se comprueba en la prueba de integración, limitando el número de veces.
Tenga en cuenta que solo la fecha se fija con un argumento, no con Mock. Si se simula Get-Date, se arrastra también la obtención de la fecha para otros usos dentro del mismo script.
14. Ejecutar en CI
Pester ya resulta útil ejecutándolo en local, pero si el equipo gestiona los scripts de forma conjunta, conviene poder ejecutarlo también en CI.
Por ejemplo, se prepara un archivo como tools/Invoke-ProjectTests.ps1.
$ErrorActionPreference = 'Stop'
# Puede quedar un Pester antiguo en el entorno, así que se carga v5 o superior de forma explícita
Import-Module Pester -MinimumVersion 5.0.0
$config = New-PesterConfiguration
$config.Run.Path = @(
Join-Path $PSScriptRoot '..\tests'
)
$config.Run.Exit = $true
$config.Output.Verbosity = 'Detailed'
$config.TestResult.Enabled = $true
$config.TestResult.OutputFormat = 'JUnitXml'
$config.TestResult.OutputPath = Join-Path $PSScriptRoot '..\test-results.xml'
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = @(
Join-Path $PSScriptRoot '..\src'
)
$config.CodeCoverage.OutputPath = Join-Path $PSScriptRoot '..\coverage.xml'
Invoke-Pester -Configuration $config
En el lado de CI, se ejecuta este script.
pwsh -NoProfile -File .\tools\Invoke-ProjectTests.ps1
El punto clave es no escribir en exceso configuración específica de CI dentro de los archivos de prueba.
El archivo de prueba es el lugar donde se escribe la especificación. El formato de salida para CI, la cobertura, el código de salida, etc., resulta más ordenado agruparlos en el script de ejecución.
Con este script de ejecución ya preparado, la configuración del lado de CI queda breve en cualquier servicio.
En el caso de GitHub Actions
Se coloca .github/workflows/pester.yml.
name: pester
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install Pester v5
shell: pwsh
run: |
Install-Module -Name Pester -MinimumVersion 5.0.0 `
-Scope CurrentUser -Force -SkipPublisherCheck
- name: Run Pester
shell: pwsh
run: ./tools/Invoke-ProjectTests.ps1
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: pester-results
path: |
test-results.xml
coverage.xml
Conviene tener en cuenta estos puntos.
- Instalar Pester de forma explícita. Como Windows trae incluida la serie v3 de Pester, si no se reinstala, la forma de escribir de v5 no funciona.
- Añadir
-SkipPublisherCheck. El Pester incluido lleva la firma de Microsoft, así que sin esta opción la instalación se detiene porque el editor es distinto. - Especificar
shell: pwshde forma explícita. En los runners de Windows alojados por GitHub, el valor predeterminado espwsh, pero en los runners autoalojados, si no hay PowerShell 7, recae en Windows PowerShell. Especificarlo evita problemas por diferencias de entorno. - Recoger los archivos de resultado con
if: always(). Precisamente cuando la prueba falla es cuando interesa ver el resultado, así que se ejecuta también en caso de fallo.
Lo que hace fallar el job es $config.Run.Exit = $true. Con esto, cuando la prueba falla, Invoke-Pester termina con un código de salida distinto de 0 y el paso también falla. Al contrario, si se olvida esta línea, el job queda en verde aunque las pruebas estén en rojo.
En el caso de Azure Pipelines
El archivo azure-pipelines.yml queda así.
trigger:
- main
pool:
vmImage: 'windows-latest'
steps:
- task: PowerShell@2
displayName: 'Install Pester v5'
inputs:
pwsh: true
targetType: 'inline'
script: |
Install-Module -Name Pester -MinimumVersion 5.0.0 `
-Scope CurrentUser -Force -SkipPublisherCheck
- task: PowerShell@2
displayName: 'Run Pester'
inputs:
pwsh: true
filePath: 'tools/Invoke-ProjectTests.ps1'
- task: PublishTestResults@2
displayName: 'Publish test results'
condition: always()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'test-results.xml'
failTaskOnFailedTests: true
Los formatos que acepta PublishTestResults@2 son JUnit, NUnit, VSTest, XUnit y CTest. Como en el script de ejecución se fija $config.TestResult.OutputFormat = 'JUnitXml', aquí se elige JUnit. Si cambia el formato de salida, cambie ambos de forma coherente.
Si se añade failTaskOnFailedTests: true, la tarea falla en el momento en que el archivo de resultados contiene algún fallo. El valor predeterminado es false, en cuyo caso solo se muestra el resultado y la tarea pasa igualmente.
15. Ver la cobertura como un mapa, no como un objetivo
Pester también puede generar cobertura de código. Sin embargo, es mejor no perseguir la cifra desde el principio, porque la cobertura no es en sí misma la calidad de la prueba.
Por ejemplo, si se llama una sola vez a la función que extrae los objetos a eliminar, esa línea queda marcada como recorrida. Pero si no se han comprobado las condiciones límite ni las de exclusión, eso no aporta tranquilidad real en el trabajo diario.
La cobertura se usa así.
- Para encontrar funciones que no se ejecutan en absoluto
- Para encontrar ramas importantes que carecen de prueba
- Para priorizar los scripts que cambian con más frecuencia
- Para dejar constancia en CI de que las pruebas se ejecutaron
Más que subir la cifra, se observa si «se prueban las decisiones importantes».
La obtención se configura en el lado CodeCoverage de New-PesterConfiguration.
$config = New-PesterConfiguration
$config.Run.Path = @('.\tests')
$config.CodeCoverage.Enabled = $true
# Objeto medido. Se indica el script a probar, no el archivo de prueba
$config.CodeCoverage.Path = @('.\src')
# Formato de salida. El predeterminado es JaCoCo, fácil de incorporar en la vista de cobertura de CI
$config.CodeCoverage.OutputFormat = 'JaCoCo'
$config.CodeCoverage.OutputPath = '.\coverage.xml'
# Valor objetivo. No hace fallar la prueba si no se alcanza; se deja solo como referencia
$config.CodeCoverage.CoveragePercentTarget = 75
Invoke-Pester -Configuration $config
El CodeCoverage.Path que se indica es el script que se está probando, no el archivo de prueba. Si aquí se pone .\tests, se acaba midiendo la cobertura del propio código de prueba, y la cifra sale alta sin que tenga contenido real.
El formato de salida predeterminado es JaCoCo. Si se elige CoverageGutters, se obtiene un formato que permite ver en el editor el estado de ejecución línea por línea. Para incorporarlo a CI, JaCoCo sin cambios es suficiente.
CoveragePercentTarget es una cifra orientativa: no hace que la ejecución falle aunque no se alcance. Precisamente cuando surja la tentación de convertir la cobertura en un criterio de aprobado o suspenso, conviene volver al principio de este capítulo. Las pruebas escritas solo para cumplir una cifra normalmente no dicen nada útil cuando algo se rompe.
16. Orden para introducirlo en scripts existentes
Al incorporar Pester a scripts de PowerShell ya existentes, avanza mejor si no se convierte de golpe todo el conjunto en objeto de prueba. A continuación, un orden recomendado.
1. Elegir los scripts cuyo fallo resultaría un problema
Al principio, conviene empezar por scripts como estos.
- Que incluyan eliminación, movimiento o sobrescritura
- Que se ejecuten a diario o mensualmente
- Que consten en un procedimiento, pero dependan de una sola persona
- Que hayan tenido errores de condición en el pasado
- Cuyo CSV de salida se use en otras tareas
Se empieza por lo que es útil pero cuya rotura resultaría un problema.
2. Extraer en una función solo la parte de lectura
Lo primero que se prueba no es el proceso de cambio, sino el de lectura.
Leer el registro
Filtrar el objetivo
Contar los elementos
Dar formato para el CSV
Esta parte es fácil de probar con $TestDrive y tiene poco riesgo de accidente.
3. Pasar la fecha y la ruta desde fuera
Si la fecha o la ruta están fijadas en el código, resulta difícil probarlo.
# Se quiere evitar
$root = 'C:\Logs'
$limit = (Get-Date).AddDays(-30)
Una forma fácil de probar.
param(
[string] $Path,
[datetime] $Now = (Get-Date)
)
Con solo poder pasar los valores desde fuera, la estabilidad de la prueba mejora mucho.
4. Dejar las operaciones peligrosas para el final
La eliminación y el movimiento se agrupan al final.
Crear el objetivo
↓
Dejar constancia del objetivo en el registro
↓
Confirmar con -WhatIf
↓
Ejecutar
En la prueba también se confirma siguiendo el mismo orden.
17. Problemas habituales
| Síntoma | Causa | Solución |
|---|---|---|
| Pasa en local, pero falla en CI | El directorio de trabajo actual es distinto | Tomar $PSScriptRoot como referencia |
| El resultado cambia según el día | Se usa Get-Date directamente |
Preparar un argumento como -Now |
| La prueba está a punto de borrar un archivo real | Se está usando una carpeta real | Usar $TestDrive y Mock |
| Mock no surte efecto | Los límites de módulo o el ámbito (scope) son distintos | Revisar -ModuleName o la forma de carga |
| No se sabe hasta dónde probar | La especificación no está separada en funciones | Separar en selección de objetivo, formateo y proceso de cambio |
| La prueba es lenta | Toca un servicio externo o la red | En la prueba unitaria, aplicar Mock a las dependencias externas |
| El nombre de la prueba no aclara nada | Tiene un nombre como It 'works' |
Incluir en el nombre la condición y el resultado esperado |
Aunque parezca un problema de Pester, con frecuencia la causa real está en la estructura del script.
Las partes difíciles de probar suelen ser también las que más se rompen en el uso diario.
Problemas con Install-Module (frecuentes en entornos corporativos)
El Install-Module del capítulo 3 funciona sin más en un entorno con salida directa a internet. En cambio, en los equipos corporativos gestionados por el departamento de sistemas, es habitual que se detenga aquí.
| Síntoma | Causa | Solución |
|---|---|---|
| No se puede conectar con PowerShell Gallery | No pasa por el proxy corporativo | Indicar -Proxy y -ProxyCredential |
| Falla con un mensaje como «se cerró la conexión subyacente» | El protocolo predeterminado de Windows PowerShell 5.1 no incluye TLS 1.2 | Habilitar TLS 1.2 en la sesión antes de ejecutar |
| Se detiene pidiendo instalar el proveedor de NuGet | Se mantiene la versión 1.0.0.1 de PowerShellGet incluida con Windows PowerShell | Instalar antes el proveedor de NuGet |
| No hay salida al exterior en absoluto | Red aislada (sin acceso a internet) | Usar Save-Module en otro equipo y llevarlo, o registrar un repositorio interno con Register-PSRepository |
Los tres casos anteriores suelen resolverse ejecutando en este orden.
# 1. Habilitar TLS 1.2 (necesario en Windows PowerShell 5.1; no hace falta en PowerShell 7)
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor
[Net.SecurityProtocolType]::Tls12
# 2. Instalar el proveedor de NuGet (se instala antes para que no se detenga en un aviso interactivo)
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force
# 3. Instalar Pester a través del proxy
$proxyUri = 'http://proxy.example.local:8080'
$proxyCredential = Get-Credential -Message 'Autenticación del proxy'
Install-Module -Name Pester -MinimumVersion 5.0.0 `
-Scope CurrentUser -Force -SkipPublisherCheck `
-Proxy $proxyUri -ProxyCredential $proxyCredential
La configuración de TLS 1.2 solo tiene efecto en esa sesión. Si resulta molesto tener que escribirla cada vez, se puede dejar en un script de perfil.
Si el proxy no requiere autenticación, se puede omitir -ProxyCredential. Si no conoce la URL del proxy, consulte con el departamento de sistemas la «configuración del proxy para salir a PowerShell Gallery (www.powershellgallery.com)».
Si la red está aislada y no hay salida al exterior, ejecute lo siguiente en un equipo con acceso a internet y lleve consigo la carpeta resultante.
# En el equipo con salida a internet
Save-Module -Name Pester -MinimumVersion 5.0.0 -Path 'D:\modules'
En el equipo de destino, coloque D:\modules\Pester en una ruta de búsqueda de módulos, como $env:USERPROFILE\Documents\WindowsPowerShell\Modules. Puede comprobar las rutas de destino con $env:PSModulePath.
Documentar este procedimiento desde el principio acelera la incorporación cuando se suman nuevos responsables. Que el motivo por el que no avanza el mantenimiento de pruebas sea «no se puede instalar Pester» es, en la práctica, algo bastante habitual.
18. Reglas que conviene fijar en el mantenimiento de pruebas
Si el equipo va a gestionar scripts de PowerShell de forma conjunta, conviene fijar reglas antes de entrar en detalles de estilo de escritura.
Por ejemplo, reglas como estas.
- Nombrar los archivos de prueba con
*.Tests.ps1 - Cargar el objeto de prueba desde
$PSScriptRoot - Usar
$TestDriveen las pruebas de operaciones de archivo - Aplicar
Mockpor norma a la eliminación, el movimiento, las notificaciones y las llamadas a API - Convertir la fecha en un argumento para poder fijarla
- Añadir etiquetas como
Unit,SmokeoRequiresAdminaDescribeo aIt - Ejecutar
Unitcomo estándar en CI - Añadir
SupportsShouldProcesssiempre que sea posible a las funciones de cambio - Conservar los errores pasados como pruebas de prevención de reaparición
Si hay demasiadas reglas, no se cumplen. Al principio basta con estas tres.
Usar TestDrive
Fijar la fecha
Aplicar Mock a las operaciones peligrosas
Con cumplir solo estas tres, las pruebas de PowerShell se vuelven bastante estables.
19. Decidir también qué no probar
En el mantenimiento de pruebas, «qué no probar» es tan importante como «qué probar».
Por ejemplo, es mejor no forzar demasiado la comprobación de estos puntos en la prueba unitaria.
- Que
Get-ChildItemde Windows funcione correctamente - Que
Remove-Itemelimine realmente el archivo - La especificación interna de los comandos estándar de PowerShell
- Que una API externa responda siempre
- Que un recurso compartido de red esté siempre disponible
Lo que se debe probar es nuestro propio criterio.
- Bajo qué condición se convierte en objetivo
- Qué ruta se pasa
- Qué columna se saca en la salida
- Cómo se trata cuando falla
- Si se puede ensayar la operación peligrosa
Se separa lo que se confía a los comandos estándar de lo que se protege como lógica propia.
20. Resumen
PowerShell es una herramienta cómoda que permite automatizar rápidamente tareas cotidianas. Sin embargo, los scripts que se usan durante mucho tiempo en el trabajo diario van ganando responsabilidad poco a poco. Lo que empezó como un comando de una sola línea para uso propio acaba convirtiéndose en un proceso operativo que se ejecuta a diario y que afecta al trabajo de otras personas y a los datos del negocio.
El mantenimiento de pruebas con Pester es el trabajo necesario para proteger el script a medida que se produce ese cambio.
A modo de resumen:
- Probar primero la selección del objetivo
- Permitir pasar la fecha y la ruta desde fuera
- Confinar las operaciones de archivo dentro de
$TestDrive - Aplicar
Mocka la eliminación, el movimiento, las notificaciones y las llamadas a API - Hacer que las funciones de cambio se puedan ensayar con
-WhatIf - Hacer que el nombre de la prueba se pueda leer como especificación
- En CI, ejecutar primero las pruebas rápidas y sin efectos secundarios
Una operación segura de PowerShell no consiste en introducir de golpe un mecanismo grande.
Dividir en funciones pequeñas. Escribir pruebas pequeñas. Dejar la posibilidad de confirmar antes de un proceso peligroso.
Con esta acumulación, PowerShell se acerca, de ser «un script cómodo pero un poco temido», a convertirse en «una herramienta de negocio que se puede verificar incluso al cambiarla».
Referencias
- Conjunto completo del código de ejemplo de este artículo (el script a probar, las pruebas de Pester y el script de ejecución para CI) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/pester-powershell-test-maintenance
- Pester Quick Start
- Pester Installation and Update
- Pester File placement and naming
- Pester TestDrive
- Pester Mocking
- Pester Tags
- Pester Configuration
- Pester Test Results
- Pester Code Coverage
- PowerShell Gallery: Pester
- PowerShell Documentation - Microsoft Learn
- Install a package manager for PowerShell (TLS 1.2 y el proveedor de NuGet) - Microsoft Learn
- Install-Module (-Proxy / -ProxyCredential) - Microsoft Learn
- PublishTestResults@2 - Referencia de tareas de Azure Pipelines
- Workflow syntax for GitHub Actions (jobs.<job_id>.steps[*].shell)
Artículos relacionados
- Fundamentos de los comandos de PowerShell — las operaciones que hay que aprender primero y su uso seguro
- PowerShell aplicado — automatizar de forma segura la investigación de registros, el archivado y la generación de informes
- Colección práctica de comandos de PowerShell — ampliar las pequeñas funciones que se usan a diario
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Cómo invocar COM y .NET desde PowerShell en la práctica ── ampliar de un salto el alcance de sus scripts
Cómo invocar clases .NET desde PowerShell, integrar C# y la API Win32 con Add-Type, operar COM, gestionar los procesos residuales de Exce...
Diseño de parámetros y modularización de scripts de PowerShell — de un «script que funciona» a un «script que se puede entregar»
Explicamos cómo llevar un script de PowerShell a una calidad entregable: param, [CmdletBinding()], validación, pipeline, -WhatIf, módulos...
Diferencias entre Windows PowerShell 5.1 y PowerShell 7 ── Guía práctica de migración de scripts internos
Explicamos la relación entre Windows PowerShell 5.1 y PowerShell 7 (coexistencia y pwsh.exe), la política oficial de no añadir funciones ...
Automatizar el procesamiento de Excel y CSV con PowerShell — recetas prácticas de agregación, cotejo y generación de informes
Recetas prácticas para automatizar con PowerShell la agregación y el cotejo de CSV y la generación de informes en Excel: codificación, Gr...
Cómo ejecutar PowerShell desde C# (CSharp) y recibir los resultados como objetos
Explica, con enfoque práctico, cómo iniciar PowerShell desde C# y recibir los resultados como PSObject en vez de cadenas: PowerShell SDK,...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Qué debería probar primero con Pester?
- Empiece no por el propio proceso de eliminación, sino por el proceso que selecciona qué eliminar (la selección del objetivo). En los scripts operativos, suele dar buen resultado priorizar estos cuatro aspectos: la evaluación de condiciones, la forma de la salida, el paso previo a las operaciones peligrosas y las dependencias externas. No es necesario rediseñar de golpe un script existente: basta con extraer como función la parte de decisión que precede a la operación peligrosa y comprobar su valor de retorno con Pester.
- ¿Qué es TestDrive de Pester?
- $TestDrive es un área temporal para pruebas que proporciona Pester. Permite validar operaciones de archivo usando archivos creados únicamente dentro de la prueba, sin tocar la carpeta compartida ni la ruta real como C:\Logs. En las pruebas de scripts de PowerShell que implican operaciones de archivo, acostumbrarse a usar primero $TestDrive evita accidentes como borrar por error un archivo real.
- ¿Hasta dónde conviene usar Mock en Pester?
- Conviene sustituir con Mock las operaciones que no deben ejecutarse realmente, como eliminar, mover, llamar a una API web o enviar correos y notificaciones; fijar la fecha mediante un argumento; y, para la creación de archivos o la lectura y escritura de CSV, es preferible crear archivos reales pequeños en $TestDrive. Si se simula todo, la prueba se aleja demasiado del comportamiento real de PowerShell y puede pasar por alto problemas de codificación de caracteres, saltos de línea o nombres de columna. La clave está en separar «dónde usar lo real» de «dónde usar Mock».
- ¿Por qué una prueba de Pester que pasa en local falla en CI?
- Una causa habitual es la diferencia en el directorio de trabajo actual, que se resuelve cargando el objeto de prueba tomando $PSScriptRoot como referencia. Si el resultado cambia según el día, suele deberse a que se usa Get-Date directamente, así que conviene fijar la fecha con un argumento como -Now. Si Mock no surte efecto, sospeche de diferencias en los límites de módulo o en el ámbito (scope), y revise -ModuleName o la forma de carga. Aunque parezca un problema de Pester, la causa suele estar en la estructura del script.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.