Módulos PowerShell reutilizables con Pester: guía completa

Última actualización: 17/12/2025
Autor: Isaac
  • Convertir funciones en módulos PowerShell con manifiesto permite reutilizarlas, versionarlas y distribuirlas fácilmente.
  • Pester proporciona pruebas unitarias e integración para validar módulos y scripts, evitando regresiones en cambios futuros.
  • Integrar PowerShell, Pester y PowerShellGet en pipelines CI/CD automatiza pruebas, linting y publicación en PowerShell Gallery.

módulos PowerShell reutilizables con Pester

Si trabajas con PowerShell a diario, tarde o temprano te cansarás de copiar y pegar siempre las mismas funciones. En ese punto es cuando de verdad tiene sentido hablar de módulos PowerShell reutilizables y de cómo probarlos en serio con Pester, igual que harías con cualquier proyecto de desarrollo “serio”.

Aunque PowerShell nació como herramienta para adminsys, hoy es un lenguaje de automatización completo, multiplataforma y con un ecosistema brutal: módulos, scripting avanzado, pruebas automatizadas, CI/CD… y si sabes empaquetar tus funciones en módulos bien construidos y cubiertos con Pester, puedes versionar, compartir, publicar en PowerShell Gallery y desplegar con confianza sin sudar cada vez que cambias una línea.

Qué es realmente un módulo de PowerShell y en qué se diferencia de un script

conceptos básicos de módulos de PowerShell

Un módulo de PowerShell es, hablando en plata, un paquete con comandos reutilizables: funciones, cmdlets, alias, variables, clases o recursos DSC. Lo que hace especial a un módulo es que PowerShell puede cargarlo y descargarlo, controlar qué se expone al usuario, asignarle versión, autor, dependencias, etc.

Desde el punto de vista de archivos, un módulo típico se compone al menos de un fichero .psm1 (código) y, casi siempre, un manifiesto .psd1 con metadatos. Además puede incluir otros scripts, recursos, archivos de ayuda, datos, etc. Al importarlo con Import-Module o gracias a la carga automática, sus comandos pasan a estar disponibles como si fueran nativos.

En cambio, un script de PowerShell es solo un archivo .ps1 que se ejecuta “de una tacada”. Puede contener funciones, pero esas funciones viven normalmente en el ámbito del script. Si no las dot-sources o no las metes en un módulo, desaparecen en cuanto termina la ejecución.

La diferencia práctica es grande: los scripts suelen ser “tareas que se lanzan”, mientras que los módulos son “librerías reutilizables” que puedes consumir desde otros scripts, desde la consola o incluso desde pipelines CI/CD sin duplicar código.

Scripts vs módulos: cuándo usar cada uno

diferencias entre scripts y módulos de PowerShell

Un script (.ps1) es ideal cuando tienes que resolver tareas concretas: un backup, un reporte, una limpieza programada… Lo ejecutas, hace su trabajo y termina. Si dentro de ese script defines funciones, esas funciones viven en el ámbito de script; al acabar, el ámbito se descarta y con él tus funciones.

Un módulo de script (.psm1), en cambio, está pensado para encapsular funciones que vas a reutilizar muchas veces, en distintas máquinas, incluso en distintos proyectos. Al guardarlas en un módulo, se cargan en un ámbito de módulo y puedes exportar solo lo que te interese como API pública.

Muchos admins empiezan con un script gigante que hace de todo y, con el tiempo, ven que comparten constantemente partes de ese código. El paso natural es extraer esas funciones repetidas a un módulo, dejarlas testeadas con Pester y que ese módulo sea la base que consumen otros scripts más pequeños y limpios.

Además, los módulos permiten algo clave en equipos: versionado, publicación y distribución. Puedes instalar módulos desde PowerShell Gallery, desde repositorios privados, desde repos locales o incluso incrustarlos en imágenes de sistema para que estén disponibles por defecto.

Dot-sourcing: el “truco rápido” antes de pasar a módulos

Antes de tener tus funciones en un módulo, quizá las tengas en un simple script .ps1 que quieres reutilizar. En ese caso, la única forma decente de tenerlas disponibles en tu sesión es usar dot-sourcing.

Cuando ejecutas un script con . MiScript.ps1, el contenido se procesa en el ámbito de script. Si en ese fichero hay funciones, al terminar el script desaparecen. Si en cambio lo invocas con un punto y espacio delante (dot-sourcing), PowerShell lo ejecuta en el ámbito global y las funciones quedan cargadas:

