Тестирование PowerShell с помощью Pester — практический подход к тому, чтобы эксплуатационные скрипты было труднее сломать

· · PowerShell, Pester, Windows, Тестирование, Автоматизация, CI, Использование существующих активов

1. Что нужно понять в первую очередь

PowerShell-скрипты начинаются как автоматизация небольших задач.

Собрать файлы. Найти что-то в логах. Сформировать CSV. Переместить старые файлы. Проверить состояние службы.

Пока каждый из них — это несколько десятков строк, их можно проверить визуально. Но по мере того как скрипт продолжает использоваться в рабочей практике, в него начинают закрадываться такие изменения:

  • добавляются новые целевые папки;
  • добавляются условия исключения;
  • меняются столбцы CSV;
  • перед удалением добавляется архивирование;
  • запуск переносится в планировщик задач или CI;
  • при ошибке добавляется уведомление.

На этом этапе «один раз сработало у меня локально» уже недостаточно.

Пугающая сторона PowerShell — оборотная сторона его удобства. Операции только для чтения можно пробовать спокойно, но для удаления, перемещения, перезаписи, перезапуска служб или изменения прав небольшая ошибка в условии может превратиться в инцидент.

Именно здесь пригодится Pester — фреймворк тестирования для PowerShell. В этой статье мы не стремимся охватить все возможности Pester, а разбираем подход к выстраиванию тестов, который делает существующие PowerShell-скрипты сложнее сломать на практике.

Тестирование PowerShell — это не только про написание чистого кода. Это инструмент, снижающий тревогу перед изменением и позволяющий подтвердить результат доказательно после изменения.

Код, встречающийся в этой статье, опубликован на GitHub как полный набор примеров, запускаемых через Invoke-Pester (тестируемый скрипт, тесты Pester и скрипт запуска для CI).

pester-powershell-test-maintenance - komurasoft-blog-samples (GitHub)

2. Что именно защищает Pester

Подключение Pester не делает автоматически всё безопасным. Первое, что нужно решить, — «что именно защищают тесты».

Для эксплуатационных скриптов PowerShell, как правило, окупается приоритет вот этих четырёх областей.

Что нужно защитить Что проверяют тесты
Логика условий Какие файлы, строки, пользователи, службы становятся объектом
Форма вывода Имена столбцов в CSV, свойства возвращаемого значения, число записей
Шаг перед опасной операцией Соответствуют ли объекты удаления, перемещения, остановки замыслу
Внешние зависимости Обращение к файловой системе, API, выполнению команд, дате/времени, переменным окружения

Тестировать в первую очередь стоит не само удаление, а логику выбора того, что удалять.

Например, в скрипте, удаляющем старые логи, не начинайте с тестирования Remove-Item. Сначала протестируйте, «какие логи выбираются в качестве объектов».

Такое разделение заметно упрощает тестирование.

Функция, собирающая объекты
  ↓
Шаг проверки и фиксации объектов
  ↓
Изменяющий шаг: перемещение, удаление, уведомление и т. п.

Выстраивание тестов PowerShell не означает немедленную масштабную переделку существующих скриптов. Начните с того, что вынесите логику принятия решений, стоящую перед опасной операцией, в функцию, и проверьте её возвращаемое значение через Pester.

3. Выровняйте версии

В этой статье предполагается Pester v5.

В старых средах Windows Pester может быть уже установлен — но версии v3. Вместо того чтобы использовать то, что уже есть в среде, сначала проверьте версию.

Get-Module Pester -ListAvailable |
  Sort-Object Version -Descending |
  Select-Object Name, Version, Path

Для новой установки ставьте из PowerShell Gallery.

Install-Module -Name Pester -Scope CurrentUser -Force -SkipPublisherCheck
Import-Module Pester
Get-Module Pester

При командной работе проверьте, чтобы версия Pester не расходилась между рабочей машиной разработчика, сервером сборки и средой выполнения задач.

Частый источник путаницы при тестировании PowerShell — не код, а различие версий тестового раннера.

В частности, в старых статьях и внутренних заметках может сохраняться синтаксис Pester версии 4 и раньше. Если вы настраиваете всё заново, ориентация на стиль v5 облегчит чтение в дальнейшем.

4. Определите расположение файлов

В Pester принято называть файлы тестов *.Tests.ps1.

Минимальная структура выглядит так.

scripts/
  Get-OldLogFile.ps1
  Get-OldLogFile.Tests.ps1

Для чуть более крупного проекта разделите src и tests.

