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: · · 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.ps1 al 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-Item si hay un objetivo
  • No llama a Remove-Item cuando 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.

  1. La prueba unitaria observa «nuestro propio juicio». No se comprueba que los comandos estándar funcionen correctamente (capítulo 19).
  2. 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: pwsh de forma explícita. En los runners de Windows alojados por GitHub, el valor predeterminado es pwsh, 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 $TestDrive en las pruebas de operaciones de archivo
  • Aplicar Mock por 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, Smoke o RequiresAdmin a Describe o a It
  • Ejecutar Unit como estándar en CI
  • Añadir SupportsShouldProcess siempre 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-ChildItem de Windows funcione correctamente
  • Que Remove-Item elimine 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 Mock a 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

Artículos relacionados

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

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.

Volver al blog