xUnit の使い方 テストケースの準備&データの渡し方

## 1. テストケースの準備

テストケースの準備方法について

共通のテストのセットアップを適切に使うことで得られるメリット

- ボイラープレートコードの削減
- 重たい処理など生成コスト、時間の削減

## コンストラクタ
Unit におけるコンストラクタによるセットアップは、テストクラスのインスタンスが生成される際に、テストに必要なオブジェクトを初期化するための最も一般的な方法

- シンプルで直感的
- テストケースごとにオブジェクトを初期化する
- テストケースごとに異なるセットアップが必要な場合、複雑になる

```mermaid
flowchart TB
    A(テストクラス:コンストラクタ) --> B(テストメソッドX)
    B --> C(テストクラス:Dispose)
    C --> A
    A --> D(テストメソッドY)
    D --> E(テストクラス:Dispose)
```

```cs
/// <summary>
/// サンプルテストクラス
/// </summary>
/// <remarks>
/// 1. テストクラスのインスタンスは、テストメソッドごとに生成される
/// 2. テストメソッドの実行順は、常に同じにはならない
/// </remarks>
/// <see href="https://xunit.net/docs/shared-context"/>
public class StackTests : IDisposable
{
    Stack<int> stack;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <remarks>
    /// オブジェクト インスタンスを共有せずに、セットアップ/クリーンアップ コードを共有する
    /// 実行されるごとに常にクリーンなコピーが取得される
    /// </remarks>
    public StackTests()
    {
        // ここにセットアップコードを実装する
        stack = new Stack<int>();
    }

    public void Dispose()
    {
        // ここにクリーンアップコードを実装する
        stack.GetEnumerator().Dispose();            
    }

    [Fact]
    public void WithNoItems_CountShouldReturnZero()
    {
        var count = stack.Count;

        Assert.Equal(0, count);
    }

    [Fact]
    public void AfterPushingItem_CountShouldReturnThree()
    {
        stack.Push(1);
        stack.Push(2);
        stack.Push(3);

        var enumerator = stack.GetEnumerator();

        while (enumerator.MoveNext())
        {
            var item = enumerator.Current;
            Console.WriteLine(item);
        }

        Assert.Equal(3, stack.Count);
    }
}
```

## Fixture

Fixture は、複数のテストケースで共有できるテスト環境を構築するための仕組み。Fixture 属性が付与されたメソッドは、各テストケースの実行前に呼び出され、テストに必要なリソースを準備する

- 複数のテストケースでコンテキストを共有できる
- テストケースの実行前にはセットアップ、実行後にはクリーンアップ処理を実行できる
- カスタムのFixture属性を作成することで、さまざまなテストケースに対応

```mermaid
flowchart TB
    A(Fixture:コンストラクタ) --> B(テストクラス:コンストラクタ)
    B --> C(SetFixture)
    C --> D(テストメソッドY)
    D --> E(テストクラス:Dispose)
    E --> B
    B --> F(SetFixture)
    F --> G(テストメソッドX)
    G --> H(テストクラス:Dispose)
    H --> I(Fixture:Dispose)
```

```cs
/// <summary>
/// Fixtureクラス
/// </summary>
/// <remarks>
/// メリット
/// 1. リソースの効率的な使用：データベースコンテキストや他の重いリソースをテストごとに再生成する必要がなくなり、リソースの使用効率が向上する
/// 2. 実行時間を短縮：データベースの初期化や接続の確立など、コストのかかる操作を一度だけ行うことで、テストの実行時間を短縮できる
/// 3. セットアップとクリーンアップの簡素化：セットアップとクリーンアップのコードを一箇所にまとめることができ、コードの重複を避けることができる
/// </remarks>
public class InMemoryDbContextFixture : IDisposable
{
    /// <summary>
    /// コンストラクタ
    /// </summary>
    public InMemoryDbContextFixture()
    {
        this.Context = new InMemoryDbContext();

        // ... initialize data in the test database ...
    }

    public void Dispose()
    {
        // ... clean up test data from the database ...
    }

    public InMemoryDbContext Context { get; private set; }
}

public class SharedInMemoryDbContextTests : IClassFixture<InMemoryDbContextFixture>, IDisposable
{
    InMemoryDbContextFixture fixture;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public SharedInMemoryDbContextTests(InMemoryDbContextFixture fixture)
    {
        // 共有したFixtureを受け取る
        this.fixture = fixture;
    }

    public void Dispose()
    {
        // ...
    }

    [Fact]
    public void WithNoItems_CountShouldReturnZero()
    {
        var count = this.fixture.Context.Users.Count();
        this.fixture.Context.Users.Add(new User());

        Assert.True(0 <= count);
    }

    [Fact]
    public void AfterAddingItem_CountShouldReturnOne()
    {
        this.fixture.Context.Users.Add(new User());

        var count = this.fixture.Context.Users.Count;

        Assert.True(0 <= count);
    }
}

/// <summary>
/// インメモリデータベースコンテキスト
/// </summary>
public sealed class InMemoryDbContext
{
    public List<User> Users { get; set; } = new List<User>();
}

/// <summary>
/// ユーザークラス
/// </summary>
public sealed class User
{
    public string? Name { get; set; }
}
```