src/
  public/
    Get-OldLogFile.ps1
    Remove-OldLogFile.ps1

tests/
  public/
    Get-OldLogFile.Tests.ps1
    Remove-OldLogFile.Tests.ps1

Подойдёт любой вариант. Важно закрепить единое правило.

  • один файл тестов на одну функцию;
  • имя файла тестов заканчивается на .Tests.ps1;
  • способ загрузки тестируемого кода единообразен;
  • модульные и интеграционные тесты не смешиваются слишком свободно.

Для начала достаточно расположить целевой .ps1 рядом с его .Tests.ps1.

5. Запускаем минимальный тест

Сначала подготовим простую функцию 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
}

Здесь для упрощения тестирования $Now можно передавать как аргумент.

Если функция каждый раз напрямую вызывает Get-Date, результат будет зависеть от дня запуска теста. Если дату сделать параметром, можно зафиксировать условие — «файлы старше 30 дней на момент 1 июня 2026 года» — и протестировать именно его.

Далее напишем тесты в 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 'возвращает только .log-файлы старше указанного числа дней' {
        $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 'завершается ошибкой для несуществующей папки' {
        { Get-OldLogFile -Path (Join-Path $TestDrive 'missing') } |
            Should -Throw
    }
}

Выполним.

Invoke-Pester -Output Detailed .\Get-OldLogFile.Tests.ps1

Используемый здесь $TestDrive — это временная область, которую Pester предоставляет для тестов. Вместо обращения к реальной папке C:\Logs или общему ресурсу можно работать только с файлами, созданными внутри теста. Для PowerShell-скриптов, включающих файловые операции, безопасно взять за привычку сначала использовать $TestDrive.

6. Пишите имена тестов как спецификацию

Строка, которую вы передаёте в It в Pester, — это не просто описание, а для того, кто будет читать её позже, небольшой документ спецификации.

Например, вот такое имя слабовато.

It 'works' {
    # ...
}

Непонятно, что именно должно работать.

На практике имена, включающие условие и ожидаемый результат, читаются легче.

It 'возвращает только .log-файлы старше указанного числа дней' {
    # ...
}

It 'не включает файлы ровно на дату истечения срока' {
    # ...
}

It 'завершается ошибкой для несуществующей папки' {
    # ...
}

Хорошие имена тестов окупаются в момент падения. Когда в логе CI видна такая строка, сразу понятно, что сломалось.

[-] Get-OldLogFile.не включает файлы ровно на дату истечения срока

Имя теста — это заметка для вас самих в будущем.

7. Добавляем одно граничное условие

Приведённая выше Get-OldLogFile определяет старые файлы по такому условию.

$_.LastWriteTime -lt $limit

Поскольку используется -lt, файлы с меткой времени, ровно совпадающей с датой истечения, исключаются.

Это небольшое решение, но на практике оно важно. «Старше 30 дней» и «начиная с 30 дней назад включительно» — разные условия, и они меняют число выбранных файлов.

Добавим граничное условие в тесты.

It 'не включает файлы ровно на дату истечения срока' {
    $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'
}

Дело не в том, чтобы писать как можно больше тестов. Но логика с границами — даты, числа, количество, права доступа, шаблоны имён файлов — это то место, где тесты приносят максимальную пользу.

8. Фиксируем форму возвращаемого значения

В PowerShell-скриптах форма возвращаемого значения может измениться незаметно.

Сначала функция напрямую возвращала объекты FileInfo. Потом кто-то добавил Select-Object. Затем имена столбцов изменили под вывод в CSV.

Такие изменения влияют на дальнейшую обработку, поэтому тестирование свойств возвращаемого значения позволяет заметить неожиданные изменения.

It 'возвращает свойства, используемые в дальнейшей обработке' {
    $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'
}

Для функций, передающих данные для вывода в CSV или формирования отчёта, имена столбцов — часть спецификации, а не только сами значения.

Проверяйте не только «сработало», но и «возвращается в форме, которую ожидает следующий шаг».

9. Отделяем удаление от выбора объектов

Теперь рассмотрим удаление. Начнём с плохого примера.

Get-ChildItem C:\Logs -Filter *.log -File |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
    Remove-Item -Force

Коротко и удобно, но сложно тестировать. Поскольку выбор объектов и удаление объединены в одном конвейере, непонятно, что и где проверять.

На практике разделяем так.

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
        }
    }
}

Здесь добавлен SupportsShouldProcess, чтобы функция могла принимать -WhatIf.

