PowerShellモジュールをテスト駆動で開発していきます。環境は以下の通り。
- Windows 10 Enterprise
- PowerShell 5.1
- Pester 4.0.2
- Visual Studio Code 1.10.1
- PowerShell Language Support for Visual Studio Code 0.9.0
お題はおなじみFizzBuzzです。
FizzBuzz モジュールの仕様
FizzBuzzモジュールは以下の 2 つの Function を含みます。
- Get-FizzBuzz
- Invoke-FizzBuzz
Get-FizzBuzz は渡された値が 3 で割り切れる場合は 'Fizz' 、5 で割り切れる場合は 'Buzz'、両方で割り切れる場合は 'FizzBuzz' 、その他の数字の場合はその数字を返します。Invoke-FizzBuzz は渡された数字の範囲で FizzBuzz の結果を出力します。
Get-FizzBuzz -Value 3 # Fizz
Get-FizzBuzz -Value 5 # Buzz
Get-FizzBuzz -Value 15 # FizzBuzz
Get-FizzBuzz -Value 1 # 1
Invoke-FizzBuzz -Start 1 -End 5 # 1,2,'Fizz',4,'Buzz'
モジュールの作成
以下のコマンドを実行すると FizzBuzz フォルダーが作成され、その配下に FizzBuzz.ps1 と FizzBuzz.Tests.ps1 が配置されます。
New-Fixture -Path $env:USERPROFILE\Documents\WindowsPowerShell\Modules\FizzBuzz -Name FizzBuzz
FizzBuzz.ps1 をモジュールにするため拡張子を FizzBuzz.psm1 に変更します。ここまで出来たら Visual Studio Code で $env:USERPROFILE\Documents\WindowsPowerShell\Modules\FizzBuzz フォルダーを開きましょう。
FizzBuzz.Tests.ps1 の冒頭を以下の通り書き換えます。
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.ps1', '.psm1'
Import-Module "$here\$sut" -Force
統合ターミナルを開いて、以下のコマンドを実行してテストが実行されることを確認します。(テストは失敗します。)
Invoke-Pester #Red
これでモジュールとテストコードの準備ができました。
Get-FizzBuzz の実装
それでは FizzBuzz モジュールに Get-FizzBuzz Function を記述していきましょう。FizzBuzz.psm1 を以下のように変更します。Visual Studio Code では cmdlet スニペットが使えます。
function Get-FizzBuzz {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[int]$Value
)
}
続いて FizzBuzz.Tests.ps1 を次のように書き換えましょう。
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.ps1', '.psm1'
Import-Module "$here\$sut" -Force
Describe "Get-FizzBuzz" {
It "3のときに'Fizz'を返す" {
Get-FizzBuzz -Value 3 | Should Be 'Fizz'
}
}
統合ターミナルでテストを実行します。
Invoke-Pester #Red
Get-FizzBuzz に処理を何も書いていないので、当然ながらテストは失敗します。
3 で割り切れる場合は 'Fizz' を返すようにコードを修正しましょう。
function Get-FizzBuzz {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[int]$Value
)
if ($Value % 3 -eq 0) {
return 'Fizz'
}
}
再度テストを実行すると今度は成功します。
Invoke-Pester #Green
どんどん行きましょう。次は 5 で割り切れる場合です。まずはテストから。
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.ps1', '.psm1'
Import-Module "$here\$sut" -Force
Describe "Get-FizzBuzz" {
It "3のときに'Fizz'を返す" {
Get-FizzBuzz -Value 3 | Should Be 'Fizz'
}
It "5のときに'Buzz'を返す" {
Get-FizzBuzz -Value 5 | Should Be 'Buzz'
}
}
Invoke-Pester #Red
FizzBuzz.Tests.ps1 に処理を追加します。
function Get-FizzBuzz {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[int]$Value
)
if ($Value % 3 -eq 0) {
return 'Fizz'
} elseif ($Value % 5 -eq 0) {
return 'Buzz'
}
}
Invoke-Pester #Green
続いて 3 でも 5 でも割り切れる場合のテスト。
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.ps1', '.psm1'
Import-Module "$here\$sut" -Force
Describe "Get-FizzBuzz" {
It "3のときに'Fizz'を返す" {
Get-FizzBuzz -Value 3 | Should Be 'Fizz'
}
It "5のときに'Buzz'を返す" {
Get-FizzBuzz -Value 5 | Should Be 'Buzz'
}
It "15のときに'FizzBuzz'を返す" {
Get-FizzBuzz -Value 15 | Should Be 'FizzBuzz'
}
}
Invoke-Pester #Red
修正。
function Get-FizzBuzz {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[int]$Value
)
if ($Value % 3 -eq 0) {
return 'Fizz'
} elseif ($Value % 5 -eq 0) {
return 'Buzz'
} elseif ($Value % 3 -eq 0 -and $Value % 5 -eq 0) {
return 'FizzBuzz'
}
}
Invoke-Pester #Red!
はい、順番が違いますね。再度コードを修正します。
function Get-FizzBuzz {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[int]$Value
)
if ($Value % 3 -eq 0 -and $Value % 5 -eq 0) {
return 'FizzBuzz'
} elseif ($Value % 3 -eq 0) {
return 'Fizz'
} elseif ($Value % 5 -eq 0) {
return 'Buzz'
}
}
Invoke-Pester #Green
今度は OK です。最後に数字をそのまま返すパターンを書いていきましょう。
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.ps1', '.psm1'
Import-Module "$here\$sut" -Force
Describe "Get-FizzBuzz" {
It "3のときに'Fizz'を返す" {
Get-FizzBuzz -Value 3 | Should Be 'Fizz'
}
It "5のときに'Buzz'を返す" {
Get-FizzBuzz -Value 5 | Should Be 'Buzz'
}
It "15のときに'FizzBuzz'を返す" {
Get-FizzBuzz -Value 15 | Should Be 'FizzBuzz'
}
It "1のときに1を返す" {
Get-FizzBuzz -Value 1 | Should Be 1
}
}
Invoke-Pester #Red
修正します。
function Get-FizzBuzz {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[int]$Value
)
if ($Value % 3 -eq 0 -and $Value % 5 -eq 0) {
return 'FizzBuzz'
} elseif ($Value % 3 -eq 0) {
return 'Fizz'
} elseif ($Value % 5 -eq 0) {
return 'Buzz'
} else {
return $Value
}
}
Invoke-Pester #Green
はい、いったんこれで完成です。
Get-FizzBuzz の実装(異常系)
正常系は問題なくなったので異常系を考えていきましょう。パラメーターが以下の場合を異常系として例外がスローされるようにします。
- 1 より小さい場合(ゼロまたは負数)
- 数字以外の場合
なお、Pester で例外のスローをテストする場合はインプットオブジェクトを ScriptBlock {} にする必要があります。
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.ps1', '.psm1'
Import-Module "$here\$sut" -Force
Describe "Get-FizzBuzz" {
It "3のときに'Fizz'を返す" {
Get-FizzBuzz -Value 3 | Should Be 'Fizz'
}
It "5のときに'Buzz'を返す" {
Get-FizzBuzz -Value 5 | Should Be 'Buzz'
}
It "15のときに'FizzBuzz'を返す" {
Get-FizzBuzz -Value 15 | Should Be 'FizzBuzz'
}
It "1のときに1を返す" {
Get-FizzBuzz -Value 1 | Should Be 1
}
It "0のときに例外をスローする" {
{Get-FizzBuzz -Value 0} | Should Throw
}
It "-1のときに例外をスローする" {
{Get-FizzBuzz -Value -1} | Should Throw
}
}
Invoke-Pester #Red
$Value は 1 以上の数だけ受け入れるようにバリデーションします。上限は int 型の最大値にしています。
function Get-FizzBuzz {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateRange(1,[int]::MaxValue)]
[int]$Value
)
if ($Value % 3 -eq 0 -and $Value % 5 -eq 0) {
return 'FizzBuzz'
} elseif ($Value % 3 -eq 0) {
return 'Fizz'
} elseif ($Value % 5 -eq 0) {
return 'Buzz'
} else {
return $Value
}
}
Invoke-Pester #Green
次は数字以外の場合、、、ですがこれは何もしなくても型キャストに失敗して例外になるのでモジュールの修正は無しです。
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.ps1', '.psm1'
Import-Module "$here\$sut" -Force
Describe "Get-FizzBuzz" {
It "3のときに'Fizz'を返す" {
Get-FizzBuzz -Value 3 | Should Be 'Fizz'
}
It "5のときに'Buzz'を返す" {
Get-FizzBuzz -Value 5 | Should Be 'Buzz'
}
It "15のときに'FizzBuzz'を返す" {
Get-FizzBuzz -Value 15 | Should Be 'FizzBuzz'
}
It "1のときに1を返す" {
Get-FizzBuzz -Value 1 | Should Be 1
}
It "0のときに例外をスローする" {
{Get-FizzBuzz -Value 0} | Should Throw
}
It "-1のときに例外をスローする" {
{Get-FizzBuzz -Value -1} | Should Throw
}
It "aのときに例外をスローする" {
{Get-FizzBuzz -Value -1} | Should Throw
}
}
Invoke-Pester #Green
これで Get-FizzBuzz の実装はおしまいです。ちなみに $Value を int 型にしているので、小数点付きの数が渡された場合は整数にキャストします。基本的には四捨五入なんですが、小数部が .5 の場合は偶数に寄せられます。(1.5 は 2、2.5 も 2 になる)
そのため 0.5 が渡された場合は 0 にキャストされて例外がスローされますが、0.6 が渡された場合は 1 が返ってきます。
Invoke-FizzBuzz の実装
続いて Invoke-FizzBuzz です。モジュールファイルの Get-FizzBuzz Function の下に Invoke-FizzBuzz を定義します。
function Invoke-FizzBuzz {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateRange(1,[int]::MaxValue)]
[int]
$Start,
[Parameter(Mandatory=$true)]
[ValidateRange(1,[int]::MaxValue)]
[int]
$End
)
}
テストコードを記述します。Invoke-FizzBuzz の結果を配列にして、期待される結果の配列と要素数が同じことを確認してから、配列の要素をひとつずつ比較します。Describe "Get-FizzBuzz" の下に新たに Describe "Invoke-FizzBuzz" を記述していきます。
Describe "Invoke-FizzBuzz" {
It "1から15のときに '1,2,Fizz,4,Buzz,6,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz' を返す" {
$results = Invoke-FizzBuzz -Start 1 -End 15
$expects = 1,2,'Fizz',4,'Buzz','Fizz',7,8,'Fizz','Buzz',11,'Fizz',13,14,'FizzBuzz'
$results.Count | Should Be $expects.Count
for ($i = 0; $i -lt $results.Count; $i++) {
$results[$i] | Should Be $expects[$i]
}
}
}
Invoke-Pester #Red
Invoke-FizzBuzz を実装します。
function Invoke-FizzBuzz {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateRange(1,[int]::MaxValue)]
[int]
$Start,
[Parameter(Mandatory=$true)]
[ValidateRange(1,[int]::MaxValue)]
[int]
$End
)
for ($i = $Start; $i -le $End; $i++) {
Get-FizzBuzz $i
}
}
Invoke-Pester #Green
$End が $Start より小さい場合は例外をスローすることにします。
Describe "Invoke-FizzBuzz" {
It "1から15のときに '1,2,Fizz,4,Buzz,6,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz' を返す" {
$results = Invoke-FizzBuzz -Start 1 -End 15
$expects = 1,2,'Fizz',4,'Buzz','Fizz',7,8,'Fizz','Buzz',11,'Fizz',13,14,'FizzBuzz'
$results.Count | Should Be $expects.Count
for ($i = 0; $i -lt $results.Count; $i++) {
$results[$i] | Should Be $expects[$i]
}
}
It "最後の値が最初の値より小さい場合は例外をスローする" {
{Invoke-FizzBuzz -Start 2 -End 1} | Should Throw
}
}
Invoke-Pester #Red
モジュールを修正します。
function Invoke-FizzBuzz {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateRange(1,[int]::MaxValue)]
[int]
$Start,
[Parameter(Mandatory=$true)]
[ValidateRange(1,[int]::MaxValue)]
[int]
$End
)
if ($End -lt $Start) {
throw "The end value must be greater than or equal to the start value."
}
for ($i = $Start; $i -le $End; $i++) {
Get-FizzBuzz $i
}
}
Invoke-Pester #Green
FizzBuzz モジュールの完成です。最終的なコードは以下の通り。
function Get-FizzBuzz {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateRange(1,[int]::MaxValue)]
[int]$Value
)
if ($Value % 3 -eq 0 -and $Value % 5 -eq 0) {
return 'FizzBuzz'
} elseif ($Value % 3 -eq 0) {
return 'Fizz'
} elseif ($Value % 5 -eq 0) {
return 'Buzz'
} else {
return $Value
}
}
function Invoke-FizzBuzz {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateRange(1,[int]::MaxValue)]
[int]
$Start,
[Parameter(Mandatory=$true)]
[ValidateRange(1,[int]::MaxValue)]
[int]
$End
)
if ($End -lt $Start) {
throw "The end value must be greater than or equal to the start value."
}
for ($i = $Start; $i -le $End; $i++) {
Get-FizzBuzz $i
}
}
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.ps1', '.psm1'
Import-Module "$here\$sut" -Force
Describe "Get-FizzBuzz" {
It "3のときに'Fizz'を返す" {
Get-FizzBuzz -Value 3 | Should Be 'Fizz'
}
It "5のときに'Buzz'を返す" {
Get-FizzBuzz -Value 5 | Should Be 'Buzz'
}
It "15のときに'FizzBuzz'を返す" {
Get-FizzBuzz -Value 15 | Should Be 'FizzBuzz'
}
It "1のときに1を返す" {
Get-FizzBuzz -Value 1 | Should Be 1
}
It "0のときに例外をスローする" {
{Get-FizzBuzz -Value 0} | Should Throw
}
It "-1のときに例外をスローする" {
{Get-FizzBuzz -Value -1} | Should Throw
}
It "aのときに例外をスローする" {
{Get-FizzBuzz -Value -1} | Should Throw
}
}
Describe "Invoke-FizzBuzz" {
It "1から15のときに '1,2,Fizz,4,Buzz,6,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz' を返す" {
$results = Invoke-FizzBuzz -Start 1 -End 15
$expects = 1,2,'Fizz',4,'Buzz','Fizz',7,8,'Fizz','Buzz',11,'Fizz',13,14,'FizzBuzz'
$results.Count | Should Be $expects.Count
for ($i = 0; $i -lt $results.Count; $i++) {
$results[$i] | Should Be $expects[$i]
}
}
It "最後の値が最初の値より小さい場合は例外をスローする" {
{Invoke-FizzBuzz -Start 2 -End 1} | Should Throw
}
}
まとめ
ユニットテスト楽しいですね!! Pester の日本語情報が極めて少ないので少しでも参考になれば。