· 16 мин чтения

Pester 5 для инфраструктурного тестирования: от acceptance-тестов до операционной валидации

Я Семёнов Евгений Сергеевич, директор АйТи Фреш. За 15+ лет работы с Windows-инфраструктурами пришёл к железному правилу: если не могу проверить работу сервера одной командой, я не закрываю тикет. Pester — наш рабочий инструмент для этого: написали тест, запустили на сервере, получили зелёные галочки или красные строки с указанием, что именно сломалось. Его используем и после установки нового Exchange, и после миграции AD, и для ежедневной регресс-проверки критичных сервисов. В этой статье — как внедрить Pester 5 в инфраструктурную практику.

Что такое Pester и зачем он админам

Pester — фреймворк модульного тестирования для PowerShell. Исторически создан для разработчиков скриптов и модулей: чтобы проверять функции, моки, покрытие кода. Но начиная с версии 3 он стал стандартом для инфраструктурных тестов. Причины просты:

Установка последней версии:

Install-Module Pester -MinimumVersion 5.5.0 -Force -SkipPublisherCheck
Import-Module Pester -MinimumVersion 5.5.0
Get-Module Pester

Первый тест: проверка роли сервера

Задача — убедиться, что на сервере установлен IIS, запущена служба W3SVC и порт 80 слушается. Файл IIS.Tests.ps1:

Describe 'IIS base configuration on srv-web01' {
    BeforeAll {
        $target = 'srv-web01'
        $features = Invoke-Command -ComputerName $target { Get-WindowsFeature Web-* }
        $services = Invoke-Command -ComputerName $target { Get-Service W3SVC,WAS,WinRM }
    }
    Context 'Roles and features' {
        It 'Web-Server role is installed' {
            ($features | Where-Object Name -eq 'Web-Server').Installed | Should -BeTrue
        }
        It 'ASP.NET 4.5 is installed' {
            ($features | Where-Object Name -eq 'Web-Asp-Net45').Installed | Should -BeTrue
        }
    }
    Context 'Services' {
        It 'W3SVC is running' { ($services | Where-Object Name -eq 'W3SVC').Status | Should -Be 'Running' }
        It 'WAS is running' { ($services | Where-Object Name -eq 'WAS').Status | Should -Be 'Running' }
    }
    Context 'Network' {
        It 'TCP port 80 is listening' {
            (Test-NetConnection -ComputerName $target -Port 80).TcpTestSucceeded | Should -BeTrue
        }
        It 'TCP port 443 is listening' {
            (Test-NetConnection -ComputerName $target -Port 443).TcpTestSucceeded | Should -BeTrue
        }
    }
}

Запуск: Invoke-Pester -Path .\IIS.Tests.ps1. Вывод цветной: зелёное — прошло, красное — сломано с указанием, чего не хватает.

Discovery и Run фазы в Pester 5

Pester 5 работает в две фазы:

  1. Discovery — проходит по всем файлам, строит дерево Describe/Context/It. В этот момент выполняется только код, собирающий структуру: BeforeDiscovery, параметры, -ForEach. Любые тяжёлые операции (Invoke-Command, запросы к AD) на этой фазе делать нельзя — сломается параметризация.
  2. Run — выполняет BeforeAll, BeforeEach, It-блоки. Здесь уже реальные запросы.

Это принципиально отличает v5 от v4. Если хотите параметризовать тесты списком серверов — используйте BeforeDiscovery:

BeforeDiscovery {
    $servers = Get-Content .\servers.txt
}

Describe 'Base server health' -ForEach $servers {
    BeforeAll {
        $target = $_
        $os = Get-CimInstance Win32_OperatingSystem -ComputerName $target
    }
    It 'Uptime less than 90 days on <_>' {
        ((Get-Date) - $os.LastBootUpTime).TotalDays | Should -BeLessThan 90
    }
    It 'C: free space > 10% on <_>' {
        $c = Get-CimInstance Win32_LogicalDisk -ComputerName $target -Filter "DeviceID='C:'"
        (100 * $c.FreeSpace / $c.Size) | Should -BeGreaterThan 10
    }
}

