Pesterを使ったPowerShellモジュールのテスト駆動開発

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 の結果を出力します。

```PowerShell
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 の冒頭を以下の通り書き換えます。

```PowerShell: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
```

統合ターミナルを開いて、以下のコマンドを実行してテストが実行されることを確認します。（テストは失敗します。）

```PowerShell
Invoke-Pester #Red
```

これでモジュールとテストコードの準備ができました。

# Get-FizzBuzz の実装
それでは FizzBuzz モジュールに Get-FizzBuzz Function を記述していきましょう。FizzBuzz.psm1 を以下のように変更します。Visual Studio Code では cmdlet スニペットが使えます。

```PowerShell:FizzBuzz.psm1
function Get-FizzBuzz {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [int]$Value
    )
}
```

続いて FizzBuzz.Tests.ps1 を次のように書き換えましょう。

```PowerShell: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'
    }
}
```

統合ターミナルでテストを実行します。

```PowerShell
Invoke-Pester #Red
```

Get-FizzBuzz に処理を何も書いていないので、当然ながらテストは失敗します。
3 で割り切れる場合は 'Fizz' を返すようにコードを修正しましょう。

```PowerShell:FizzBuzz.psm1
function Get-FizzBuzz {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [int]$Value
    )
    if ($Value % 3 -eq 0) {
        return 'Fizz'
    }
}
```

再度テストを実行すると今度は成功します。

```PowerShell
Invoke-Pester #Green
```

どんどん行きましょう。次は 5 で割り切れる場合です。まずはテストから。

```PowerShell: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'
    }
    It "5のときに'Buzz'を返す" {
        Get-FizzBuzz -Value 5 | Should Be 'Buzz'
    }
}
```

```PowerShell
Invoke-Pester #Red
```

FizzBuzz.Tests.ps1 に処理を追加します。

```PowerShell:FizzBuzz.psm1
function Get-FizzBuzz {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [int]$Value
    )
    if ($Value % 3 -eq 0) {
        return 'Fizz'
    } elseif ($Value % 5 -eq 0) {
        return 'Buzz'
    }
}
```

```PowerShell
Invoke-Pester #Green
```

続いて 3 でも 5 でも割り切れる場合のテスト。

```PowerShell: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'
    }
    It "5のときに'Buzz'を返す" {
        Get-FizzBuzz -Value 5 | Should Be 'Buzz'
    }
    It "15のときに'FizzBuzz'を返す" {
        Get-FizzBuzz -Value 15 | Should Be 'FizzBuzz'
    }
}
```

```PowerShell
Invoke-Pester #Red
```

修正。

```PowerShell:FizzBuzz.psm1
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'
    }
}
```

```PowerShell
Invoke-Pester #Red!
```

はい、順番が違いますね。再度コードを修正します。

```PowerShell:FizzBuzz.psm1
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'
    }
}
```

```PowerShell
Invoke-Pester #Green
```

今度は OK です。最後に数字をそのまま返すパターンを書いていきましょう。

```PowerShell: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'
    }
    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
    }
}
```

```PowerShell
Invoke-Pester #Red
```

修正します。

```PowerShell:FizzBuzz.psm1
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
    }
}
```

```PowerShell
Invoke-Pester #Green
```

はい、いったんこれで完成です。

# Get-FizzBuzz の実装（異常系）

正常系は問題なくなったので異常系を考えていきましょう。パラメーターが以下の場合を異常系として例外がスローされるようにします。

- 1 より小さい場合（ゼロまたは負数）
- 数字以外の場合

なお、Pester で例外のスローをテストする場合はインプットオブジェクトを ScriptBlock {} にする必要があります。

```PowerShell: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'
    }
    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
    }
}
```

```PowerShell
Invoke-Pester #Red
```

$Value は 1 以上の数だけ受け入れるようにバリデーションします。上限は int 型の最大値にしています。

```PowerShell:FizzBuzz.psm1
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
    }
}
```

```PowerShell
Invoke-Pester #Green
```

次は数字以外の場合、、、ですがこれは何もしなくても型キャストに失敗して例外になるのでモジュールの修正は無しです。

```PowerShell: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'
    }
    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
    }
}
```

```PowerShell
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 を定義します。

```PowerShell:FizzBuzz.psm1
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" を記述していきます。

```PowerShell:FizzBuzz.Tests.ps1
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]
        }
    }
}
```

```PowerShell
Invoke-Pester #Red
```

Invoke-FizzBuzz を実装します。

```PowerShell:FizzBuzz.psm1
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
    }
}
```

```PowerShell
Invoke-Pester #Green
```

\$End が \$Start より小さい場合は例外をスローすることにします。

```PowerShell:FizzBuzz.Tests.ps1
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
    }
}
```

```PowerShell
Invoke-Pester #Red
```

モジュールを修正します。

```PowerShell:FizzBuzz.psm1
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
    }
}
```

```PowerShell
Invoke-Pester #Green
```

FizzBuzz モジュールの完成です。最終的なコードは以下の通り。

```PowerShell:FizzBuzz.psm1
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
    }
}
```

```PowerShell: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'
    }
    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 の日本語情報が極めて少ないので少しでも参考になれば。