dotnet testによるC#単体テストとTDD

# 概要
業務にて単体テストを利用した開発を始めたため、[公式ドキュメント](https://learn.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-with-dotnet-test)を見ながら練習します。

# 環境
.NET SDK:
 Version:           9.0.100
 Commit:            59db016f11
 Workload version:  9.0.100-manifests.4a280210
 MSBuild version:   17.12.7+5b8665660

ランタイム環境:
 OS Name:     Windows
 OS Version:  10.0.22631
 OS Platform: Windows
 RID:         win-x64
 Base Path:   C:\Program Files\dotnet\sdk\9.0.100\

# 単体テストをしながら開発してみる
## 本記事の目標成果物
2つの整数を足し合わせるソフトウェア

## ソリューションとプロジェクトのセットアップ
ソリューションを作成し、プロジェクトを追加していきます。
詳細は[公式ドキュメント](https://learn.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-with-dotnet-test)をご参照ください。
```コマンドライン
dotnet new sln -o calculator 
cd calculator
dotnet new classlib -o Calculator
dotnet sln add ./Calculator/Calculator.csproj
dotnet new xunit -o CalculatorTest 
dotnet sln add ./CalculatorTest/CalculatorTest.csproj
```
テスト用プロジェクトの参照にテスト対象プロジェクトを加えます。
```
dotnet add ./CalculatorTest/CalculatorTest.csproj reference ./Calculator/Calculator.csproj
```
ソースファイルは以下の通りとします。
- Calculatorフォルダ内にはAddService.cs
- CalculatorTestフォルダ内にはAddServiceTest.cs

## テストを書いてみる
TDDの作法に則り、まずはテストを書いてみます。
テストメソッドには[Fact]属性をつけます。
```AddServiceTest.cs
using Xunit;
using Calculator;

namespace CalculatorTest
{
    public class AddTest_AddTwoNumbersShould
    {
        [Fact]  // 単一のケースを実施する
        public void AddTwoNumbers_Input1And2_Return3()
        {
            var addService = new AddService();
            int actual = addService.AddTwoNumbers(1, 2);
            Assert.Equal(3, actual);
        }
    }
}
```
この段階ではAddServiceクラスにメソッドはありません。
```AddService.cs
namespace Calculator
{
    public class AddService
    {

    }
}
```

## テストを実行し、失敗する
CalculatorTestフォルダ直下にて下記のコマンドを実行し、テストを実行します。
```
dotnet test
```
AddTwoNumbersは未実装のため、当然テストは失敗します。
```
  CalculatorTest 1 件のエラーで失敗しました (0.8 秒)
    C:\Users\hsato\Desktop\dotnet-test\calculator\CalculatorTest\AddServiceTest.cs(12,37): error CS1061: 'AddService' に 'AddTwoNumbers' の定義が含まれておらず、型 'AddService' の最初の引数を受け付ける アクセス可能な拡張メソッド 'AddTwoNumbers' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください
```

## テストに通るように修正する
テストに通るよう、AddService.csに最低限の修正を加えます。
```AddService.cs
namespace Calculator
{
    public class AddService
    {
        public int AddTwoNumbers(int num1, int num2)
        {
            return 3;  //（実際にはこんなことしないと思いますが、説明の都合上）
        }
    }
}
```

## もう一度テストをし、成功する
CalculatorTestフォルダ直下にて下記のコマンドを実行し、テストを実行します。
```
dotnet test
```
AddTwoNumbersの戻り値は3であるため、テストに成功します。
```
  CalculatorTest テスト 成功しました (3.4 秒)

テスト概要: 合計: 1, 失敗数: 0, 成功数: 1, スキップ済み数: 0, 期間: 3.4 秒
6.4 秒後に 成功しました をビルド
```

## 更にテストを書き、実行し、失敗する
目標に向かって、更にテストを書きます。
ついでに、AddServiceの生成処理をヘルパーメソッドに集約します。
```AddServiceTest.cs
using Xunit;
using Calculator;

namespace CalculatorTest
{
    public class AddTest_AddTwoNumbersShould
    {

        // AddServiceクラスを生成するヘルパーメソッド
        private AddService CreateDefaultAddService()
        {
            return new AddService();
        }

        [Fact]  // 単一のケースを実施する
        public void AddTwoNumbers_Input1And2_Return3()
        {
            var addService = CreateDefaultAddService();
            int actual = addService.AddTwoNumbers(1, 2);
            Assert.Equal(3, actual);
        }

        [Theory]  // 複数のケースを実施する
        [InlineData(0, 1)]
        [InlineData(0, -1)]
        public void AddTwoNumbers_Input0AndOther_ReturnOther(int num1, int num2)
        {
            var addService = CreateDefaultAddService();
            int actual = addService.AddTwoNumbers(num1, num2);
            Assert.Equal(num2, actual);
        }
    }
}
```
AddTwoNumbersは常に3を返すため、テストは失敗するようになります。
```
  CalculatorTest テスト 2 件のエラーで失敗しました (1.8 秒)
    C:\Users\hsato\Desktop\dotnet-test\calculator\CalculatorTest\AddServiceTest.cs(30): error TESTERROR:
      CalculatorTest.AddTest_AddTwoNumbersShould.AddTwoNumbers_Input0_ReturnOther(num1: 0, num2: 1) (
      19ms): エラー メッセージ: Assert.Equal() Failure: Values differ
      Expected: 1
      Actual:   3
      スタック トレース:
         at CalculatorTest.AddTest_AddTwoNumbersShould.AddTwoNumbers_Input0_ReturnOther(Int32 num1, I
      nt32 num2) in C:\Users\hsato\Desktop\dotnet-test\calculator\CalculatorTest\AddServiceTest.cs:li
      ne 30
         at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, B
      oolean isConstructor)
         at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyO
      fArgs, BindingFlags invokeAttr)
    C:\Users\hsato\Desktop\dotnet-test\calculator\CalculatorTest\AddServiceTest.cs(30): error TESTERROR:
      CalculatorTest.AddTest_AddTwoNumbersShould.AddTwoNumbers_Input0_ReturnOther(num1: 0, num2: -1) 
      (1ms): エラー メッセージ: Assert.Equal() Failure: Values differ
      Expected: -1
      Actual:   3
      スタック トレース:
         at CalculatorTest.AddTest_AddTwoNumbersShould.AddTwoNumbers_Input0_ReturnOther(Int32 num1, I
      nt32 num2) in C:\Users\hsato\Desktop\dotnet-test\calculator\CalculatorTest\AddServiceTest.cs:li
      ne 30
         at InvokeStub_AddTest_AddTwoNumbersShould.AddTwoNumbers_Input0_ReturnOther(Object, Span`1)  
         at System.Reflection.MethodBaseInvoker.InvokeWithFewArgs(Object obj, BindingFlags invokeAttr
      , Binder binder, Object[] parameters, CultureInfo culture)

テスト概要: 合計: 3, 失敗数: 2, 成功数: 1, スキップ済み数: 0, 期間: 1.8 秒
4.1 秒後に 2 件のエラーで失敗しました をビルド
```

## テストに通るように更に修正する
AddService.csを修正します。
ベタ書きしていた3という数字を取り除き、num1とnum2を使ってちゃんと計算するようにします。
```AddService.cs
namespace Calculator
{
    public class AddService
    {
        public int AddTwoNumbers(int num1, int num2)
        {
            return num1 + num2;
        }
    }
}
```

## もう一度テストをし、テストに成功する
```
  CalculatorTest テスト 成功しました (1.9 秒)

テスト概要: 合計: 3, 失敗数: 0, 成功数: 3, スキップ済み数: 0, 期間: 1.9 秒
5.6 秒後に 成功しました をビルド
```
お疲れ様でした。

# まとめ
- テストメソッドには[Fact][Theory]属性をつける。Assertにて値を検証する。
- まずは失敗する自動テストを書き、dotnet testにより実行
- 自動テストに成功するよう、ソースコードを修正していく

# 参考文献
dotnet test: https://learn.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-with-dotnet-test
単体テストのベストプラクティス: https://learn.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-best-practices
Kent Beck (著)、和田 卓人 (翻訳)、「テスト駆動開発」第1章、オーム社、2017/10/14