Ejemplos de dot-sourcing para cargar funciones en memoria:

  • Cargar con ruta relativa: . .\Get-MrPSVersion.ps1
  • Cargar con ruta completa: . C:\Demo\Get-MrPSVersion.ps1
  • Cargar usando una variable de ruta: $Path = 'C:\'; . $Path\Get-MrPSVersion.ps1

Tras hacer dot-sourcing, puedes comprobar que la función existe en la unidad Function: con un Get-ChildItem -Path Function:\Get-MrPSVersion, ya que ahora forma parte del ámbito global.

El problema es que este enfoque escala mal: hay que acordarse de hacer dot-sourcing, no tienes versión, no tienes manifiesto, no hay distinción entre público y privado. Es un atajo útil, pero para algo serio y compartible es mucho mejor dar el salto a módulos de script.

Creación de módulos de script y carga automática

creación de módulos de script y carga automática

En PowerShell, un módulo de script no tiene misterio: es un archivo .psm1 que contiene tus funciones. Si, por ejemplo, creas un fichero MyScriptModule.psm1 con algo así como:

function Get-MrPSVersion { $PSVersionTable }
function Get-MrComputerName { $env:COMPUTERNAME }

y luego intentas usar Get-MrComputerName, verás que no se encuentra hasta que importes el módulo. De forma manual puedes hacerlo con:

  macOS va lento: causas y soluciones definitivas para acelerarlo

Import-Module C:\MyScriptModule.psm1

Desde PowerShell 3 llegó la magia de la autocarga de módulos. Para que funcione, tienes que:

  • Guardar tu módulo en una carpeta cuyo nombre base coincida con el módulo, por ejemplo MyScriptModule\MyScriptModule.psm1.
  • Colocar esa carpeta en alguna ruta del $env:PSModulePath.

El valor de $env:PSModulePath es una lista de rutas separadas por ;. Puedes verla de forma legible con:

$env:PSModulePath -split ';'

Las rutas típicas incluyen la carpeta de módulos del usuario, la carpeta global de módulos en Program Files y la de sistema en System32. Lo habitual es instalar tus propios módulos en la ruta “AllUsers” (Program Files) y dejar la de System32 únicamente para módulos del sistema.

Una vez el módulo está en su sitio, cuando invoques por primera vez uno de sus comandos, PowerShell lo cargará automáticamente sin necesidad de llamar a Import-Module a mano, lo que es muy cómodo en scripts y sesiones interactivas.

Manifiestos de módulo: metadatos, versión y exportación de funciones

Para que un módulo esté bien definido, no basta con el .psm1: necesita un manifiesto .psd1, que no es más que un archivo de datos de PowerShell con metadatos del módulo (nombre, versión, autor, descripción, dependencias, funciones exportadas, etc.).

La forma estándar de crearlo es usando New-ModuleManifest, pasando como mínimo el Path del .psd1 y el parámetro RootModule apuntando al .psm1. Además, si piensas publicar el módulo en algún repositorio tipo PowerShell Gallery, campos como Author, Description o CompanyName son obligatorios.

Una pista para saber que a tu módulo le falta manifiesto es revisar su versión con Get-Module -Name MyScriptModule y ver un número de versión 0.0, síntoma claro de que solo hay un .psm1 suelto sin .psd1 asociado.

Patrón típico para crear el manifiesto sería algo como:

$moduleManifestParams = @{
Path = "$env:ProgramFiles\WindowsPowerShell\Modules\MyScriptModule\MyScriptModule.psd1"
RootModule = 'MyScriptModule'
Author = 'Autor del módulo'
Description = 'Funciones reutilizables para administración'
CompanyName = 'MiEmpresa'
}
New-ModuleManifest @moduleManifestParams

Más adelante, si necesitas cambiar algún dato, en lugar de recrear el manifiesto (lo que generaría un nuevo GUID y puede romper dependencias), lo correcto es usar Update-ModuleManifest para modificar solo lo necesario.

El manifiesto incluye también secciones clave como FunctionsToExport, donde puedes listar exactamente qué funciones del módulo se exponen al usuario, dejando el resto como funciones privadas internas al módulo.

Funciones públicas y privadas en tus módulos

