チェリー本ことプロを目指す人のためのRuby入門に『RGB変換プログラム』という例題があります。
そちらで自分なりに C# + VSCode + dotnet CLI でテスト駆動開発したのでまとめました。。
プロを目指す人のためのRuby入門は Ruby プログラマーでなくても勉強になる凄い本だと思います。。
環境構築
ライブラリとテストのプロジェクトを構築します。
PS > dotnet --version
dotnet 2.1.300
PS > mkdir tdd-csharp
PS > cd tdd-csharp # root
PS > dotnet new sln
PS > dotnet new classlib -o ./tdd-csharp.Lib
PS > dotnet new xunit -o ./tdd-csharp.Test
PS > dotnet sln add ./tdd-csharp.Lib ./tdd-csharp.Test
PS > dotnet add ./tdd-csharp.Test reference ./tdd-csharp.Lib
visual studio code でtdd-csharp
ディレクトリを開く。
Required assets to build and debug are missing from 'tdd-csharp'. Add them?
と聞かれるので Yes を選択。
正しく作成できたか.csproj
を確認
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>tdd_csharp.Lib</RootNamespace>
</PropertyGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RootNamespace>tdd_csharp.Test</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\tdd-csharp.Lib\tdd-csharp.Lib.csproj" />
</ItemGroup>
</Project>
Rgb -> カラーコード(16進数)のメソッドを作成
tdd-csharp.Lib/class1.cs
を削除して、ColorConveter.cs
を作成する。
メソッドは未実装例外をスローする仮実装。
using System;
namespace tdd_csharp.Lib {
public class ColorConverter {
public string ToHex(int red, int green, int blue) =>
throw new NotImplementedException(nameof(ToHex));
}
}
tdd-csharp.Test/UnitTest1.cs
を削除する。
先に失敗するテストを書く。
using tdd_csharp.Lib;
using Xunit;
namespace tdd_csharp.Test {
public class ColorConverterTest {
[Fact]
public void Test_ToHex() {
// Arrange
var sut = new ColorConverter(); // sut means `System under test`
// Act
var colorcode = sut.ToHex(red: 0, green: 0, blue: 0);
// Assert
Assert.Equal("#000000", colorcode);
}
}
}
テストを実行する。
未実装例外で失敗するが、プロジェクト参照ができてビルドが通ることは確認できる。
PS > dotnet test ./tdd-csharp.Test
ビルドが開始されました。しばらくお待ちください...
ビルドが完了しました。
tdd-csharp\tdd-csharp.Test\bin\Debug\netcoreapp2.1\tdd-csharp.Test.dll(.NETCoreApp,Version=v2.1) のテスト実行
Microsoft (R) Test Execution Command Line Tool Version 15.7.0
Copyright (c) Microsoft Corporation. All rights reserved.
テスト実行を開始しています。お待ちください...
[xUnit.net 00:00:00.5976127] tdd_csharp.Test.ColorConverterTest.Test_ToHex [FAIL]
失敗 tdd_csharp.Test.ColorConverterTest.Test_ToHex
エラー メッセージ:
System.NotImplementedException : ToHex
スタック トレース:
at tdd_csharp.Lib.ColorConverter.ToHex(Int32 red, Int32 green, Int32 blue) in tdd-csharp\tdd-csharp.Lib\ColorConverter.cs:line 6
at tdd_csharp.Test.ColorConverterTest.Test_ToHex() in tdd-csharp\tdd-csharp.Test\ColorConverterTest.cs:line 11
テストの合計数: 1。成功: 0。失敗:1。スキップ: 0。
テストの実行に失敗しました。
テスト実行時間: 1.2166 秒
期待値をそのまま返してテストが通るコードにする。
namespace tdd_csharp.Lib {
public class ColorConverter {
public string ToHex(int red, int green, int blue) => "#000000"; // Fake It
}
}
今度は成功する。
PS RgbConverter> dotnet test ./tdd-csharp.Test
# 中略
テスト実行を開始しています。お待ちください...
テストの合計数: 1。成功: 1。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.1687 秒
検証コードを追加する。
複数ケースでテストするから XUnit のパラメータ付きテスト形式にする。
using tdd_csharp.Lib;
using Xunit;
namespace tdd_csharp.Test {
public class ColorConverterTest {
[Theory]
[InlineData("#000000", 0, 0, 0)]
[InlineData("#ffffff", 255, 255, 255)]
public void Test_ToHex(string expected, int red, int green, int blue) {
// Arrange
var sut = new ColorConverter(); // sut means `System under test`
// Act
var colorcode = sut.ToHex(red, green, blue);
// Assert
Assert.Equal(expected, colorcode);
}
}
}
テストを実行する。
PS > dotnet test ./tdd-csharp.Test
# 中略
[xUnit.net 00:00:00.5045876] tdd_csharp.Test.ColorConverterTest.Test_ToHex(expected: "#ffffff", red: 255, green: 255, blue: 255) [FAIL]
失敗 tdd_csharp.Test.ColorConverterTest.Test_ToHex(expected: "#ffffff", red: 255, green: 255, blue: 255)
エラー メッセージ:
Assert.Equal() Failure
↓ (pos 1)
Expected: #ffffff
Actual: #000000
↑ (pos 1)
スタック トレース:
at tdd_csharp.Test.ColorConverterTest.Test_ToHex(String expected, Int32 red, Int32 green, Int32 blue) in tdd-csharp\tdd-csharp.Test\ColorConverterTest.cs:line 15
テストの合計数: 2。成功: 1。失敗:1。スキップ: 0。
テストの実行に失敗しました。
テスト実行時間: 1.0961 秒
固定値を返しているから失敗する。
ColorConverter.ToHex
をちゃんと実装する。
int
を16進数string
にするには以下の方法がある。
> int n = 123;
> Convert.ToString(n, 16)
"7b"
> String.Format("{0:X}", n)
"7B"
> String.Format("{0:x}", n)
"7b"
> $"{n:X}"
"7B"
> $"{n:x}"
"7b"
短い$""
文字列補間を使うことにする。
※簡単な C# コードを検証するときはcsi.exe
とかTry.Netが便利。
※Try.Netはブラウザで実行できる C# のコードランナー。
数値を0埋めのstring
にする場合は書式設定でできる。
> $"{3:d2}"
"03"
> $"{3:00}"
"03"
> 3.ToString("00")
"03"
ただ、今回は対象が16進数string
なので数値->文字列の書式設定ができない。
なので2文字になるまで左側に'0'埋めはstring.PadLeft
でやる。
> "3".PadLeft(2, '0')
"03"
> "12".PadLeft(2, '0')
"12"
ColorConverter.ToHex()
メソッドはこうなる。
namespace tdd_csharp.Lib {
public class ColorConverter {
public string ToHex(int red, int green, int blue) =>
"#" +
red.ToHex().PadLeft(2, '0') +
green.ToHex().PadLeft(2, '0') +
blue.ToHex().PadLeft(2, '0');
}
}
テストを通す。
PS > dotnet test ./tdd-cshapr.Test
# 中略
テストの合計数: 2。成功: 2。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.1976 秒
さらにテストケースを追加する。
using tdd_csharp.Lib;
using Xunit;
namespace tdd_csharp.Test {
public class ColorConverterTest {
[Theory]
[InlineData("#000000", 0, 0, 0)]
[InlineData("#ffffff", 255, 255, 255)]
+ [InlineData("#043c78", 4, 60, 120)]
public void Test_ToHex(string expected, int red, int green, int blue) {
3つ目のテストケースも通る。
PS > dotnet test ./tdd-cshapr.Test
# 中略
テストの合計数: 3。成功: 3。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.1809 秒
ToHex
メソッドをリファクタリングする
.PadLeft(2, '0')
が3回登場している。
これでは何か変更があったときに3か所も直す必要がある。
配列に入れて繰り返し処理にする。
namespace tdd_csharp.Lib {
public class ColorConverter {
public string ToHex(int red, int green, int blue) {
string hex = "#";
foreach (int n in new int[] { red, green, blue }) {
hex += $"{n:x}".PadLeft(2, '0');
}
return hex;
}
}
}
メソッドが壊れていないかテストする
PS > dotnet test ./tdd-cshapr.Test
# 中略
テストの合計数: 3。成功: 3。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.0885 秒
foreach
が使えるということは LINQ が使える。
Enumerable.Aggregate
を使うともっと短くできる。
整数の配列new int[]
も匿名型の配列new[]
にできる。型は推論してくれる。
using System.Linq;
namespace tdd_csharp.Lib {
public class ColorConverter {
public string ToHex(int red, int green, int blue) =>
new [] { red, green, blue }.Aggregate("#", (hex, n) =>
hex + $"{n:x}".PadLeft(2, '0'));
}
}
デグレしていないか確認する。
PS > dotnet test ./tdd-cshapr.Test
# 中略
テストの合計数: 3。成功: 3。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.4149 秒
これでリファクタリングは終了。
カラーコード(16進数) -> Rgb のメソッドを作成
まず失敗するテストコードから。
ColorConverter.cs を開き未実装例外をスローするtoRgb
を定義。
using System;
using System.Linq;
namespace tdd_csharp.Lib {
public class ColorConverter {
public string ToHex(int red, int green, int blue) =>
new [] { red, green, blue }.Aggregate("#", (hex, n) =>
hex + $"{n:x}".PadLeft(2, '0'));
// ↓追加
public(int r, int g, int b) ToRgb(String hex) =>
throw new NotImplementedException(nameof(ToRgb));
}
}
ColorConverterTest.csを開きテストを追加する。
using tdd_csharp.Lib;
using Xunit;
namespace tdd_csharp.Test {
public class ColorConverterTest {
// 略
[Fact]
public void Test_ToRgb() {
// Arenge
var sut = new ColorConverter();
// Act
var rgb = sut.ToRgb("#000000");
// Assert
Assert.True((0, 0, 0) == rgb);
}
}
}
タプル同士を ==
、!=
演算子で比較できるようになったのは C# 7.3 からなので設定する。
*.csproj
に<LangVersion>7.3</LangVersion>
もしくはを<LangVersion>latest</LangVersion>
追加する。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>tdd_csharp.Lib</RootNamespace>
+ <LangVersion>7.3</LangVersion>
</PropertyGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RootNamespace>tdd_csharp.Test</RootNamespace>
<IsPackable>false</IsPackable>
+ <LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\tdd-csharp.Lib\tdd-csharp.Lib.csproj" />
</ItemGroup>
</Project>
Ctrl + ,
で Visual Studio Code の設定を開く。
以下の設定を追加。
"omnisharp.path": "latest",
ビルドを通す。
PS > dotnet build
ominisharp が最新バージョンになっていない場合、ダウンロードとインストールされる。
うまくいかない場合、一度 Visual Stuidio Code を閉じてからもう一度行うとうまくいく。
ビルドが通ったので、テストする。
PS > dotnet test ./tdd-csharp.Test
# 略
エラー メッセージ:
System.NotImplementedException : ToRgb
# 略
テストの合計数: 4。成功: 3。失敗:1。スキップ: 0。
テストの実行に失敗しました。
テスト実行時間: 2.4092 秒
未実装例外で失敗する。
テストをパスする仮実装をする。
using System;
using System.Linq;
namespace tdd_csharp.Lib {
public class ColorConverter {
// 略
public (int r, int g, int b) ToRgb(String hex) => (0, 0, 0);
}
}
仮実装がテストをパスすることを確認。
PS > dotnet test ./tdd-csharp.Test
# 略
テストの合計数: 4。成功: 4。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.3565 秒
パラメータ付きテストに書き換えて、2つ目のテストケースを追加する。
using tdd_csharp.Lib;
using Xunit;
namespace tdd_csharp.Test {
public class ColorConverterTest {
// 略
[Theory]
[InlineData(0, 0, 0, "#000000")]
[InlineData(255, 255, 255, "#ffffff")]
public void Test_ToRgb(int r, int g, int b, string hex) {
// Arenge
var sut = new ColorConverter();
var expected = (r, g, b);
// Act
var rgb = sut.ToRgb(hex);
// Assert
Assert.True(expected == rgb);
}
}
}
InlineData
属性引数に直接タプルを渡したかったけど、
An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type [tdd-csharp.Test]
と怒られるので上記のようになった。
メソッドの引数に直接タプル書くのはできるけどInlineData
にはダメだった。
// こういうのは可能
(int x, string y) GetValueTuple((int i, string s) vt) => vt;
var (i, s) = GetValueTuple((1, "A"));
Console.WriteLine($"{i} {s}"); //=> "1 A"
テストを実行する。
PS > dotnet test ./tdd-csharp.Test
テストの合計数: 5。成功: 4。失敗:1。スキップ: 0。
テストの実行に失敗しました。
テスト実行時間: 1.7179 秒
固定値を返しているためテストが失敗する。
※タプル同士の比較はメンバーごとの ==
を &&
で繋いだものに展開される。
ToRgb
の実装に入る。
16進数string
を数値に変換する方法
> string hex = "8e";
> Convert.ToInt32(hex, 16)
142
> int.Parse(hex, System.Globalization.NumberStyles.HexNumber)
142
/* TryParse で変換できるか確かめることも可能 */
> bool can = int.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out int n);
> can
True
> n
142
簡単なConvert.ToInt32
を使う。
ToRgb
の実装は以下のようになる。
using System;
using System.Linq;
namespace tdd_csharp.Lib {
public class ColorConverter {
// 略
public(int r, int g, int b) ToRgb(String hex) {
string r = hex.Substring(1, 2);
string g = hex.Substring(3, 2);
string b = hex.Substring(5, 2);
var rgb = new[] { r, g, b }
.Select(s => Convert.ToInt32(s, 16))
.ToList();
return (rgb[0], rgb[1], rgb[2]);
}
}
}
テストを実行する。
PS > dotnet test ./tdd-csharp.Test
# 略
テストの合計数: 5。成功: 5。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.6214 秒
ちゃんとパスすることを確認。
さらにテストケースを追加する。
using tdd_csharp.Lib;
using Xunit;
namespace tdd_csharp.Test {
public class ColorConverterTest {
// 略
[Theory]
[InlineData(0, 0, 0, "#000000")]
[InlineData(255, 255, 255, "#ffffff")]
+ [InlineData(4, 60, 120, "#043c78")]
public void Test_ToRgb(int r, int g, int b, string hex) {
3つ目のテストケースも無事パス。
PS > dotnet test ./tdd-csharp.Test
# 略
テストの合計数: 6。成功: 6。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.7007 秒
ToRgb
をリファクタリングする
カンマで区切ることで複数の変数を同時に宣言できる。
using System;
using System.Linq;
namespace tdd_csharp.Lib {
public class ColorConverter {
// 略
public (int r, int g, int b) ToRgb(String hex) {
string
r = hex.Substring(1, 2),
g = hex.Substring(3, 2),
b = hex.Substring(5, 2);
var rgb = new [] { r, g, b }
.Select(s => Convert.ToInt32(s, 16))
.ToList();
return (rgb[0], rgb[1], rgb[2]);
}
}
}
正規表現を使えばSubstring
を3回書かなくても1度で取得できる。
\w
は単語に使用される正規表現。アルファベット、数字、アンダーバー(_)、ひらがな、カタカナ、漢字などにマッチする。
> Regex.Match("#%r&", @"\w").Value
"r"
2文字ずつ取得するので以下のようになる。
Regex.Matches
はマッチした文字列をMatchCollection
で返す。
MatchCollection
はIEnumerable<T>
を実装していないので.Cast<Match>()
している。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace tdd_csharp.Lib {
public class ColorConverter {
// 略
public(int r, int g, int b) ToRgb(String hex) {
var rgb = Regex.Matches(hex, @"\w\w").Cast<Match>()
.Select(m => Convert.ToInt32(m.Value, 16))
.ToList();
return (rgb[0], rgb[1], rgb[2]);
}
}
}
テストをパスすることを確認しリファクタリングを終了。
PS > dotnet test ./tdd-csharp.Test
# 略
テストの合計数: 6。成功: 6。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.4715 秒
まとめ
ユニットテストの AAA
- Arrange … テストの事前条件のセットアップ
- Act … テスト対象となるアクションの実行
- Assert … 振る舞いが期待通りであることの検証
XUnit のテストメソッドの属性
-
[Fact]
… テスト ランナーによって実行されるテストメソッド -
[Theory]
… 同じコードを実行するものの、異なる入力引数が含まれる一連のテスト -
[InlineData]
…[Theory]
の入力の値を指定
その他テスト用語
- Fake it … 仮実装
- SUT(System Under Test) … テスト対象のクラス
テスト駆動開発のサイクル
Red -> Green -> Refactor
- Red … SUT の期待される振る舞いターゲットとした失敗するテストを書く
- Green … テストを成功させる最小限の実装を SUT に追加する
- Refactor … SUT の設計や全体の設計を向上させるためのリファクタリングを行う
最終的なソースコード
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace tdd_csharp.Lib {
public class ColorConverter {
public string ToHex(int red, int green, int blue) =>
new [] { red, green, blue }.Aggregate("#", (hex, n) =>
hex + $"{n:x}".PadLeft(2, '0'));
public(int r, int g, int b) ToRgb(String hex) {
var rgb = Regex.Matches(hex, @"\w\w").Cast<Match>()
.Select(m => Convert.ToInt32(m.Value, 16))
.ToList();
return (rgb[0], rgb[1], rgb[2]);
}
}
}
using tdd_csharp.Lib;
using Xunit;
namespace tdd_csharp.Test {
public class ColorConverterTest {
[Theory]
[InlineData("#000000", 0, 0, 0)]
[InlineData("#ffffff", 255, 255, 255)]
[InlineData("#043c78", 4, 60, 120)]
public void Test_ToHex(string expected, int red, int green, int blue) {
// Arrange
var sut = new ColorConverter(); // sut means `System under test`
// Act
var actual = sut.ToHex(red, green, blue);
// Assert
Assert.Equal(expected, actual);
}
[Theory]
[InlineData(0, 0, 0, "#000000")]
[InlineData(255, 255, 255, "#ffffff")]
[InlineData(4, 60, 120, "#043c78")]
public void Test_ToRgb(int r, int g, int b, string hex) {
// Arenge
var expected = (r, g, b);
var sut = new ColorConverter();
// Act
var actual = sut.ToRgb(hex);
// Assert
Assert.True(expected == actual);
}
}
}