[Из песочницы] Использование Pester для тестирования при разработке PowerShell скриптов
Когда пришлось писать сложные, большие скрипты на PowerShell и с течением времени изменять их, мне хотелось найти средство, которое позволит упростить проверку работоспособности моих скриптов. Таким средством оказался Pester — фреймворк для модульного тестирования.
О том, что он может и об основах его использования я и расскажу.
Pester позволяет писать тесты к любым исполняемым в powershell командам или скриптам. В том числе к функциям, кмдлетам, модулям. Он позволяет группировать тесты, так что вы можете запустить все тесты сразу, или тесты только определенной функции или тесты всех функций определенного скрипта.
Pester может быть запущен в консоли powershell или интегрирован в среду разработки. Pester поможет вам, если вы слышали про разработку через тестирование и хотели попробовать ее для разработки ваших скриптов. И если у вас есть уже готовые скрипты, для которых вы хотите сделать тесты — Pester тоже вам поможет.
Как начать. Загрузка и интеграция Pester с PowerShell ISE
Pester представляет собой модуль для powershell, написанный Scott Muc и опубликованный на Github. Для того, чтобы пользоваться Pester надо просто скачать его и распаковать в папку одну из папок Modules на вашем компьютере.
Актуальный для вашей системы список папок для хранения модулей powershell хранится в переменной окружения $env: PSModulePath. К примеру, список папок Modules с моего компьютера:
PS C:\> $env:PSModulePath -split ';'
F:\Users\sgerasimov\Documents\WindowsPowerShell\Modules
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\
Этот список может меняться при установке программного обеспечения, к примеру средства администрирования Lync Server при установке добавляют к списку путь к папке со своими модулями.
Воспользуйтесь папкой Modules в профиле текущего пользователя. Создайте ее с помощью проводника или с помощью powershell, как показано ниже:
cd $env:USERPROFILE\documents
new-item -Name WindowsPowerShell -ItemType directory
new-item -Path .\WindowsPowerShell -Name Modules -ItemType directory
После этого разархивируйте архив в папку Pester в папке Modules.
Чтобы интегрировать Pester с PowerShell ISE создайте в папке %UserProfile%\Documents\WindowsPowerShell файл Microsoft.PowerShellISE_profile.ps1 со следующим содержанием:
try
{
Import-Module Pester
}
catch
{
Write-Warning "Импорт модуля Pester не удался"
}
Если файл уже есть, то просто добавьте указанный выше код в файл.
Теперь, каждый раз, когда вы будете запускать PowerShell ISE модуль Pester будет подгружаться автоматически и вам останется лишь пользоваться им.
Как писать тесты и исполнять? Общая схема
Тесты пишутся в отдельных файлах. По-умолчанию предлагается следующее решение:
На каждый скрипт создается файл с именем имяскрипта.Tests.ps1. Например, у вас есть скрипт CreateUser.ps1 или вы планируете написать скрипт с таким именем. Тогда тесты для этого скрипта и его функций вы помещаете в файл CreateUser.Tests.ps1.
Когда вы напишите тесты и будете запускать их, Pester будет просматривать все файлы с ».Tests.» в имени в текущем и во вложенных каталогах и выполнять тесты из них. Это позволяет, например, хранить файлы с тестами во вложенной папке, а не в папке со скриптами.
Файл тестов представляет собой powershell скрипт с группами тестов. Можно задавать несколько уровней вложенности групп тестов пользуясь командами Describe и Context. Команда It описывает 1 тест.
Приведу совсем простой пример, который нам продемонстрирует как пользоваться Pester для написания и выполнения тестов. Для понимания схемы.
Пример
Допустим у вас есть скрипт, возвращающий после выполнения «Hello World!» и вам надо написать для него тест.
Файл скрипта HelloWorld.ps1 у вас уже есть:
return "Hello world!"
Создайте файл с именем HelloWorld.Tests.ps1. В нем будет находиться тест для вашего скрипта, который будет проверять, что скрипт после запуска возвращает «Hello world!»:
Describe "Проверка скрипта HelloWorld" {
it "Скрипт возвращает строку Hello World!" {
$result = .\HelloWorld.ps1
$result | Should Be "Hello World!"
}
}
Блок Describe описывает в целом какой скрипт тестируется, а в блоке It содержится сам тест. Вначале строкой
$result = .\HelloWorld.ps1
осуществляется выполнение скрипта и получение его результатов, а затем строкой
$result | Should Be "Hello World!"
описывается, каким должен быть полученный результат. Для этого используется команда Should которая выполняет проверку соответствия полученного значения заданному условию. А условие задается оператором Be, который говорит, что условие — это равенство строке «Hello World!».
Если проверка, заданная командой Should завершается успешно, то тест пройден, в ином случае тест считается проваленным.
Скопируйте код, указанный выше в файл HelloWorld.Tests.ps1 и сохраните этот файл.
После этого, убедитесь, что текущая директория указывает на папку, в которой находятся файлы HelloWorld.ps1 и HelloWorld.Tests.ps1. У меня это «F:\Projects\iLearnPester\Examples>» и выполните команду Invoke-Pester для запуска тестов:
Тест прошел успешно. Об этом свидетельствует зеленый цвет строки с названием теста (соответствует фразе после блока It). Если тест завершается неудачей, то названием теста выводится красным, а ниже указывается, что пошло не так.
Ожидалась строка «Hello World!», но скрипт вернул строку «Hello all!». Кроме того, указывается файл тестов и строка, на которой в файле тестов находится проваленный тест.
Если вы хотите попробовать разработку через тестирование. Тогда вы вначале пишете тест, а уж затем скрипт/функцию к нему.
Команда Should и оператор, следующий за ней (например, Be) вместе создают Утверждение. В Pester есть следующие утверждения:
- Should Be
- Should BeExactly
- Should Exist
- Should Contain
- Should ContainExactly
- Should Match
- Should MatchExactly
- Should Throw
- Should BeNullOrEmpty
Внутрь утверждения всегда можно вставить Not и сделать отрицание, например: Should Not Be, Should Not Exist.
Сравнивает один объект с другим и выдает исключение, если объекты не равны. Сравниваются строки без учета регистра, числа, массивы чисел и строк. Пользовательские объекты (pscustomobject) и ассоциативные массивы не сравниваются.
#строки
$a = "строка"
$a | Should Be "строка" #пройдет успешно
$a | Should Be "СТРОКА" #пройдет успешно
$a | Should Be "Другая строка" #пройдет неудачно
$a | Should Not Be "Другая строка" #пройдет успешно
#числа
$a = 10
$a | Should Be 10 #пройдет успешно
$a | Should Be 2 #пройдет неудачно
$a | Should Not 2 #пройдет успешно
#массивы чисел
$a = 1,2,3
$a | Should Be 1,2,3 #пройдет успешно
$a | Should Be 1,2,3,4 #пройдет успешно
$a | Should Be 4,5,6 #пройдет неудачно
#массивы строк
$a = "qwer","asdf","zxcv"
$a | Should Be "qwer","asdf","ZXCV" #пройдет успешно
$a | Should Be "qwer","asdf","zxcv", "rrr" #пройдет успешно
Should BeExtactly
То же, что и Should Be, только строки сравниваются с учетом регистра
$actual="Actual value"
$actual | Should BeExactly "Actual value" # пройдет успешно
$actual | Should BeExactly "actual value" # пройдет неудачно
Should Exist
Проверяет, что объект существует и доступен одному из PS провайдеров. Самое типичное — проверить что файл существует. По сути выполняет кмдлет test-path для переданного значения.
$actual=(Dir . )[0].FullName
Remove-Item $actual
$actual | Should Exist # Пройдет неудачно
import-module ActiveDirectory
$ADObjectFQDN = "AD:CN=Some User,OU=Users,DC=company,DC=com"
$ADObjectFQDN | Should Exist # Пройдет успешно если пользователь есть
$registryKey = "HKCU:\Software\Microsoft\Driver Signing"
$registryKey | Should Exist # Пройдет успешно если ветка реестра есть.
Учтите, что можно проверить лишь наличие ветки реестра таким образом, но не какого-то конкретного ключа, т.к. PS провайдер, работающий с реестром дает доступ к ключам как к свойствам ветвей реестра. Он не считает их объектами.Should Contain
Проверяет, что файл содержит заданный текст. Поиск выполняется без учета регистра и может использовать регулярные выражения.
Set-Content -Path c:\temp\file.txt -Value 'Съешь еще этих мягких французских булок'
'c:\temp\file.txt' | Should Contain 'Съешь Еще' # Пройдет успешно
'c:\temp\file.txt' | Should Contain 'Съешь*булок' # Пройдет успешно
Should ContainExactly
Проверяет, что файл содержит заданный текст. Поиск выполняется с учетом регистра и может использовать регулярные выражения.
Set-Content -Path c:\temp\file.txt -Value 'Съешь еще этих мягких французских булок'
'c:\temp\file.txt' | Should Contain 'Съешь Еще' # Пройдет неудачно
'c:\temp\file.txt' | Should Contain 'Съешь*булок' # Пройдет успешно
Should Match
Сравнивает две строки с использованием регулярных выражений без учета регистра.
"Вася" | Should Match ".ася" # Пройдет успешно
"Вася" | Should Match ([regex]::Escape(".ася")) # Пройдет неудачно
Should MatchExactly
Сравнивает две строки с использованием регулярных выражений с учетом регистра.
"Вася" | Should Match "ВАСЯ" # Пройдет неудачно
"Вася" | Should Match ".ася" # Пройдет успешно
Should Throw
Считается истинным, если в тестируемом скрипт-блоке происходит исключение. Можно так же указать ожидаемый текст исключения.
На вход передается скрипт-блок. С функциями, к сожалению, не работает.
{ необъявленнаяфункция } | Should Throw # Пройдет успешно
{ throw "Ошибка в функции проверки параметров" } | Should Throw "Ошибка в функции проверки параметров" # Пройдет успешно
{ throw "Ошибка в функции проверки результатов" } | Should Throw "Ошибка в функции проверки параметров" # Пройдет неудачно
{throw "Ошибка в функции проверки результатов"} | Should Throw "результатов" # Пройдет успешно
{ $foo = 1 } | Should Not Throw # Пройдет успешно
Should BeNullOrEmpty
Проверяет, что переданное значение равно $null или пусто (для строки, массива и т.п.). Тут стоит напомнить, что $null это не 0.
$a = $null
$b = 0
$c = [string]""
$d = @()
$a | Should BeNullOrEmpty # Пройдет успешно
$b | Should BeNullOrEmpty # Пройдет неудачно
$c | Should BeNullOrEmpty # Пройдет успешно
$d | Should BeNullOrEmpty # Пройдет успешно
Что он еще умеет?
Mock-функции.
В Pester есть mock-функции, которые позволяют перед вызовом теста переопределить какую-либо функцию или кмдлет.
Например, вы разрабатываете скрипт, который будет получать ip-адрес текущей машины и в зависимости от того к какой сети принадлежит этот адрес прописывать тот или иной dns-сервер в настройках адаптера. Но у вашей машины, на которой вы разрабатываете скрипт всего 1 ip адрес и менять его для тестов хлопотно. Тогда вы просто перед вызовом теста переопределите функцию, получающую ip-адрес так, чтобы она возвращала не текущий адрес, а нужный для проверки.
Вот эскиз нашего скрипта (назовем SmartChangeDNS.ps1).
$MoskowNetworkMask = "192.168.1.0/24"
$RostovNetworkMask = "192.168.2.0/24"
$IPv4Addresses = GetIPv4Addresses
foreach($Address in $IPv4Addresses)
{
if(CheckSubnet -cidr $MoskowNetworkMask -ip $Address)
{
#устанавливаете dns 192.168.1.1
}
if(CheckSubnet -cidr $RostovNetworkMask -ip $Address)
{
#устанавливаете dns 192.168.2.1
}
}
Он знает 2 маски сети в Москве и Ростове. Получает с помощью функции GetIPv4Addresses все IPv4 адреса текущей машины и дальше в цикле foreach проверяет принадлежность какого-либо адреса подсети функцией CheckSubnet. Функции GetIPv4Addresses и CheckSubnet вы уже написали и проверили. Теперь, чтобы проверить функции в целом, нам надо написать тесты, в которых мы переопределим функцию GetIPv4Addresses так, чтобы она возвращала нужный адрес. Вот как это делается:
describe "SmartChangeDNS" {
it "если компьютер в сети 192.168.1.0/24" {
Mock GetIPv4Addresses {return "192.168.1.115"}
.\SmartChangeDNS.ps1
$DNSServerAddres = Get-DnsClientServerAddress -InterfaceAlias "Ethernet" -AddressFamily IPv4 | Select -ExpandProperty ServerAddresses
$DNSServerAddres | Should Be "192.168.1.1"
}
it "если компьютер в сети 192.168.2.0/24" {
Mock GetIPv4Addresses {return "192.168.2.20"}
.\SmartChangeDNS.ps1
$DNSServerAddres = Get-DnsClientServerAddress -InterfaceAlias "Ethernet" -AddressFamily IPv4 | Select -ExpandProperty ServerAddresses
$DNSServerAddres | Should Be "192.168.2.1"
}
}
Теперь при исполнении скрипта дело дойдет до выполнения функции GetIPv4Addresses, будет исполнена не та ее версия, что указана в скрипте, а та, которую мы определили командой Mock.
Переопределение функций с помощью Mock позволяет абстрагироваться, когда нужно, от внешних систем, модулей или вызываемых функций.
TestDrive
Pester так же предоставляет временный PS-диск, который можно использовать для работы с файловой системой в рамках выполнения тестов. Такой диск существует в рамках одного блока Describe или Context.
Если диск создан в блоке Describe, то он и все файлы созданные на нем видны и доступны для модификации в блоках Context. Файлы, созданные в блоке Context с завершением этого блока удаляются и остаются лишь файлы, созданные в блоке Describe.