En un módulo real no quieres exponerlo todo. Es normal tener funciones auxiliares internas que solo se usan entre sí, y que no quieres que los usuarios llamen directamente. Hay dos formas principales de controlar qué sale “de puertas para afuera”:

Si no tienes manifiesto (módulo simple solo con .psm1), puedes controlar la visibilidad con Export-ModuleMember dentro del propio .psm1. Por ejemplo:

function Get-MrPSVersion { $PSVersionTable }
function Get-MrComputerName { $env:COMPUTERNAME }
Export-ModuleMember -Function Get-MrPSVersion

En ese caso, solo Get-MrPSVersion estará visible como comando del módulo, mientras que Get-MrComputerName seguirá siendo accesible desde otras funciones del módulo pero no se exportará públicamente. Puedes comprobar qué comandos se exponen con:

Get-Command -Module MyScriptModule

Si tienes manifiesto, lo más limpio es definir esa lista en la clave FunctionsToExport del .psd1, por ejemplo:

FunctionsToExport = 'Get-MrPSVersion'

No es necesario duplicar lógica usando a la vez Export-ModuleMember en el .psm1 y FunctionsToExport en el .psd1; con uno de los dos mecanismos tienes suficiente. El enfoque basado en manifiesto suele ser más claro a medio plazo.

PowerShell Gallery, requisitos y publicación de módulos y scripts

La PowerShell Gallery es el repositorio público principal de módulos y scripts. Desde ahí instalas dependencias con Install-Module, actualizas con Update-Module y publicas tus propios paquetes con Publish-Module o Publish-Script.

Para publicar un módulo hay una serie de requisitos mínimos que conviene tener claros: nombre único, versión semántica, manifiesto correcto, metadatos básicos rellenos y, si usas dependencias, que estén también disponibles. Similarly, los scripts que quieras publicar como tales necesitan un encabezado con metadatos en formato de archivo de script (autor, descripción, versión, etc.).

La conexión entre PowerShellGet, PackageManagement y NuGet funciona así a grandes rasgos: PowerShellGet es el módulo que tú usas (Install-Module, Publish-Module…), PackageManagement es la capa genérica de gestión de paquetes, y debajo de todo, para la Gallery, se usa NuGet como proveedor. Eso significa que, aunque el backend sea NuGet, no es lo mismo usar nuget.exe que usar PowerShellGet; para la mayoría de tareas de módulos PowerShell usarás siempre los cmdlets de PowerShellGet.

Para publicar necesitas un API key de NuGet (NUGET_KEY) que se usa como credencial. En entornos CI/CD se guarda como secreto y se expone en tiempo de build para ejecutar un Publish-Module -NuGetApiKey. También puedes reclamar la propiedad de un paquete, reservar nombres o denunciar infracciones de licencia directamente en la Gallery, siguiendo sus flujos de soporte.

  Qué hacer si Windows asigna una IP APIPA: guía completa y soluciones

Qué es Pester y por qué deberías usarlo con tus módulos

Pester es el framework de pruebas unitarias y de integración estándar para PowerShell. Es un módulo escrito en PowerShell que define un lenguaje de pruebas muy legible basado en bloques como Describe, Context, It, BeforeAll, AfterAll y en aserciones tipo Should -Be, Should -Not -Be, etc.

El objetivo de Pester es que cada vez que cambies tu código, puedas lanzar un conjunto de pruebas automáticas que te aseguren que sigues cumpliendo el comportamiento esperado. Esto evita muchos clásicos: arreglar algo en producción, romper otra cosa de rebote, o que un cambio de autenticación en un proyecto grande destroce media plataforma sin que nadie lo vea venir.

En la práctica, Pester te permite cubrir tanto pruebas unitarias (funciones aisladas, con mocks donde haga falta) como pruebas de integración (tu módulo hablando con SharePoint, Azure, SQL, etc.). Para muchas organizaciones, tener pruebas con Pester es un mínimo de calidad, incluso para “simples” scripts administrativos.

Pester ofrece, además de aserciones, agrupación lógica de pruebas, mocking de comandos externos y medición de code coverage, lo que te da mucha visibilidad de qué partes del módulo se están cubriendo y cuáles no.

Instalación y uso básico de Pester con módulos

Las versiones modernas de Windows suelen traer una versión antigua de Pester (v3) preinstalada. Lo habitual hoy es instalar la versión actualizada desde la Gallery con:

Install-Module -Name Pester -Force

