はじめに
テスト駆動開発(TDD)というものを知ったので、FizzBuzz問題を元にC#でxUnit使ってコーディングしました。その手順を残しておきます。
実行環境はWindowsでVisualStudio Codeと内部実行のPowershellで行ったため、VisualStudioをインストールする必要はありません。
テストの準備
まずは適当なディレクトリに移動して、ソリューションファイルを準備します 。
> dotnet new sln
> mkdir FizzBuzz
> cd .\FizzBuzz\
FizzBuzz> dotnet new console
テンプレート "Console Application" が正常に作成されました。
FizzBuzz> cd ../
dotnet sln add .\FizzBuzz\FizzBuzz.csproj
> mkdir FizzBuzz.Tests
> cd .\FizzBuzz.Tests\
FizzBuzz\FizzBuzz.Tests> dotnet new xunit
テンプレート "xUnit Test Project" が正常に作成されました。
FizzBuzz.Tests> dotnet add reference
FizzBuzz.Tests> cd ..
> dotnet sln add
作成したクラスファイルに対応する形でテストクラスの名前を付けます。
今回は、Class1.csをFizzBuzz.csに、UnitTest1.csをFizzBuzz.Test.csにリネームします。
TODOリストの作成
テストを書き始める前に、TDDではまずTODOリストを作成し、テストを書くべき項目を確認しながら進めていきます。テストやその実装を書いている途中で必要なものができた場合は随時変更していきます。
まずはFizzBuzz問題のルールに従って列挙します。
ルールに従い、1から100までの数字が与えられ、それ以外が与えられる場合は考慮しないものとします。
- 与えられた数字が表示されること
- 3の倍数の場合、Fizzと表示されること
- 5の倍数の場合、Buzzと表示されること
- 3の倍数かつ5の倍数の場合、FizzBuzzと表示されること
テストコードの記述
TODOリストで列挙した順にテストと実装をしていきます。
与えられた数字が表示されること
まずは、「1を与えると、1が返ってくる」ことをテストするコードです。 「1を与えると、1が返ってくる」Outputメソッドを実装すると想定して書いています。
using Xunit;
using FizzBuzz;
namespace FIzzBuzz.Test
{
public class FizzBuzzProblemTest
{
[Fact]
public static void UnitTest()
{
var _FIzzBuzz = new FizzBuzzProblem();
var expected = 1;
var actual = _FIzzBuzz.Output(1);
Assert.Equal(expected, actual);
}
}
}
テストは3つのフェーズで成り立っています。
- arrange: 準備
- act: 実行
- assert: 検証
インスタンスと期待値を準備し、実行して得られた値をactualに代入しています。 最後にassertで期待値と実効値を検証しています。
C#の場合、Assert.Equal( 期待値, 実効値 ) の順で書きます。
言語によっては順序が逆なので(Node.jsの場合、実効値, 期待値の順)注意が必要です。
また、UnitTestメソッドの属性である[Fact]
は、xUnit.netによって定義されているもので、テストメソッドが引数を取らない場合に使用するものです。
早速テストを実行してみます。
FizzBuzz.Test> dotnet test
テストの実行に失敗しました。
Total tests: 1
Failed: 1
この段階では、当然テストは失敗します。
次は、テストで呼び出しているOutputメソッドを実装します。
なお、今回テスト項目にない1から100まで数字を与え表示する部分も実装しています。
using System;
namespace FizzBuzz
{
public class FizzBuzzProblem
{
public static int Output(int num)
{
var result = num;
return result;
}
public static void Main()
{
for (int i = 1; i <= 100; i++)
{
var result = Output(i);
Console.WriteLine(result);
}
}
}
}
早速実行してみましょう。
テストの.csprojファイルがあるディレクトリに移動して下記コマンドを実行します。
FizzBuzz.Test> dotnet test
テストの実行に成功しました。
Total tests: 1
Passed: 1
無事テストが通りました。
[Theory]属性を使用して[InlineData]でパラメータを利用したテストを行う
念のため、別のパターンもテストしてみます。
別のパターンもテストするには、UnitTestメンバ内の変数expected
の値を変更することで可能です。しかし、値が1と2の両方をテストするには似たようなコードを複数書く必要があります。
そこで今回は、[Fact]
属性を[Theory]
属性に変更して、テストメソッドに引数として入力値と期待値を与えるように書き換えます。
[Theory]
属性はxUnit.netによって定義されているもので、テストメソッドが一つ以上の引数を取る場合に使用するものです。
書き換え後のテストです。InlineDataでUnitTestメソッドの引数である変数num
とexpected
の値をそれぞれ指定しています。
[Theory]
[InlineData(1, 1)]
[InlineData(2, 2)]
public void UnitTest(int num, int expected)
{
var _FIzzBuzz = new FizzBuzzProblem();
var actual = _FIzzBuzz.Output(num);
Assert.Equal(expected, actual);
}
同じくdotnet test
を実行してテストが通ることを確認します。
3の倍数の場合、Fizzが表示されること
次は 「3の倍数の場合、Fizzが表示されること」 を実装します。
テストケースの一つは [InlineData(3, "Fizz")]
となるため、Outputメソッドの返り値をint型からstring型に変更する必要があります。そこで、UnintTestメソッドの引数expected
の型を変更します。
型の変更
テストです。
[Theory]
[InlineData(1, "1")]
[InlineData(2, "2")]
public void UnitTest(int num, string expected)
コードです。
public string Output(int num)
{
var result = num.ToString();
return result;
}
テストして確認します。
FizzBuzz.Test> dotnet test
テストの実行に成功しました。
Total tests: 2
Passed: 2
では本題に戻って「3の倍数の場合、Fizzが表示されること」を実装します。
テストケースとして適当に3と6とを設定しています。
[Theory]
[InlineData(1, "1")]
[InlineData(2, "2")]
[InlineData(3, "Fizz")]
[InlineData(6, "Fizz")]
コードです。
public static string Output(int num)
{
var result = num.ToString();
if (num % 3 == 0)
{
result = "Fizz";
}
return result;
}
テストして確認します。
5の倍数の場合、Buzzが表示されること
テストケースに5と10, そして問題で与えられる境界値である100を追加してテストします。
[InlineData(5, "Buzz")]
[InlineData(10, "Buzz")]
[InlineData(100, "Buzz")]
コードは3の倍数の時とほぼ同じなので省略します。
3の倍数かつ5の倍数の場合、FizzBuzzが表示されること
テストケースに15と30を追加します。
[InlineData(15, "FizzBuzz")]
[InlineData(30, "FizzBuzz")]
コードです。
public static string Output(int num)
{
var result = num.ToString();
if (num % 3 == 0)
{
result = "Fizz";
}
if (num % 5 == 0)
{
result = "Buzz";
}
if (num % 3 == 0 && num % 5 == 0)
{
result = "FizzBuzz";
}
return result;
}
テストして確認します。
リファクタリング
これまでのコードを書き換えてみます。
public static string Output(int num)
{
var result = "";
if (num % 3 == 0)
{
result = "Fizz";
}
if (num % 5 == 0)
{
result += "Buzz";
}
return result == "" ? num.ToString() : result;
}
書き換え後にテストを実行することで、入出力に変更がないことを確認できます。
詰まったところ
dotnet runで実行できない場合に下記のエラーが出ることがあります。
FIzzBuzz> dotnet run
プロジェクトを実行できません。
プロジェクトの種類が実行可能であること、このプロジェクトが 'dotnet run' でサポートされていることを確認してください。
実行可能なプロジェクトは実行可能な TFM (たとえば、netcoreapp2.0) を対象としている必要があり、OutputType 'Exe' が必要です。
現在の OutputType は 'Library' です。
dotnetプロジェクトの作成ミスです。classlibraryで作成していたのが原因でした。
この場合、FizzBuzzディレクトリにあるFizzBuzz.csproj
を下記に書き換えると実行できました。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
</Project>
最後に
リファクタリングしたときに抜けがあってもテストのエラーで弾いてくれるのがいいですね。公開する前に確認できる安心感があります。