Pull to refresh

Использование Pester для тестирования при разработке PowerShell скриптов

Reading time 9 min
Views 14K
Когда пришлось писать сложные, большие скрипты на PowerShell и с течением времени изменять их, мне хотелось найти средство, которое позволит упростить проверку работоспособности моих скриптов. Таким средством оказался Pester — фреймворк для модульного тестирования.

О том, что он может и об основах его использования я и расскажу.

Pester позволяет писать тесты к любым исполняемым в powershell командам или скриптам. В том числе к функциям, кмдлетам, модулям. Он позволяет группировать тесты, так что вы можете запустить все тесты сразу, или тесты только определенной функции или тесты всех функций определенного скрипта.

Pester может быть запущен в консоли powershell или интегрирован в среду разработки. При этом это может быть как одна из созданных для PowerShell сред разработки (PowerShell ISE и т.п.), так и Visual Studio с помощью PowerShell Tools for Visual Studio 2015. Pester поможет вам, если вы слышали про разработку через тестирование и хотели попробовать ее для разработки ваших скриптов. И если у вас есть уже готовые скрипты, для которых вы хотите сделать тесты – Pester тоже вам поможет.

Как начать. Загрузка и интеграция Pester с PowerShell ISE


Pester представляет собой модуль для powershell, написанный Scott Muc и опубликованный на Github. Для того, чтобы пользоваться Pester надо просто скачать его и распаковать в папку одну из папок Modules на вашем компьютере.



Что за папка Modules?
Папок Modules несколько. К примеру, папка Modules по пути %UserProfile%\Documents\WindowsPowerShell\Modules позволяет хранить модули, которые необходимы только вашей учетной записи. Причем, как правило, этой папки не существует, пока вы самостоятельно ее не создадите. А в папке %windir%\system32\WindowsPowerShell\v1.0\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.

Расскажу подробнее про утверждения
Should Be

Сравнивает один объект с другим и выдает исключение, если объекты не равны. Сравниваются строки без учета регистра, числа, массивы чисел и строк. Пользовательские объекты (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.
Tags:
Hubs:
+10
Comments 8
Comments Comments 8

Articles