プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2020/06/26

[C#] ClassData でテストデータを作成する際に、テストクラス内のリソースにアクセスしたい

update2020/06/30 event_note2020/06/26 1:14

xUnit で複雑なテストデータを作成する場合、ClassData を使って別途テストデータ作成用のクラスを用意したりしますが、このテストデータ作成用のクラスでテストクラス内のリソースを操作したいことがあったので、思いついた案を載せておきます。

ここでいうリソースとは、例えばテスト用のデータベースだとか、外部ファイルなどです。

あくまで私が思いついた一例ですが、概要としては、以下です。

  • テストメソッドの引数にデリゲートを用意し、テストメソッド内からリソースを渡す
  • テストデータ作成用のクラスで、リソースに対する処理を記述

環境

  • Visual Studio 2017
  • .NET Core 2.2

サンプルコード

resource がデータベースなどの外部のリソースを想定しています。

テスト対象のクラス

以下のようなクラスをテストしたいとします。

// テストしたいクラス
public class Target
{
    // テストしたいメソッド
    public int Add(IList<int> resource)
    {
        // 例えば、データベースなどから値を読み込んで、何か処理をした値を返すなど
        // ここでは全レコードを読み込んで総和を返すようなことを想定
        return resource.Sum();
    }
}

テストコード

以下がテストコードです。
解説はコメントに書いてある通りです。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Xunit;
using Xunit.Abstractions;

namespace TestProject
{
    // テストを行うクラス
    public class TestClass
    {
        ITestOutputHelper output;
        Target target;
        readonly IList<int> resource;// 例えば、データベースやファイルなどの外部リソース

        public TestClass(ITestOutputHelper output)
        {
            // デバッグ用にテストエクスプローラーに出力できるようにしておく
            this.output = output;

            // リソースの初期化(実際には IClassFixture を使うなどする)
            resource = new List<int> { 1, 2 };

            // テストクラスのインスタンスを作成
            target = new Target();
        }

        [Theory,
            ClassData(typeof(TestData))]
        public void TestMethod(
            // テスト前にリソースに対して行う処理(戻り値が必要なら Func にする)
            Action<IList<int>> action,
            // テスト実行後の期待値
            int expected
            )
        {
            // テストに必要な前処理を行う
            action(resource);

            // デバッグ用にテスト前のリソースの状態を出力
            output.WriteLine($"resource: {string.Join(", ", resource)}");

            // テストの実行
            var result = target.Add(resource);

            // デバッグ用に結果を出力
            output.WriteLine($"result: {result.ToString()}");

            // 結果の確認
            Assert.Equal(expected, result);

        }
    }

    // テストデータを作成するクラス
    class TestData : IEnumerable<object[]>
    {
        List<object[]> _testData = new List<object[]>();

        public TestData()
        {
            // いくつかのパターンでテストデータを作成してみる

            // テストケース1(リソースの中身を書き換え)
            Action<IList<int>> action = resource =>
            {
                resource[0] = 1;
                resource[1] = 2;
            };
            _testData.Add(new object[] {
                action,
                3
            });

            // テストケース2(リソースに要素を追加)
            _testData.Add(new object[] {
                new Action<IList<int>>(resource => resource.Add(3)),
                6
            });

            // テストケース3(リソースをクリア)
            action = resource => resource.Clear();
            _testData.Add(new object[] {
                action,
                0
            });
        }

        public IEnumerator<object[]> GetEnumerator() => _testData.GetEnumerator();

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

実行結果

テストケース1

resource: 1, 2
result: 3

テストケース2

resource: 1, 2, 3
result: 6

テストケース3

resource: 
result: 0

参考 URL