Pester funciona en Windows, Linux y macOS y es compatible con PowerShell 3, 4, 5, 6 y 7, así que no vas a tener problemas en entornos mixtos. Una vez instalado, lo único que tienes que hacer es crear archivos de pruebas con el sufijo .Tests.ps1 o .Test.ps1 que describan el comportamiento de tus módulos.

La estructura típica de un archivo de pruebas incluye bloques como:

  • BeforeAll: se ejecuta una vez antes de todos los tests (ideal para importar el módulo o definir datos comunes).
  • Describe: agrupa pruebas para una función o un área de funcionalidad.
  • Context: subdivide escenarios (“cuando los parámetros son correctos”, “cuando hay error de credenciales”…).
  • It: cada bloque It es una prueba individual con su conjunto de aserciones usando Should.
  • AfterAll: limpia al final de todas las pruebas (cerrar conexiones, borrar temporales, etc.).

Para lanzar las pruebas basta con llamar a Invoke-Pester apuntando al archivo o carpeta de tests, y obtendrás un resumen del número de aserciones correctas y fallidas, junto con el tiempo de ejecución y, si quieres, salida detallada con -Output Detailed.

Ejemplo práctico: probando una función de login con Pester

Imagina una función de PowerShell llamada LoginSP que encapsula el inicio de sesión en SharePoint Online utilizando PnP o CSOM, y que devuelve un contexto de SharePoint. Esta función vive en tu módulo o en un .ps1 que utilizas en muchos scripts de automatización.

Si de ella dependen decenas de funciones de negocio, no puedes permitirte que un cambio de implementación (por ejemplo, pasar de PnP a CSOM) rompa todo. Lo razonable es escribir pruebas unitarias con Pester en un archivo LoginSharePoint.Test.ps1 o similar.

En ese archivo de pruebas, en el bloque BeforeAll sueles dot-sourcer o importar el módulo con la función LoginSP. Luego creas uno o varios bloques Describe «Testing LoginSP» y dentro contextos para diferentes escenarios: parámetros correctos, parámetros erróneos, credenciales incorrectas, etc.

Cada bloque It alberga una prueba concreta, por ejemplo: “no debe devolver null” o “la URL del contexto devuelto coincide con la URL de entrada”. En una prueba podrías llamar a la función directamente dentro de la tubería del Should, en otra almacenar el resultado en una variable para inspeccionar propiedades y escribir información adicional con Write-Host si hace falta.

La ejecución se realiza con Invoke-Pester ".\LoginSharePoint.Test.ps1" y, si quieres más detalle, añades -Output Detailed. Además de la salida inmediata, Pester puede integrarse con code coverage para ver qué partes del módulo se han ejecutado durante las pruebas.

Parámetros de entrada en pruebas Pester y uso de contenedores

Meter credenciales o URLs directamente en cada prueba es un infierno de mantenimiento y, además, poco automatizable. Lo elegante con Pester es declarar parámetros en el propio archivo de test, exactamente igual que harías en un script de PowerShell:

Param (
[Parameter(Mandatory=$true)] [string]$UserAccount,
[Parameter(Mandatory=$true)] [string]$UserPW,
[Parameter(Mandatory=$true)] [string]$SiteCollUrl
)

Con esto, dentro del test puedes usar $UserAccount, $UserPW y $SiteCollUrl en todas las pruebas, sin repetirlos. Para pasar esos valores cuando ejecutas las pruebas, Pester proporciona los PesterContainer:

$myContainer = New-PesterContainer -Path 'LoginSharePoint.Test.ps1' -Data @{
UserAccount = 'usuario@tenant.onmicrosoft.com';
UserPw = 'MiMuySeguraPW';
SiteCollUrl = 'https://tenant.sharepoint.com/sites/TeamSite'
}
Invoke-Pester -Container $myContainer -Output Detailed

De esta forma puedes inyectar distintos datos de prueba sin tocar el código del test, lo que es perfecto para pipelines de CI donde las credenciales, URLs o tenants cambian según el entorno (desarrollo, pre, producción) y se suministran como secretos o variables protegidas.

  Comparación de lectores electrónicos Kindle | ¿Qué Kindle comprar?

Pruebas unitarias y de integración: qué cubre cada una

En el mundo de Pester es importante distinguir entre pruebas unitarias y pruebas de integración. Las unitarias se centran en bloques pequeños de código en aislamiento, normalmente con mocks de dependencias externas. Por ejemplo, una función “New-Cajón” debería devolver un objeto cajón con cuatro esquinas, dos raíles, un tirador, el color correcto y las medidas correctas.

