LoginSignup
13
11

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-03-02

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

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 スニペットが使えます。

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

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

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' を返すようにコードを修正しましょう。

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

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

Invoke-Pester #Green

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

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'
    }
}
Invoke-Pester #Red

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

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'
    }
}
Invoke-Pester #Green

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

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'
    }
}
Invoke-Pester #Red

修正。

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'
    }
}
Invoke-Pester #Red!

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

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'
    }
}
Invoke-Pester #Green

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

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
    }
}
Invoke-Pester #Red

修正します。

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
    }
}
Invoke-Pester #Green

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

Get-FizzBuzz の実装(異常系)

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

  • 1 より小さい場合(ゼロまたは負数)
  • 数字以外の場合

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

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
    }
}
Invoke-Pester #Red

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

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
    }
}
Invoke-Pester #Green

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

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

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" を記述していきます。

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]
        }
    }
}
Invoke-Pester #Red

Invoke-FizzBuzz を実装します。

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
    }
}
Invoke-Pester #Green

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

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
    }
}
Invoke-Pester #Red

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

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
    }
}
Invoke-Pester #Green

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

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

13
11
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
11