【C#】RGB変換プログラムでテスト駆動開発

チェリー本:cherries:こと[プロを目指す人のためのRuby入門](https://www.amazon.co.jp/プロを目指す人のためのRuby入門-言語仕様からテスト駆動開発・デバッグ技法まで-Software-Design-plusシリーズ/dp/4774193976)に『RGB変換プログラム』という例題があります。
そちらで自分なりに **C# + VSCode + dotnet CLI** でテスト駆動開発したのでまとめました。。

[プロを目指す人のためのRuby入門](https://www.amazon.co.jp/プロを目指す人のためのRuby入門-言語仕様からテスト駆動開発・デバッグ技法まで-Software-Design-plusシリーズ/dp/4774193976)は Ruby プログラマーでなくても勉強になる凄い本だと思います。。

## **環境構築**

ライブラリとテストのプロジェクトを構築します。

```powershell
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`ディレクトリを開く。

:warning:`Required assets to build and debug are missing from 'tdd-csharp'. Add them?`

と聞かれるので **Yes** を選択。



正しく作成できたか`.csproj`を確認

```xml:tdd-csharp.Lib.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <RootNamespace>tdd_csharp.Lib</RootNamespace>
  </PropertyGroup>
</Project>
```

```xml:tdd-csharp.Test.csproj
<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`を作成する。
メソッドは未実装例外をスローする仮実装。

```c#:tdd_csharp.Lib/ColorConverter.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`を削除する。
先に失敗するテストを書く。

```c#:tdd_csharp.Test/ColorConverterTest.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);

        }
    }
}
```

テストを実行する。
未実装例外で失敗するが、プロジェクト参照ができてビルドが通ることは確認できる。

```ps1
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 秒
```

期待値をそのまま返してテストが通るコードにする。

```c#
namespace tdd_csharp.Lib {
    public class ColorConverter {
        public string ToHex(int red, int green, int blue) => "#000000";  // Fake It
    }
}
```

今度は成功する。

```ps1
PS RgbConverter> dotnet test ./tdd-csharp.Test

# 中略

テスト実行を開始しています。お待ちください...

テストの合計数: 1。成功: 1。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.1687 秒
```

検証コードを追加する。
複数ケースでテストするから **XUnit** のパラメータ付きテスト形式にする。

```c#
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);
        }
    }
}
```

テストを実行する。

```ps1
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`にするには以下の方法がある。

```c#
> 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](https://try.dot.net/)が便利。
※[Try.Net](https://try.dot.net/)はブラウザで実行できる C# のコードランナー。

数値を0埋めの`string`にする場合は書式設定でできる。

```c#
> $"{3:d2}"
"03"

> $"{3:00}"
"03"

> 3.ToString("00")
"03"
```

ただ、今回は対象が16進数`string`なので数値->文字列の書式設定ができない。

なので2文字になるまで左側に'0'埋めは`string.PadLeft`でやる。

```c#
> "3".PadLeft(2, '0')
"03"

> "12".PadLeft(2, '0')
"12"
```

`ColorConverter.ToHex()`メソッドはこうなる。

```c#:
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');
    }
}
```

テストを通す。

```powershell
PS > dotnet test ./tdd-cshapr.Test
# 中略
テストの合計数: 2。成功: 2。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.1976 秒
```

さらにテストケースを追加する。

```diff
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つ目のテストケースも通る。

```powershell
PS > dotnet test ./tdd-cshapr.Test
# 中略
テストの合計数: 3。成功: 3。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.1809 秒
```

## **`ToHex`メソッドをリファクタリングする**

`.PadLeft(2, '0')`が3回登場している。
これでは何か変更があったときに3か所も直す必要がある。
配列に入れて繰り返し処理にする。

```c#
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;
        }
    }
}
```

メソッドが壊れていないかテストする

```powershell
PS > dotnet test ./tdd-cshapr.Test
# 中略
テストの合計数: 3。成功: 3。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.0885 秒
```

`foreach`が使えるということは **LINQ** が使える。
`Enumerable.Aggregate`を使うともっと短くできる。
整数の配列`new int[]`も匿名型の配列`new[]`にできる。型は推論してくれる。

```c#
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'));
    }
}
```

デグレしていないか確認する。

```powershell
PS > dotnet test ./tdd-cshapr.Test
# 中略
テストの合計数: 3。成功: 3。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.4149 秒
```

これでリファクタリングは終了。

## **カラーコード（16進数） -> Rgb のメソッドを作成**

まず失敗するテストコードから。

ColorConverter.cs を開き未実装例外をスローする`toRgb`を定義。

```c#
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を開きテストを追加する。

```c#
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>`追加する。

```diff:tdd-csharp.Lib.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <RootNamespace>tdd_csharp.Lib</RootNamespace>
+   <LangVersion>7.3</LangVersion>
  </PropertyGroup>
