【C#】xUnitとMoqで依存性注入(DI)を利用したクラスの単体テストを作成する方法

目次

概要

モダンなC#開発において標準的な「依存性注入(Dependency Injection: DI)」パターンを採用したクラスの単体テスト手法です。 外部サービスやデータベースへのアクセスを行うクラスを直接インスタンス化せず、インターフェース経由でモックオブジェクトを注入することで、外部依存を切り離した純粋なロジックの検証を行います。

仕様(入出力)

  • テスト対象クラス: ResponseProcessor
    • 外部サービスから取得した生の文字列データ(例: "Result:SUCCESS")を解析し、整形された値(例: "success")を返すロジックを持つ。
  • 依存インターフェース: IDataFetcher
    • 外部からデータを取得する役割。テスト時はMoqで偽装する。
  • 入力:
    • テスト対象メソッドへの引数(IDなど)。
    • モックの振る舞い定義(指定したIDに対して、固定の文字列を返す)。
  • 出力:
    • テスト対象メソッドの戻り値が、モックの戻り値を正しく加工した結果になっているか検証する。

基本の使い方

「コンストラクタ注入」を採用しているクラスに対し、Moqで作ったオブジェクトを渡してインスタンス化します。

// 1. 依存インターフェースのモックを作成
var mock = new Mock<IDataFetcher>();

// 2. 外部サービスの振る舞いを定義(ID:100 なら "Status:OK" を返す)
mock.Setup(x => x.GetData(100)).Returns("Status:OK");

// 3. モックをコンストラクタに注入してテスト対象を生成
var processor = new ResponseProcessor(mock.Object);

// 4. 検証
var result = processor.GetFormattedStatus(100);
Assert.Equal("ok", result);

コード全文

xUnitとMoqを使用した完全なテストコードです。 コンソールアプリとして実行可能な形式で記述していますが、通常はクラスライブラリ(テストプロジェクト)として作成します。

using System;
using Xunit; // dotnet add package xunit
using Moq;   // dotnet add package Moq

// テストランナーで実行する場合はMainメソッドは不要です
namespace DependencyInjectionTest
{
    class Program
    {
        static void Main()
        {
            // 手動での実行確認用
            var testClass = new ResponseProcessorTests();
            try
            {
                testClass.GetFormattedStatus_ReturnsLowerCaseValue();
                Console.WriteLine("Test Passed: GetFormattedStatus_ReturnsLowerCaseValue");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Test Failed: {ex.Message}");
            }
        }
    }

    // --- テストコード ---
    public class ResponseProcessorTests
    {
        [Fact]
        public void GetFormattedStatus_ReturnsLowerCaseValue()
        {
            // Arrange (準備)
            int testId = 99;
            string rawResponse = "Result=Active";
            string expected = "active"; // "Active" を小文字化したもの

            // モックの作成
            var mockFetcher = new Mock<IDataFetcher>();

            // モックの設定: 引数が testId の時だけ rawResponse を返す
            mockFetcher.Setup(m => m.FetchData(testId))
                       .Returns(rawResponse);

            // 依存性の注入(Dependency Injection)
            // コンストラクタ経由でモックオブジェクトを渡す
            var processor = new ResponseProcessor(mockFetcher.Object);

            // Act (実行)
            // 内部で mockFetcher.FetchData(99) が呼ばれる
            var actual = processor.GetFormattedStatus(testId);

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

    // --- 実装コード(テスト対象) ---

    // 1. 依存機能の抽象化(インターフェース)
    public interface IDataFetcher
    {
        // 外部からデータを取得するメソッド定義
        string FetchData(int id);
    }

    // 2. ビジネスロジッククラス(テスト対象)
    public class ResponseProcessor
    {
        private readonly IDataFetcher _fetcher;

        // コンストラクタ注入
        // 外部から IDataFetcher の実装を受け取る
        public ResponseProcessor(IDataFetcher fetcher)
        {
            _fetcher = fetcher ?? throw new ArgumentNullException(nameof(fetcher));
        }

        public string GetFormattedStatus(int id)
        {
            // 注入されたオブジェクトを利用してデータを取得
            var rawData = _fetcher.FetchData(id);

            // 取得データが空なら空文字を返すガード節
            if (string.IsNullOrEmpty(rawData))
            {
                return string.Empty;
            }

            // ロジック: "Key=Value" の形式を想定し、Value部分を小文字にして返す
            // 例: "Result=Active" -> ["Result", "Active"] -> "active"
            var parts = rawData.Split('=');
            if (parts.Length < 2)
            {
                return string.Empty;
            }

            return parts[1].ToLower();
        }
    }
}

出力例

Test Passed: GetFormattedStatus_ReturnsLowerCaseValue

カスタムポイント

  • 複数インターフェースの注入
    • テスト対象がロガーや設定クラスなど複数の依存を持つ場合、それぞれ var mockLog = new Mock<ILogger>(); のように作成し、new Target(mockFetch.Object, mockLog.Object) と渡します。
  • Setupの柔軟性
    • 特定のIDだけでなく「どんなIDが来てもこの値を返す」としたい場合は、Setup(m => m.FetchData(It.IsAny<int>())) を使用します。
  • 例外系のテスト
    • 外部サービスがエラーを吐いた場合を想定し、mock.Setup(...).Throws(new Exception()) と設定することで、テスト対象クラスのエラーハンドリング(try-catch)が正しく機能するか検証できます。

注意点

  1. インターフェース設計の重要性
    • Moqでモック化できるのは、基本的にインターフェースか virtual メソッドのみです。テスト容易性(Testability)を高めるため、外部依存を持つクラスは必ずインターフェースを抽出する設計(DIP: 依存関係逆転の原則)を心がけてください。
  2. モックの設定漏れ
    • Setup していない引数でメソッドが呼ばれた場合、Moqはデフォルト値(null など)を返します。これによる NullReferenceException はテストでよくある失敗原因です。It.IsAny<T>() を活用するか、MockBehavior.Strict で未設定時の呼び出しをエラーにするかを検討してください。
  3. ロジックの分離
    • テスト対象クラスの中で new DataFetcher() と直接インスタンス化してしまうと、モックを差し込む隙間がなくなります。必ずコンストラクタ等の外部から受け取る形に修正してください。

応用

プロパティ注入への対応

コンストラクタではなくプロパティで依存を受け取る場合も同様にテスト可能です。

var processor = new ResponseProcessor();
// プロパティ経由でモックを注入
processor.Fetcher = mock.Object;

var result = processor.GetFormattedStatus(100);

ただし、必須の依存関係はコンストラクタ注入を使用する方が、初期化忘れを防げるため推奨されます。

まとめ

DIパターンとMoqを組み合わせることで、以下のようなメリットがあるテストを作成できます。

  • 外部環境に依存しない: ネットワークやDBがなくてもテストが可能。
  • 異常系の再現: 実環境では起こしにくいエラー(通信タイムアウトなど)を簡単にシミュレート可能。
  • 高速な実行: 重い処理をモックで置換するため、テストが一瞬で終わる。

実際の開発では「インターフェースで依存を定義」し、「xUnit + Moqでその振る舞いを定義して注入」するのがユニットテストの黄金パターンです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次