Полный набор тестов для Windows Server baseline

Describe 'Baseline Windows Server compliance' -ForEach (Get-Content .\servers.txt) {
    BeforeAll { $t = $_ }

    Context 'Security' {
        It 'Windows Defender real-time enabled on <_>' {
            (Invoke-Command -ComputerName $t { (Get-MpComputerStatus).RealTimeProtectionEnabled }) |
                Should -BeTrue
        }
        It 'RDP requires NLA on <_>' {
            (Invoke-Command -ComputerName $t {
                (Get-ItemProperty 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp').UserAuthentication
            }) | Should -Be 1
        }
        It 'Firewall is enabled on <_>' {
            (Invoke-Command -ComputerName $t { (Get-NetFirewallProfile).Enabled }) |
                Should -Not -Contain 'False'
        }
    }
    Context 'Updates' {
        It 'No pending reboot on <_>' {
            (Invoke-Command -ComputerName $t {
                Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
            }) | Should -BeFalse
        }
    }
    Context 'Time' {
        It 'NTP sync is healthy on <_>' {
            (Invoke-Command -ComputerName $t { w32tm /query /status | Select-String 'Source' }) |
                Should -Match 'corp\.ntp'
        }
    }
}

Pester Configuration и запуск

$config = New-PesterConfiguration
$config.Run.Path = '.\tests'
$config.Run.PassThru = $true
$config.Output.Verbosity = 'Detailed'
$config.TestResult.Enabled = $true
$config.TestResult.OutputFormat = 'NUnitXml'
$config.TestResult.OutputPath = '.\reports\pester-result.xml'
$config.CodeCoverage.Enabled = $false
$config.Filter.Tag = @('Security','Network')   # только нужные теги

$result = Invoke-Pester -Configuration $config
Write-Host "Passed: $($result.PassedCount), Failed: $($result.FailedCount)"
exit [int]($result.FailedCount -gt 0)

Выход с ненулевым кодом при failed-тестах — стандартное условие для CI/CD: если тесты не прошли, pipeline красный.

Operational Validation Framework

У Microsoft есть пакет OperationValidation, который задаёт структуру директорий:

MyApp\
├── Diagnostics\
│   ├── Simple\           # быстрые тесты (минуты)
│   │   └── HealthCheck.Tests.ps1
│   └── Comprehensive\    # долгие (десятки минут, аудит)
│       └── FullCompliance.Tests.ps1
└── MyApp.psd1
Install-Module OperationValidation -Force
Invoke-OperationValidation -ModuleName 'MyApp' -TestType Simple

Это удобно для коллег: приходит новый админ, запускает одну команду — получает статус системы.

Мини-кейс: приёмка после миграции AD

В марте 2025 мы мигрировали клиента (розничная сеть, 180 РМ, 6 DC) с Windows Server 2012 R2 на 2022. После переноса нужно было быстро убедиться, что всё работает: функциональные уровни поднялись, SYSVOL работает на DFSR, FSMO на нужных DC, GPO на старом и новом среде совпадают, репликация здорова. Раньше это делали руками часа за 4, ночью, со страданиями.

Написали Pester-набор из 87 тестов (FSMO, репликация, SYSVOL, GPO-суммы, CNAME PDC, LDAP-поисковые атрибуты, группа Schema Admins пустая, DSRM-пароль одинаковый). Запустили сразу после миграции на виртуалке с Xeon Platinum 8280, 64 ГБ RAM, 40G Mellanox на дата-центре МТС — отработало за 6 минут. 3 теста красные: на одном DC не мигрировал SYSVOL на DFSR, на втором — неправильный DNS forwarder. Починили за 40 минут до утра понедельника, пользователи ничего не заметили. С тех пор одна и та же связка AD-тестов валидирует каждый проект миграции. Стоимость разработки базового набора — 60 тыс. руб.

Интеграция с GitLab CI

stages: [test]

pester:
  stage: test
  tags: [windows]
  script:
    - pwsh -c "Import-Module Pester; Invoke-Pester -Configuration (Import-PowerShellDataFile .\PesterConfig.psd1)"
  artifacts:
    when: always
    reports:
      junit: reports/pester-result.xml
    paths:
      - reports/
    expire_in: 30 days

После запуска pipeline GitLab UI показывает сами тесты, время выполнения, красные/зелёные, историю. Команда сразу видит, какой тест начал фейлиться после какого коммита.

Моки и Arrange/Act/Assert

Для тестов скриптов и модулей (не инфраструктуры) Pester даёт мокинг:

Describe 'Get-DiskSpaceReport' {
    BeforeAll { . "$PSScriptRoot\Get-DiskSpaceReport.ps1" }
    It 'Filters drives with less than 10% free' {
        Mock Get-CimInstance {
            @(
                [pscustomobject]@{ DeviceID='C:'; Size=100GB; FreeSpace=5GB },
                [pscustomobject]@{ DeviceID='D:'; Size=100GB; FreeSpace=50GB }
            )
        }
        $r = Get-DiskSpaceReport -ComputerName localhost -ThresholdPct 10
        $r.Count | Should -Be 1
        $r.DeviceID | Should -Be 'C:'
    }
}

Best practices

Внедрим Pester-тесты в вашу практику

Напишу набор инфраструктурных тестов под ваш парк: baseline Windows Server, AD-здоровье, SQL, Exchange, файловые серверы. Интеграция с GitLab CI или Jenkins. 15+ лет опыта с Windows-инфраструктурами от 30 до 800 рабочих мест. Тестовый стенд — Dell с Xeon Platinum 8280 и 40G Mellanox в дата-центре МТС, если нужна отдельная среда.

Телефон: +7 903 729-62-41
Telegram: @ITfresh_Boss
Семёнов Евгений Сергеевич, директор АйТи Фреш

FAQ — частые вопросы по Pester

Чем Pester 5 отличается от 4?
Pester 5 поддерживает Discovery и Run-фазы, новую конфигурацию через New-PesterConfiguration, BeforeDiscovery, Contexts с параметрами, параллельное выполнение. Синтаксис местами несовместим с 4.
Нужен ли Pester, если у меня уже есть мониторинг?
Мониторинг следит за текущим состоянием непрерывно. Pester-тесты проверяют соответствие эталону — удобно после установки, миграции, восстановления из бэкапа. Они дополняют мониторинг, а не заменяют.
Как запускать Pester на удалённом сервере?
Либо через Invoke-Command с передачей скрипта, либо разложить тесты локально и запускать по расписанию, складывая NUnitXml в сетевую шару. Второй вариант надёжнее — WinRM может быть отключён.
Что такое Operational Validation Framework?
OVF — подход от Microsoft: разделяем тесты на Simple (быстрые, для каждого деплоя) и Comprehensive (долгие, для аудита). Структура Diagnostics\Simple и Diagnostics\Comprehensive с модулем OperationValidation для запуска.
Как интегрировать с GitLab CI или Azure DevOps?
Запускаете pwsh -c 'Invoke-Pester -Configuration (New-PesterConfiguration) -OutputFile result.xml -OutputFormat NUnitXml', а CI-система парсит NUnit-отчёт и показывает в UI. GitLab понимает из коробки в artifacts.reports.junit.

Подпишитесь на рассылку ITfresh

Раз в неделю — практические гайды для руководителя IT и сисадмина: безопасность, 1С, миграции, резервные копии, лайфхаки из реальных проектов.

Реквизиты оператора персональных данных

ООО «АЙТИ-ФРЕШ», ИНН 7719418495, КПП 771901001. Юридический адрес: 105523, г. Москва, Щёлковское шоссе, д. 92, корп. 7. Контакт: info@itfresh.ru, +7 903 729-62-41. Оператор обрабатывает e-mail подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.