Las pruebas de integración, por su parte, verifican que todas esas piezas unitarias funcionan correctamente cuando se conectan con sistemas reales: SharePoint, bases de datos, servicios externos, etc. Puedes tener todas las unitarias en verde y que la integración sea un desastre si no la pruebas explícitamente (por ejemplo, si el entorno donde se monta el “cajón” no encaja).

En PowerShell, lo habitual es que tus módulos encapsulen toda esa lógica de negocio y que Pester te ayude tanto a asegurar que cada función hace lo que debe (unitarias) como que la combinación de funciones, módulos y servicios externos funciona en conjunto (integración).

La gran ventaja es que, una vez tienes la batería de pruebas montada, cada cambio de código se convierte en una simple cuestión de lanzar Pester y revisar resultados, lo que te protege frente a regresiones en proyectos grandes y vivos.

PowerShell, módulos y Pester en pipelines de CI/CD

Hoy es muy común integrar PowerShell y Pester en pipelines de integración y entrega continua, por ejemplo con GitHub Actions. Los runners hospedados de GitHub ya traen PowerShell 7 y Pester preinstalados, además de la opción de instalar módulos adicionales desde la Gallery con Install-Module.

Un flujo típico arranca con un job de pruebas que, en cada push, ejecuta Pester sobre tu módulo. Un ejemplo simplificado en YAML podría incluir pasos como:

  • Checkout del repositorio (actions/checkout@v5).
  • Ejecución de un test sencillo con Test-Path resultsfile.log | Should -Be $true para verificar que se genera un resultado.
  • Ejecución de un archivo de pruebas completo con Invoke-Pester Unit.Tests.ps1 -Passthru.

Además de ejecutar Pester, es habitual instalar dependencias desde la Gallery (por ejemplo, SqlServer o PSScriptAnalyzer), configurar el repositorio PSGallery como de confianza y almacenar en caché los módulos para acelerar descargas usando actions/cache. En Linux, por ejemplo, la caché típica de módulos se sitúa en ~/.local/share/powershell/Modules, mientras que en Windows cambia la ruta.

Otro patrón muy útil es usar PSScriptAnalyzer en el pipeline para hacer linting de todos los .ps1, obteniendo un listado de issues por severidad y fallando el job si hay errores graves. Esto te obliga a mantener un estilo de código consistente y sin errores evidentes antes incluso de pasar a pruebas de funcionalidad con Pester.

Publicar módulos en PowerShell Gallery desde CI

Una vez que tus pruebas Pester pasan en CI, lo lógico es automatizar la publicación de tu módulo en la Gallery cuando creas un nuevo release. En GitHub Actions, esto se hace típicamente con un workflow que se dispara en eventos de tipo release: created.

El job correspondiente suele:

  • Hacer checkout del código.
  • Ejecutar un script de build (por ejemplo ./build.ps1 -Path /tmp/samplemodule) que genere la estructura final del módulo, con .psm1, .psd1 y cualquier recurso adicional.
  • Leer el secreto NUGET_KEY de los secretos del repositorio y exponerlo como variable de entorno.
  • Llamar a Publish-Module -Path /tmp/samplemodule -NuGetApiKey $env:NUGET_KEY -Verbose para subir la nueva versión a la Gallery.

En este flujo, los secretos de publicación nunca se escriben en claro en los scripts, se gestionan solo desde el proveedor de CI, y el módulo solo se publica si las pruebas de Pester han pasado previamente. Es la forma más limpia de no romper a los usuarios aguas abajo con versiones “rotas”.

Como toque final, se suele añadir un paso para subir los artefactos de pruebas producidos por Invoke-Pester (por ejemplo, un fichero XML o CliXml con resultados) usando la acción upload-artifact. Esto permite revisar qué falló aunque el job se marque como fallido, gracias a condiciones tipo if: ${{ always() }} que garantizan la subida incluso si Pester devuelve errores.

Al final, combinar módulos PowerShell bien empaquetados, pruebas exhaustivas con Pester y pipelines CI/CD que ejecutan, analizan y publican de forma automatizada te permite pasar de scripts sueltos y frágiles a un ecosistema de herramientas reutilizables, versionadas y confiables, con las que trabajar día a día es mucho más cómodo y predecible.