コンテキストを共有するため、テストの独立性は損なわれる。インスタンス生成に重い処理など、複数個所で使用するが時間の掛かる場合などに検討する


## 2. パラメタライズテスト

パラメタライズテストとは、同じテストメソッドを異なる入力データで繰り返し実行するテスト手法。これにより、コードの複数のケースや境界条件を効率的にテストできる

パラメタライズテストを使用することで、テストケースの重複を避け、同じテストコードを再利用して異なるデータをテスト可能

### InlineData
- 最もシンプルなデータの渡し方
- テストメソッドの引数に直接、データの配列を指定
- 簡単なテストケースに適している

```cs

[Theory]
[InlineData(5, 3, 2)]
[InlineData(10, 5, 5)]
[InlineData(0, 0, 0)]
public void Ok_Add(int expected, int a, int b)
{
    // Arrange
	var calculator = new Calculator();

    // Act
    int actual = calculator.Add(a, b);

    // Assert
    Assert.Equal(expected, actual);
}
```

### ClassData
- カスタムのデータソースクラスを作成し、テストメソッドに渡す
- 複雑なデータセットを生成する場合に有効
- データ生成ロジックをテストクラスから分離できる
```cs
/// <summary>
/// ClassDataでデータを提供する
/// </summary>
[Theory]
[ClassData(typeof(CalculatorTestData))]
public void CanAddTheoryClassData(int expected, int a, int b)
{
    // Arrange
    var calculator = new Calculator();

    // Act
    var actual = calculator.Add(a, b);

    // Assert
    Assert.Equal(expected, actual);
}

/// <summary>
/// データ提供クラス
/// </summary>
public class CalculatorTestData : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[] { 3, 1, 2 };
        yield return new object[] { -10, -4, -6 };
        yield return new object[] { 0, -2, 2 };
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
```

### MemberData
- テストクラスのメンバー変数にデータの配列を定義し、テストメソッドに渡す
- 複数のテストメソッドで同じデータセットを利用する場合に有効
- データ生成ロジックをテストクラス内に保持する
```cs
/// <summary>
/// ジェネレータプロパティを使用したテスト
/// </summary>
[Theory]
[MemberData(nameof(TestData))]
public void CanAddTheoryMemberDataProperty(int expected, int a, int b)
{
    // Arrange
    var calculator = new Calculator();

    // Act
    var actual = calculator.Add(a, b);

    // Assert
    Assert.Equal(expected, actual);
}

/// <summary>
/// ジェネレータプロパティ
/// </summary>
public static IEnumerable<object[]> TestData =>
    new List<object[]>
    {
        new object[] { 3, 1, 2 },
        new object[] { -10, -4, -6 },
        new object[] { 0, -2, 2 }
    };
```

### TheoryData

`TheoryData`は、xUnitのパラメタライズテストで使用されるデータ供給手段の一つであり、他のデータ提供方法である`InlineData`、`ClassData`、`MemberData`と比較して以下のような特徴がある

#### `TheoryData` の特徴

- **型安全性:** ジェネリック型をサポートし、型安全にデータを提供。これにより、型キャストの問題やデータの誤用を防止
- **可読性とメンテナンス性:** データセットが複数行になる場合でも、データの追加や変更が容易。データがクラス内に明示的に定義されるため、テストデータの可読性とメンテナンス性が向上
- **複雑なデータ構造:** 単一のプリミティブ型に限らず、オブジェクトや複数のプロパティを持つ複雑なデータ構造もサポート。これにより、よりリッチなデータセットをテストに供給
- **再利用可能性:** テストクラスの外に定義され、他のテストクラスでも再利用可能。`TheoryData`クラスを使ってテストデータを共有し、異なるテストケースで一貫性のあるデータセットを使用できる

#### `InlineData` との比較
- `InlineData`はテストメソッドに直接インラインでデータを提供。単純なプリミティブ型のデータには便利だが、データ量が増えるとメソッドシグネチャが見づらくなる。

#### `ClassData` との比較
- `ClassData`はテストデータを別のクラスで定義するが、`IEnumerable<object[]>`の形式でデータを提供するため、型安全性はない（`object`型）。また、データの型が複数混在する場合、型キャストが必要

#### `MemberData` との比較
- `MemberData`は静的メンバーとして定義されたデータセットを使用するが、こちらも`IEnumerable<object[]>`形式が必要で、型安全性には欠けます。また、データが外部ソースから取得されることが多いため、データの内容を理解するのに手間がかかる

```cs
public class MathTestData : TheoryData<int, int, int>
{
    public MathTestData()
    {
        Add(1, 2, 3);
        Add(2, 3, 5);
        Add(3, 5, 8);
    }
}

public class MathTests
{
    [Theory]
    [ClassData(typeof(MathTestData))]
    public void Add_ReturnsCorrectSum(int a, int b, int expected)
    {
        var result = a + b;
        Assert.Equal(expected, result);
    }
}
```

活用例：テストデータに意味のある名前を付けて渡すなど

## 参考

https://qiita.com/takutoy/items/c684f761c655d832e5d2#%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9%E3%81%AF%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%A6%E4%BD%9C%E3%82%8B%E3%81%AE

https://xunit.net/docs/shared-context

https://news.mynavi.jp/techplus/article/20100716-xunit/2

https://guiferreira.me/blog/