Remove-OldLogFile -Path C:\Logs -Days 30 -WhatIf

Для PowerShell-функций, выполняющих удаление, безопаснее по возможности предусматривать проверку через -WhatIf.

10. Заменяем опасные операции через Mock

В Pester с помощью Mock можно заменить реальное выполнение команды.

В тесте логики удаления реально выполнять Remove-Item не нужно.

Была ли команда вызвана там, где должна была? Не была ли она вызвана там, где не должна была?

Достаточно проверить именно это.

Пример Remove-OldLogFile.Tests.ps1.

BeforeAll {
    . $PSScriptRoot\Get-OldLogFile.ps1
    . $PSScriptRoot\Remove-OldLogFile.ps1
}

Describe 'Remove-OldLogFile' {
    It 'вызывает Remove-Item для старых лог-файлов' {
        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 'не вызывает Remove-Item при 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
    }
}

В этих тестах замоканы и Get-OldLogFile, и Remove-Item, поэтому реальный файл C:\Logs\old.log может не существовать. Проверяется именно логика принятия решений Remove-OldLogFile.

  • если объекты есть, вызвать Remove-Item;
  • при -WhatIf не вызывать Remove-Item;
  • при вызове передавать нужный путь.

Чем опаснее операция, тем безопаснее тестировать именно условия её вызова, а не само выполнение.

11. Не злоупотребляйте Mock

Mock удобен, но чрезмерное его использование снижает ценность тестов. Если замокать абсолютно всё, тесты слишком сильно отрываются от реального поведения PowerShell.

Ориентировочно так.

Операция Рекомендация
Дата Фиксировать через параметр
Создание файлов Использовать $TestDrive
Удаление и перемещение Проверять через Mock и -WhatIf
Вызовы веб-API Мокать Invoke-RestMethod и подобное
Отправка почты и уведомлений Мокать команду отправки
Чтение и запись CSV Создавать небольшие реальные файлы в $TestDrive

Если замокать даже чтение и запись файлов, можно упустить реальные проблемы с кодировкой символов, переносами строк и именами столбцов.

С другой стороны, такие операции, как удаление, уведомления, внешние API, остановка служб, действительно лучше не выполнять по-настоящему.

Разделяйте «где использовать реальное» и «где мокать».

12. Дорабатываем существующие скрипты для тестируемости

С внедрением Pester стиль написания существующих скриптов немного меняется. Однако с самого начала не требуется масштабный пересмотр архитектуры — для начала достаточно такой доработки.

До доработки

$limit = (Get-Date).AddDays(-30)

Get-ChildItem C:\Logs -Filter *.log -File |
    Where-Object { $_.LastWriteTime -lt $limit } |
    Remove-Item -Force

После доработки

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
            }
        }
}

Изменения невелики.

  • дату сделали параметром;
  • выбор объектов вынесли в функцию;
  • удаление вынесли в отдельную функцию;
  • добавили SupportsShouldProcess.

Уже этого достаточно, чтобы код стал заметно проще тестировать.

При выстраивании тестов PowerShell эффективнее не начинать с теории проектирования, а сделать «дату», «путь», «внешнюю команду», «изменяющую операцию» заменяемыми извне.

13. Определяем категории тестов

В Pester можно навешивать теги на Describe, Context и It.

Например, разделим быстрые модульные тесты и интеграционные тесты, обращающиеся к реальной среде.

Describe 'Get-OldLogFile' -Tag 'Unit' {
    It 'возвращает только .log-файлы старше указанного числа дней' {
        # Быстрый тест с использованием TestDrive
    }
}

Describe 'Log maintenance smoke test' -Tag 'Smoke' {
    It 'может прочитать реальную папку логов' {
        Test-Path -LiteralPath 'C:\Logs' | Should -BeTrue
    }
}

Выполним только модульные тесты.

Invoke-Pester -TagFilter Unit

Исключим медленные или зависящие от среды тесты.

Invoke-Pester -ExcludeTagFilter Slow, RequiresAdmin, Network

На практике попытка запускать абсолютно все тесты каждый раз иногда не приживается в команде.

Сначала сделайте стандартом быстрые тесты без побочных эффектов, а зависящие от среды тесты разделите тегами и запускайте, когда это действительно нужно.

14. Запускаем в CI

Pester полезен даже при локальном запуске, но если скрипты ведёт команда, возможность запускать его в CI даёт дополнительное спокойствие.

Например, подготовим файл вроде tools/Invoke-ProjectTests.ps1.

$ErrorActionPreference = 'Stop'