</Project>
```

```diff:tdd-csharp.Test.csproj
<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** の設定を開く。
以下の設定を追加。

```json
  "omnisharp.path": "latest",
```

ビルドを通す。

```powershell
PS > dotnet build
```

**ominisharp** が最新バージョンになっていない場合、ダウンロードとインストールされる。
うまくいかない場合、一度 Visual Stuidio Code を閉じてからもう一度行うとうまくいく。

ビルドが通ったので、テストする。

```powershell
PS > dotnet test ./tdd-csharp.Test
# 略
エラー メッセージ:
 System.NotImplementedException : ToRgb
# 略
テストの合計数: 4。成功: 3。失敗:1。スキップ: 0。
テストの実行に失敗しました。
テスト実行時間: 2.4092 秒
```

未実装例外で失敗する。
テストをパスする仮実装をする。

```c#
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);
    }
}
```

仮実装がテストをパスすることを確認。

```powershell
PS > dotnet test ./tdd-csharp.Test
# 略
テストの合計数: 4。成功: 4。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.3565 秒
```

パラメータ付きテストに書き換えて、2つ目のテストケースを追加する。

```c#
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`にはダメだった。

```c#
// こういうのは可能
(int x, string y) GetValueTuple((int i, string s) vt) => vt;
var (i, s) = GetValueTuple((1, "A"));
Console.WriteLine($"{i} {s}"); //=> "1 A"
```

テストを実行する。

```powershell
PS > dotnet test ./tdd-csharp.Test

テストの合計数: 5。成功: 4。失敗:1。スキップ: 0。
テストの実行に失敗しました。
テスト実行時間: 1.7179 秒
```

固定値を返しているためテストが失敗する。
※[タプル同士の比較はメンバーごとの `==` を `&&` で繋いだものに展開される](https://ufcpp.net/study/csharp/datatype/tuples/#equality)。

`ToRgb`の実装に入る。

16進数`string`を数値に変換する方法

```c#
> 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`の実装は以下のようになる。

```c#
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]);
        }
    }
}
```

テストを実行する。

```powershell
PS > dotnet test ./tdd-csharp.Test
# 略
テストの合計数: 5。成功: 5。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.6214 秒
```

ちゃんとパスすることを確認。
さらにテストケースを追加する。

```diff
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つ目のテストケースも無事パス。

```powershell
PS > dotnet test ./tdd-csharp.Test
# 略
テストの合計数: 6。成功: 6。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 1.7007 秒
```

## **`ToRgb`をリファクタリングする**

カンマで区切ることで複数の変数を同時に宣言できる。

```c#
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`は単語に使用される正規表現。アルファベット、数字、アンダーバー(_)、ひらがな、カタカナ、漢字などにマッチする。

```c#
> Regex.Match("#%r&", @"\w").Value
"r"
```

2文字ずつ取得するので以下のようになる。



`Regex.Matches`はマッチした文字列を`MatchCollection`で返す。
`MatchCollection`は`IEnumerable<T>`を実装していないので`.Cast<Match>()`している。


```c#
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]);
        }
    }
}
```

テストをパスすることを確認しリファクタリングを終了。

```powershell
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 の設計や全体の設計を向上させるためのリファクタリングを行う

**最終的なソースコード**

```c#:tdd-csharp/tdd-csharp.Lib/ColorConverter.cs
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]);
        }
    }
}
```

```c#:tdd-csharp/tdd-csharp.Test/ColorConverterTest.cs
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);
        }
    }
}
```

## **参考**

- **[プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで](https://www.amazon.co.jp/プロを目指す人のためのRuby入門-言語仕様からテスト駆動開発・デバッグ技法まで-Software-Design-plusシリーズ/dp/4774193976)**
- **[Adaptive Code ~ C#実践開発手法 第2版](https://www.amazon.co.jp/Adaptive-Code-実践開発手法-第2版-マイクロソフト関連書/dp/4822253546)**
- **[.NET Core コマンドライン インターフェイス (CLI) ツール](https://docs.microsoft.com/ja-jp/dotnet/core/tools/?tabs=netcore2x)**
- **[dotnet テストと xUnit を使用した .NET Core での単体テスト C#](https://docs.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-with-dotnet-test)**
- **[xUnit.net でユニットテストを始める](https://qiita.com/takutoy/items/84fa6498f0726418825d)**
- **[C# 7.3 is not supported yet #2346](https://github.com/OmniSharp/omnisharp-vscode/issues/2346)**
- **[C# によるプログラミング入門  [データ型] タプル](https://ufcpp.net/study/csharp/datatype/tuples/#equality)**