Когда пришлось писать сложные, большие скрипты на 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.

Комментарии (6)


  1. Nikobraz
    13.08.2015 21:08

    Меня мучает вопрос, который мне мешает учить PowerShell:
    Много ли причин писать

    new-item -Name WindowsPowerShell -ItemType directory

    вместо
    mkdir WindowsPowerShell


    И зачем так извращаться? Эта избыточность синтаксиса меня просто убивает.


    1. Yakhnev
      13.08.2015 21:19

      Совсем необязательно использовать подробный синтаксис, mkdir прекрасно работает в Powershell в виде обертки над New-item. Более того, есть алиас md.


    1. realscorp
      14.08.2015 07:25

      Так можно же посмотреть список и обязательность аргументов командлета через help имя_командлета -full и указывать только обязательные аргументы.
      Потом, можно заранее посмотреть список алиасов через get-command -CommandType alias.


    1. semengerasimov
      14.08.2015 13:43
      +2

      Все достаточно сильно зависит от обстоятельств.

      Если вы единственный пользователь ваших скриптов, то вам, возможно, и нет смысла использовать подробный синтаксис. Если вы до этого все задачи решали с помощью обычных cmd-файлов, powershell позволит вам использовать (до определенной степени) привычные вам имена команд и их формы.

      Подробный синтаксис полезен и удобен, когда вы пишите скрипты, которые придется поддерживать и изменять со временем (особенно тогда, когда это придется делать не вам). Когда над скриптами работают разные люди, с разным уровнем знания команд, пришедших из cmd, и псевдонимов кмдлетов. Подробный синтаксис дольше писать, но как правило, его удобнее читать.

      Сравните строки:

      Ls | ? {$_.psiscontainer} | % {“{0}`t{1}” –f $_.name, $_.lastaccesstime}
      
      Get-ChildItem | Where-Object {$_. Psiscontainer} | ForEach-Object {“{0}`t{1}” –f $_.name, $_.lastaccesstime}
      

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

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

      Аналогично с указыванием названий параметров. Вот две строки
      Set-AzureAclConfig “192.168.0.0/8” 100 “SiteConfig” –AddRule –ACL $AclObject –Action Permit
      
      Set-AzureAclConfig -RemoteSubnet “192.168.0.0/8” -Order 100 –Description “SiteConfig” –AddRule –ACL $AclObject –Action Permit
      

      Если читающему не знаком подробно кмдлет Set-AzureAclConfig, то ему будет непонятно что означают первые 3 параметра в первой строке. А при чтении второй строки такого непонимания нет.

      Mkdir WindowsPowerShell – наверное достаточно понятно и можно было бы написать и так, но у меня уже привычка почти все писать полными именами кмдлетов (почти, потому что cd я все таки использую). К тому же автозавершение и другие фичи в средах разработки очень упрощают прописывание подробного синтаксиса.


  1. Yakhnev
    13.08.2015 21:23

    Думаю, стоить упомянуть возможность запуска тестов Pester напрямую из Visual Studio с помощью PowerShell Tools for Visual Studio.


    1. semengerasimov
      13.08.2015 22:40

      Спасибо за дополнение, внес в статью.