Import-Module Pester

$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

На стороне CI выполняем этот скрипт.

pwsh -NoProfile -File .\tools\Invoke-ProjectTests.ps1

Важно не переносить слишком много специфичных для CI настроек в сами файлы тестов.

Файлы тестов — это место, где пишется спецификация. Формат вывода для CI, покрытие, коды завершения и подобное проще держать организованными в скрипте запуска.

15. Покрытие — это карта, а не цель

Pester может выводить и покрытие кода. Однако поначалу не стоит слишком гнаться за этой цифрой, потому что покрытие — не то же самое, что качество тестов.

Например, если один раз вызвать функцию выбора объектов удаления, эти строки будут отмечены как покрытые. Но если граничные условия и условия исключения не проверены, это не даёт реальной уверенности на практике.

Используйте покрытие так:

  • находите функции, которые вообще не выполняются;
  • находите важные ветвления, для которых нет тестов;
  • расставляйте приоритеты, начиная со скриптов, которые меняются чаще всего;
  • сохраняйте в CI свидетельство того, что тесты выполнялись.

Смотрите не столько на рост цифры, сколько на то, «протестированы ли важные решения».

16. Порядок внедрения тестов в существующие скрипты

При внедрении Pester в существующий набор PowerShell-активов проще не покрывать всё тестами сразу. Вот рекомендуемый порядок.

1. Выберите скрипты, чей сбой был бы болезненным

Лучшие первые кандидаты выглядят так:

  • включают удаление, перемещение, перезапись;
  • запускаются ежедневно или ежемесячно;
  • есть в регламенте, но понятны только одному человеку;
  • в прошлом уже были ошибки в условиях;
  • их выходной CSV используется в другом бизнес-процессе.

Начните с того, что полезно, но было бы болезненно потерять при сбое.

2. Вынесите в функции только часть, связанную с чтением

Первым делом тестируйте не изменяющую логику, а логику чтения.

Прочитать логи
Отфильтровать объекты
Посчитать количество
Привести к форме для CSV

Эту часть легко тестировать с $TestDrive, и аварии здесь маловероятны.

3. Передавайте дату и путь извне

Жёстко зашитые дата и путь усложняют тестирование.

# Стоит избегать
$root = 'C:\Logs'
$limit = (Get-Date).AddDays(-30)

Тестируемая форма выглядит так.

param(
    [string] $Path,
    [datetime] $Now = (Get-Date)
)

Уже сама возможность передавать значения извне значительно повышает стабильность тестов.

4. Опасные операции — в конец

Удаление и перемещение группируйте в конце.

Сформировать список объектов
  ↓
Зафиксировать объекты в логе
  ↓
Проверить через -WhatIf
  ↓
Выполнить

Проверяйте в тестах в том же порядке.

17. Частые затруднения

Симптом Причина Решение
Проходит локально, но падает в CI Отличается текущий каталог Строить пути от $PSScriptRoot
Результат меняется день ото дня Прямое использование Get-Date Предусмотреть параметр вроде -Now
Тест едва не удаляет реальные файлы Используются реальные папки Использовать $TestDrive и Mock
Mock не срабатывает Несовпадение границ модуля или области видимости Проверить -ModuleName и способ загрузки
Непонятно, до какого предела тестировать Логика не разбита на функции Разделить на выбор объектов, оформление и изменение
Тесты медленные Обращение к внешним сервисам или сети Мокать внешние зависимости в модульных тестах
Имена тестов ничего не говорят Имена вроде It 'works' Включать в имя условие и ожидаемый результат

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

Части, которые трудно тестировать, обычно и есть те части, которые чаще ломаются в эксплуатации.

18. Правила, которые стоит закрепить для поддержания тестов

Если PowerShell-скрипты ведёт команда, правила стоит закрепить раньше, чем спорить о мелких деталях стиля.

Например, такие правила:

  • файлы тестов называются *.Tests.ps1;
  • тестируемый код загружается относительно $PSScriptRoot;
  • тесты файловых операций используют $TestDrive;
  • удаление, перемещение, уведомления, вызовы API по умолчанию мокаются;
  • дата параметризована так, чтобы её можно было зафиксировать;
  • блоки Describe или It несут теги вроде Unit, Smoke, RequiresAdmin;
  • в CI по умолчанию запускается Unit;
  • изменяющие функции по возможности получают SupportsShouldProcess;
  • прошлые дефекты сохраняются как регрессионные тесты.

Слишком много правил — и они перестают соблюдаться. Поначалу достаточно всего этих трёх:

