C#のテストでモックを活用!Moqの使い方と単体テストの基本を初心者向けに解説
生徒
「C#でプログラムのテストを書いているのですが、データベースや外部のシステムと連携する部分のテストがうまくいきません。本物のデータを使わずにテストする方法はありますか?」
先生
「それは『モック』という技術を使うのが一番ですね。C#では『Moq(モック)』というライブラリを使うことで、本物の部品の代わりになる『身代わり』を簡単に作ることができますよ。」
生徒
「身代わりですか!それなら本物のデータベースを準備しなくても、その部分の動作をシミュレートしてテストができるということですね?」
先生
「その通りです!今回は、テストを劇的に効率化するMoqの基本的な使い方について、じっくり解説していきましょう。」
1. モック(Mock)とは何か?初心者のための基礎知識
プログラミングにおけるモック(Mock)とは、一言で言えば「本物のオブジェクトのふりをする代役」のことです。たとえば、あなたが「メールを送信する機能」を持つプログラムをテストしたいとします。しかし、テストのたびに実際にメールが送られてしまっては困りますし、インターネットに繋がっていない環境ではテストができなくなってしまいます。
そこで登場するのがモックです。本物のメール送信機能の代わりに、「メールを送ったことにする」偽物の部品を差し込むのです。これにより、外部環境に左右されることなく、自分が作ったプログラムが正しく動くかどうかを確認できるようになります。
C#の世界で、このモックを最も簡単に作れるライブラリがMoq(モック)です。Moqを使うことで、「このメソッドが呼ばれたら、この値を返す」という設定を数行のコードで記述できます。これにより、複雑な準備なしに単体テスト(ユニットテスト)をスムーズに進めることが可能になります。
2. なぜテストにモックが必要なのか?メリットを解説
なぜ本物を使わずに、わざわざモックという偽物を用意するのでしょうか。それには大きな理由が三つあります。一つ目は「隔離(アイソレーション)」です。テストしたいのは「自分の書いたロジック」であり、外部のデータベースが壊れているかどうかではありません。モックを使えば、外部要因を切り離して、純粋に自分のコードだけを評価できます。
二つ目は「速度」です。本物のデータベースやサーバーに接続すると、ネットワークの待ち時間が発生し、テストの実行に時間がかかります。数千件のテストがある場合、この差は数時間の差になります。モックはパソコンのメモリ上だけで完結するため、一瞬でテストが終わります。
三つ目は「異常系のテスト」です。「データベースが突然故障した時」や「サーバーがタイムアウトした時」など、本物では再現しにくい失敗パターンも、モックを使えば「あえてエラーを出す」という設定を簡単に行えます。これにより、故障に強い頑丈なプログラムを作ることができるのです。
3. 準備編:Moqをプロジェクトに導入する方法
C#でMoqを利用するには、まずライブラリをプロジェクトに追加する必要があります。通常は、Visual Studioの「NuGetパッケージマネージャー」を使用します。検索窓に「Moq」と入力し、最新の安定版をインストールしましょう。
Moqはインターフェースという仕組みを利用して動作します。インターフェースとは、クラスが「どのような機能を持っているか」を定義した設計図のようなものです。Moqはこの設計図を読み取って、中身が空っぽの状態の代役を自動生成してくれます。そのため、テストしたいクラスは、具体的な部品ではなく、インターフェースに依存するように作っておくのがC#の作法です。これを「依存性の注入(DI)」と呼びますが、今は「インターフェースを使えばテストがしやすくなる」と覚えておけば大丈夫です。
4. Moqの基本的な使い方!SetupとReturnsを覚えよう
Moqの最も基本的な操作は、SetupメソッドとReturnsメソッドの組み合わせです。これは「もし〇〇というメソッドが呼ばれたら、××という値を返してね」という予約をする作業です。それでは、具体的なコードを見てみましょう。ここでは、商品の価格を計算するサービスを例にします。
using Moq;
using Xunit;
// 1. インターフェースの定義(設計図)
public interface IPriceRepository
{
int GetPrice(string itemName);
}
// 2. テストコード
public class PriceServiceTest
{
[Fact]
public void Test_GetItemPrice()
{
// モックの作成(代役を作る)
var mockRepo = new Mock<IPriceRepository>();
// 動作の設定(「リンゴ」なら100を返すと決める)
mockRepo.Setup(repo => repo.GetPrice("リンゴ")).Returns(100);
// テスト実行
int result = mockRepo.Object.GetPrice("リンゴ");
// 結果の検証
Assert.Equal(100, result);
}
}
このコードでは、IPriceRepositoryというインターフェースの代役を作っています。mockRepo.Objectと書くことで、本物のクラスと同じように振る舞うインスタンスを取り出すことができます。Returns(100)のおかげで、データベースにアクセスしなくても確実に100という数字が返ってきます。これがモックの基本です。
5. 引数に依存しない設定!It.IsAnyの使い方
先ほどの例では「リンゴ」という特定の文字が来た時だけ反応するように設定しました。しかし、実際のテストでは「どんな文字が来ても同じ値を返してほしい」という場面があります。その時に使うのがIt.IsAny<T>()という便利な機能です。
[Fact]
public void Test_AnyItemPrice()
{
var mockRepo = new Mock<IPriceRepository>();
// どんな文字列が引数に来ても、一律で500を返す設定
mockRepo.Setup(x => x.GetPrice(It.IsAny<string>())).Returns(500);
// テスト
var price1 = mockRepo.Object.GetPrice("ミカン");
var price2 = mockRepo.Object.GetPrice("ブドウ");
// 検証:どちらも500になっているはず
Assert.Equal(500, price1);
Assert.Equal(500, price2);
}
It.IsAny<string>()は「文字列なら何でもOK」という意味です。これを使うことで、テストデータの準備を最小限に抑えることができます。数字の場合はIt.IsAny<int>()のように書きます。型を指定するだけで、広範囲な入力をカバーできる非常に強力な武器になります。
6. 呼び出し回数の検証!Verifyで動作確認
モックの役割は値を返すだけではありません。「そのメソッドが本当に呼ばれたかどうか」を確認することもできます。これを「検証(Verify)」と呼びます。例えば、「保存ボタンを押した時に、データベースの保存メソッドが1回だけ呼ばれること」を確かめたい場合に便利です。
public interface ILogger
{
void Log(string message);
}
[Fact]
public void Test_LogMethodCalled()
{
var mockLogger = new Mock<ILogger>();
// 何らかの処理を実行(ここでは直接メソッドを呼びます)
mockLogger.Object.Log("テストメッセージ");
// 検証:Logメソッドが「ちょうど1回」呼ばれたかチェック
mockLogger.Verify(logger => logger.Log("テストメッセージ"), Times.Once());
}
Times.Once()は「1回だけ」という意味です。もし1回も呼ばれなかったり、2回以上呼ばれたりした場合は、このテストは失敗します。このように、目に見える戻り値がない処理(void型のメソッドなど)でも、正しく動いているかをチェックできるのがMoqの素晴らしいところです。プログラムが意図しない挙動をして、何度も無駄に処理を繰り返していないかを確認するのにも役立ちます。
7. 意図的にエラーを起こす!例外のシミュレーション
優れた開発者は、成功パターンだけでなく失敗パターンもテストします。モックを使えば、「特定のメソッドを呼んだ時にわざとエラー(例外)を発生させる」という設定が可能です。これにより、エラーが発生した際のメッセージ表示や、後始末の処理が正しく行われるかをテストできます。
[Fact]
public void Test_ExceptionHandling()
{
var mockRepo = new Mock<IPriceRepository>();
// GetPriceが呼ばれたら、強制的に「通信エラー」という例外を投げる設定
mockRepo.Setup(x => x.GetPrice(It.IsAny<string>()))
.Throws(new System.Exception("通信に失敗しました"));
// テスト:例外が発生することを検証
var ex = Assert.Throws<System.Exception>(() =>
mockRepo.Object.GetPrice("エラー商品")
);
Assert.Equal("通信に失敗しました", ex.Message);
}
Throwsメソッドを使うことで、例外オブジェクトを発生させることができます。実際の開発現場では、ネットワーク遮断やディスク容量不足といった、めったに起きないけれど重要なトラブルのシミュレーションに多用されます。これにより、本番環境で予期せぬエラーが起きてシステムがクラッシュするのを未然に防ぐことができるのです。
8. 複数の設定を組み合わせる!シーケンスの設定
同じメソッドを複数回呼んだ時に、1回目は「成功」、2回目は「失敗」というように、動作を変化させたい場合もあります。MoqではSetupSequenceを使うことで、呼び出しごとに異なる戻り値を設定できます。
[Fact]
public void Test_SequenceSetup()
{
var mock = new Mock<IPriceRepository>();
// 呼び出し順に戻り値を指定する
mock.SetupSequence(x => x.GetPrice(It.IsAny<string>()))
.Returns(100) // 1回目の戻り値
.Returns(200) // 2回目の戻り値
.Throws(new System.Exception("3回目はエラー")); // 3回目は例外
Assert.Equal(100, mock.Object.GetPrice("A"));
Assert.Equal(200, mock.Object.GetPrice("B"));
Assert.Throws<System.Exception>(() => mock.Object.GetPrice("C"));
}
リトライ処理(失敗した時にもう一度やり直す処理)が正しく動くかを確認する際、このシーケンス設定は非常に便利です。「最初は通信失敗したけれど、2回目は成功した」という複雑なシナリオを簡単に作ることができます。
9. 非同期メソッドのモック化!Taskを返す方法
現代のC#開発では、asyncやawaitを使った非同期処理が一般的です。非同期メソッドをモックにする場合は、単に値を返すのではなく、Taskを返すように設定する必要があります。MoqではReturnsAsyncという専用のメソッドが用意されています。
public interface IAsyncService
{
System.Threading.Tasks.Task<string> GetDataAsync();
}
[Fact]
public async System.Threading.Tasks.Task Test_AsyncMethod()
{
var mock = new Mock<IAsyncService>();
// 非同期メソッド用にReturnsAsyncを使用する
mock.Setup(x => x.GetDataAsync()).ReturnsAsync("完了");
string result = await mock.Object.GetDataAsync();
Assert.Equal("完了", result);
}
非同期処理は初心者がつまずきやすいポイントですが、Moqを使えば同期処理とほとんど変わらない感覚でテストが書けます。ReturnsAsyncを使うことで、自動的にTask.FromResultなどでラップされた値が返されるようになります。これでウェブAPIの呼び出しなど、時間がかかる処理の代役もバッチリ作れますね。
10. モックを使いこなすための注意点
モックは非常に便利ですが、使いすぎには注意が必要です。何でもかんでもモックにしてしまうと、「何をテストしているのかわからない」状態になってしまいます。理想的なテストは、自分の書いた計算ロジックなどの重要な部分は本物を使い、データベースや外部APIといった「自分ではコントロールできない重い部分」だけをモックにすることです。
また、モックの設定(Setup)が複雑になりすぎている場合は、元のプログラムの設計に問題があるサインかもしれません。クラスの責任が多すぎて、一つのテストのために何十行もSetupを書かなければならないなら、クラスを分割することを検討しましょう。モックは単なるテストツールではなく、あなたのコードが「疎結合(バラバラにしやすい良い設計)」であるかどうかを教えてくれる物差しにもなるのです。