目次
概要
xUnitとMoqを使用して、クラスの一部のメソッドだけをモック(偽装)し、残りのメソッドは本来の実装をそのまま動作させる「Partial Mock(部分モック)」の手法です。 乱数生成や現在時刻の取得など、テストのたびに結果が変わる不安定なメソッドだけを固定化し、その結果を利用するメインのロジックをテストしたい場合に有効です。
仕様(入出力)
- テスト対象: ランダムなセキュリティコードを生成するクラス。
- 入力:
- 生成するコードの長さ(整数)
- モック設定: 乱数生成メソッドが「常に特定のインデックス」または「特定の順序」を返すように固定。
- 出力:
- 生成された文字列が、モックで固定した通りのパターン(例: “AAAA” や “ABCD”)になっているかを検証。
- 技術要件:
- Moqの
CallBase = trueを使用し、Setupしていないメソッドは元のクラスの実装を動かす。 - モック化するメソッドは
virtualである必要がある。
- Moqの
基本の使い方
クラスの実装を活かしつつ一部だけ書き換えるには、CallBase = true を設定します。
// モック作成時に CallBase = true を指定
var mock = new Mock<MyClass> { CallBase = true };
// virtualメソッドだけ動作を書き換える
mock.Setup(m => m.GetRandomValue()).Returns(100);
// 書き換えていないメソッドは元の実装が動く
var result = mock.Object.Calculate();
コード全文
コンソールアプリケーションとして実行可能な形式ですが、内容はxUnitテストクラスの構造です。 Main メソッド内でテストを実行し、擬似的に動作確認ができるようにしています。
using System;
using System.Text;
using Xunit; // dotnet add package xunit
using Moq; // dotnet add package Moq
// 実際のテストプロジェクトでは Main メソッドは不要です。
// dotnet test コマンドで実行します。
namespace PartialMockExample
{
class Program
{
static void Main()
{
try
{
var test = new SecurityCodeGeneratorTests();
Console.WriteLine("Test 1: 固定値テスト実行中...");
test.Generate_ReturnsFixedString_WhenRandomIsFixed();
Console.WriteLine("-> Pass");
Console.WriteLine("Test 2: シーケンステスト実行中...");
test.Generate_ReturnsSequentialString_WhenRandomIsSequential();
Console.WriteLine("-> Pass");
}
catch (Exception ex)
{
Console.WriteLine($"-> Fail: {ex.Message}");
}
}
}
// --- テストクラス ---
public class SecurityCodeGeneratorTests
{
// クラス自体をモック化対象とする
private readonly Mock<SecurityCodeGenerator> _mock;
public SecurityCodeGeneratorTests()
{
// 【重要】CallBase = true にすることで、
// Setupしていないメソッドはクラス本来の実装が実行される
_mock = new Mock<SecurityCodeGenerator> { CallBase = true };
}
[Fact]
public void Generate_ReturnsFixedString_WhenRandomIsFixed()
{
// Arrange
// GetRandomIndex が常に 0 (文字セットの先頭) を返すように固定
_mock.Setup(x => x.GetRandomIndex(It.IsAny<int>()))
.Returns(0);
var generator = _mock.Object;
// Act
// Generateメソッド自体は本来の実装が動く
var result = generator.Generate(4);
// Assert
// "ABC..." の0番目 "A" が4回選ばれるはず
Assert.Equal("AAAA", result);
}
[Fact]
public void Generate_ReturnsSequentialString_WhenRandomIsSequential()
{
// Arrange
// 呼び出されるたびに 0, 1, 2, 3 を順番に返す
_mock.SetupSequence(x => x.GetRandomIndex(It.IsAny<int>()))
.Returns(0)
.Returns(1)
.Returns(2)
.Returns(3);
var generator = _mock.Object;
// Act
var result = generator.Generate(4);
// Assert
// "ABC..." の0,1,2,3番目が順に選ばれるはず
Assert.Equal("ABCD", result);
}
}
// --- テスト対象クラス ---
public class SecurityCodeGenerator
{
// 使用する文字セット
private readonly string _charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
// メインロジック(ここはモック化せず、そのままテストしたい)
public string Generate(int length)
{
var sb = new StringBuilder(length);
for (int i = 0; i < length; i++)
{
// 内部で仮想メソッドを呼んでいるのがポイント
int index = GetRandomIndex(_charSet.Length);
sb.Append(_charSet[index]);
}
return sb.ToString();
}
// 乱数生成部分
// 【重要】Moqで上書きするために virtual にする
public virtual int GetRandomIndex(int max)
{
// 本番ではランダムな値を返す
return Random.Shared.Next(max);
}
}
}
出力例
Test 1: 固定値テスト実行中...
-> Pass
Test 2: シーケンステスト実行中...
-> Pass
カスタムポイント
- 対象メソッドの可視性
- Moqでオーバーライドするためには、メソッドは
public virtual(またはprotected virtual+Protected().Setup(...)) である必要があります。設計上virtualにしたくない場合は、インターフェース抽出(IRandomProviderなど)を検討してください。
- Moqでオーバーライドするためには、メソッドは
- 乱数以外の用途
DateTime.Nowをラップする仮想メソッドを作成し、テスト時のみ特定の日時を返すように設定することで、時間依存のロジック(有効期限判定など)も同様にテスト可能です。
注意点
- CallBase = true の忘れ
- これを忘れると、
Setupしていないメソッド(テスト対象のGenerateなど)がデフォルト値(nullなど)を返すだけの空虚なメソッドになり、テストになりません。
- これを忘れると、
- 設計への影響
- テストのためだけにメソッドを
virtualにするのは、設計としてのカプセル化を弱める可能性があります。Partial Mockはレガシーコードの救済措置として使い、可能なら「乱数生成ロジック」を別のインターフェースに切り出して依存注入(DI)する方がクリーンです。
- テストのためだけにメソッドを
- コンストラクタの引数
- テスト対象クラスに引数付きコンストラクタがある場合、
new Mock<TargetClass>(arg1, arg2)のようにMockのコンストラクタに引数を渡す必要があります。
- テスト対象クラスに引数付きコンストラクタがある場合、
応用
Protectedメソッドのモック化
外部には公開していない内部ロジック(protected virtual)をモック化したい場合、Moq.Protected 名前空間を使用します。
using Moq.Protected;
// protected virtual int GetInternalValue() をモック化
mock.Protected()
.Setup<int>("GetInternalValue")
.Returns(99);
まとめ
xUnitとMoqを使ったPartial Mockは、virtual メソッドと CallBase = true を組み合わせることで実現できます。これにより、乱数や時間といった不確定要素を含むクラスでも、ロジック部分の厳密な単体テストが可能になります。ただし、多用するとクラス設計がテストに引きずられるため、適切な場面で利用してください。