Использовать TestDrive
Фиксировать дату
Мокать опасные операции

Соблюдение только этих трёх правил уже заметно стабилизирует тестирование PowerShell.

19. Решите также, что не нужно тестировать

При выстраивании тестов «что не тестировать» так же важно, как «что тестировать».

Например, следующее не стоит через силу проверять в модульных тестах:

  • что собственный Get-ChildItem Windows работает корректно;
  • что Remove-Item действительно удаляет файлы;
  • внутреннее поведение стандартных команд PowerShell;
  • что внешний API всегда отвечает;
  • что сетевой ресурс всегда доступен.

Тестировать нужно ваши собственные решения:

  • при каких условиях объект становится целью;
  • какой путь передаётся;
  • какие столбцы выводятся;
  • как обрабатываются сбои;
  • можно ли прогнать опасную операцию вхолостую.

Разделяйте места, где вы доверяете стандартным командам, и места, где защищаете собственную логику.

20. Итог

PowerShell — удобный инструмент для быстрой автоматизации повседневной работы. Но скрипты, остающиеся в эксплуатации надолго, постепенно набирают вес ответственности. То, что начиналось как однострочная команда только для себя, со временем превращается в эксплуатационный процесс, работающий каждый день и влияющий на работу других людей и бизнес-данные.

Выстраивание тестов с помощью Pester — это работа по защите скриптов по мере этого сдвига.

Перечислим ключевые моменты.

  • Сначала тестируйте выбор объектов.
  • Сделайте дату и путь передаваемыми извне.
  • Заключайте файловые операции в $TestDrive.
  • Мокайте удаление, перемещение, уведомления, вызовы API.
  • Делайте изменяющие функции прогоняемыми вхолостую через -WhatIf.
  • Пишите имена тестов так, чтобы их можно было читать как спецификацию.
  • В CI начинайте с быстрых тестов без побочных эффектов.

Безопасная эксплуатация PowerShell не появляется от внедрения сразу какого-то большого механизма.

Разбивайте на маленькие функции. Пишите маленькие тесты. Делайте так, чтобы можно было проверить перед опасным шагом.

Через такое накопление PowerShell превращается из «удобного, но немного пугающего скрипта» в «бизнес-инструмент, который можно проверить после каждого изменения».

Справочные ссылки

Похожие статьи

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Что в PowerShell-скрипте нужно протестировать в Pester в первую очередь?
Логику выбора объектов — раньше, чем саму опасную операцию. Для скрипта, удаляющего старые логи, не стоит начинать с тестирования Remove-Item — сначала протестируйте, какие логи выбираются в качестве объектов удаления. Вынесение этой логики принятия решений в функцию и проверка её возвращаемого значения через Pester дают максимум безопасности при минимуме усилий, поскольку именно ошибки в условиях выбора объектов превращаются в инциденты.
Как в Pester тестировать файловые операции, не трогая реальные папки?
Используйте $TestDrive — временную область, которую Pester предоставляет для тестов. Вместо обращения к реальной папке C:\Logs или общему ресурсу вы создаёте файлы только внутри теста и при необходимости задаёте их временные метки. Для по-настоящему опасных операций — удаления, перемещения, уведомлений, вызовов API — используйте Mock и проверяйте через Should -Invoke, что команда была вызвана (или не вызвана) с нужными параметрами, в сочетании с поддержкой -WhatIf через SupportsShouldProcess.
До какого предела стоит использовать Mock в Pester?
Операции, которые нельзя выполнять по-настоящему — удаление, перемещение, вызовы веб-API, отправку писем и уведомлений — стоит заменять через Mock, дату фиксировать аргументом, а для создания файлов и чтения/записи CSV лучше подходят небольшие реальные файлы в $TestDrive. Если замокать вообще всё, тесты слишком сильно отрываются от реального поведения PowerShell, и можно упустить проблемы с кодировкой, переносами строк или именами столбцов, поэтому важно разделять «где использовать реальное», а «где мокать».
Почему тест Pester проходит локально, но падает в CI?
Частая причина — разница в текущем каталоге, и она решается, если загрузка тестируемого кода строится от $PSScriptRoot. Если результат меняется день ото дня, причина в прямом использовании Get-Date — дату нужно зафиксировать через параметр вроде -Now. Если Mock не срабатывает, стоит заподозрить границы модуля или область видимости и проверить -ModuleName и способ загрузки кода. То, что выглядит как проблема Pester, часто на самом деле вызвано структурой самого скрипта.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог