C#入門編です。今回はモックスモークテストについて解説します。

前回の記事で自動テストの基本を学びましたが、「ファイルに依存するテストは不便」という課題が残りました。

本記事は以下の方に役立つ内容となっています。

  • モックの概念と使いどころを理解したい方
  • NSubstituteを使ったモックの書き方を学びたい方
  • 自動テストを「何から始めればよいか」を知りたい方
プロ太

モックはテストの目的を達成するための手段(便利な道具)です。必要なときに使えるようになりましょう!

また、初めて自動テストを導入するなら「スモークテスト」から始めるのがおすすめです。

前回の記事をまずみてもらえると、より理解が深まるかと思います。

C#入門編(27)自動テスト入門 ~xUnitで「壊れたら気づける」仕組みを作る~ C#入門編です。今回はC#における自動テストについて解説します。 「コードを変更するたびに、手動で動作確認するのが大変…」「いつ...

演習コードをGitHubで公開しています。

講義1:モックとは何か

前回の課題を振り返る

前回の演習で、CalculateWithTaxメソッドのテストを書きました。設定ファイルからの税率読み込みを含めた動作を確認するテストを行っています。

[Fact]
public void CalculateWithTax_WithValidPrice_ReturnsPriceWithTax()
{
    // Arrange
    var taxRateLoader = new TaxRateLoader();
    var calculator = new Calculator(taxRateLoader);

    // Act
    var result = calculator.CalculateWithTax(1000m);

    // Assert
    Assert.Equal(1100m, result);  // 税率10%: 1000 * 1.1 = 1100
}

このように外部ファイルを直接使う方法ですと、以下のようなテスト目的で課題が生じます。

  • (1)テスト目的様々な税率で計算ロジックの正しさを確認したい
    →課題:税率5%や20%など、様々なケースをテストするにはファイルの内容を変更する必要があり、手間がかかる
  • (2)テスト目的ファイルI/Oエラーの場合の挙動を確認したい
    →課題:ファイルI/Oエラーを起こすには、ファイルを削除するなどの操作を適切なタイミングで行う必要があり、難しい場合がある
プロ美

本当にテストしたいのは「税込計算のロジック」なのに、ファイルの準備が必要なのは面倒だね…。

プロ太

ファイルアクセスだけではなく、外部APIの呼び出しデータベースアクセスなど様々な状況でこのような課題が発生します。

モックの概念

この課題を解決するのがモックです。モックとは、テスト対象が依存する部品の「代役」となるオブジェクトのことです。

本物の代わりにモックを使うことで、テスト対象を外部依存から切り離して検証できます。以下のようなイメージです。

  • 【本番環境】
    Calculator → TaxRateLoader → taxrate.txt(ファイル)
  • 【テスト環境(モック使用)】
    Calculator → モック(任意の税率を返す)

モックを使うと、以下のメリットがあります。

メリット説明
環境構築が不要ファイルやDBがなくてもテストできる
テストが高速ファイルI/Oやネットワーク通信が不要
自在にコントロール任意の値や例外を返せる
原因究明が容易問題の切り分けがしやすい

インターフェイスと手動モック

前回の演習で、ITaxRateLoaderインターフェイスを定義しました。

public interface ITaxRateLoader
{
    int GetTaxRatePercent();
}

このインターフェイスを実装した「偽物」のクラスを自分で作ることで、モックを実現できます。

// 手動で作成したモック(テスト用の偽実装)
public class FakeTaxRateLoader : ITaxRateLoader
{
    private readonly int _taxRate;

    public FakeTaxRateLoader(int taxRate)
    {
        _taxRate = taxRate;
    }

    public int GetTaxRatePercent()
    {
        return _taxRate;
    }
}

このモックを使えば、ファイルなしでテストできます。

[Fact]
public void CalculateWithTax_WithTaxRate5Percent_ReturnsCorrectPrice()
{
    // Arrange - 税率5%を返すモックを使用
    var fakeTaxRateLoader = new FakeTaxRateLoader(5);
    var calculator = new Calculator(fakeTaxRateLoader);

    // Act
    var result = calculator.CalculateWithTax(1000m);

    // Assert
    Assert.Equal(1050m, result);  // 1000 * 1.05 = 1050
}
プロ太

インターフェイスを使って依存性を注入(DI)していたからこそ、モックに差し替えられるのです。設計の効果がここで活きてきます。

インターフェイス・依存性注入については以下も参考にしてください。

C#入門編(12)オブジェクト指向とは?「インターフェイス」 ~さまざまなクラスを一貫した方法でJSON出力する~ C#入門編です。今回は「インターフェイス」について解説します。 インターフェイスとは、関連性のないクラス間で共通の振る舞いを定義...
C#入門編(24)依存性注入(DI)を徹底解説 ~DIの本質からDIコンテナの使い方まで~ C#入門編です。今回はC#における依存性注入(DI: Dependency Injection)について解説します。 「依存性注...

モックライブラリを使う

手動でモッククラスを作るのは基本ですが、テストケースごとに異なる振る舞いが必要な場合、毎回クラスを作るのは手間がかかります。

そこで便利なのがモックライブラリです。C#では以下のライブラリがよく使われます。

  • Moq:長い歴史があり、広く使われている
  • NSubstitute:シンプルで直感的な記法が特徴

本記事では、記法がシンプルで学びやすいNSubstituteを使います。NSubstituteを使うと、先ほどの手動モックと同じことが1行で書けます。

// NSubstituteを使ったモック作成
var taxRateLoader = Substitute.For<ITaxRateLoader>();
taxRateLoader.GetTaxRatePercent().Returns(5);  // 5%を返すように設定

クラスを定義する必要がなく、テストメソッド内で直接振る舞いを設定できます。

プロ美

これなら、テストケースごとに違う税率を簡単に試せるね!

演習:NSubstituteでファイル読み込みをモック化する

前回記事演習コードを改良してNSubstituteによるモック化を実践し、以下のテストを作成してみましょう。

  • (1)様々な税率で計算ロジックの正しさを確認
  • (2)ファイルI/Oエラーの場合の挙動を確認

手順1:NSubstituteのインストール

前回コードのソリューション構成は以下のようになっていました。

まず、テストプロジェクト(CalcApp.Test)で、以下を行います。

  • NSubstituteをインストール

あと、前回の演習では「xunit」パッケージを使っていたのですが、こちらはメンテナンスが終了しており、「xunit.v3」の使用が奨励されているため、以下のように入れ替えます。

  • xunitをアンインストール
  • xunit.v3をインストール

バージョンをあげても、テストコードはそのまま動くの大丈夫です。

プロ太

xUnitのプロジェクトひな型が、xunit(v2系)を使っていたようです。

NuGetパッケージマネージャで、以下のように非推奨マークがでていたら、チェックするようにしましょう。

手順2:ITaxRateLoaderをモック化

CalculatorTests.csを編集し、まずは練習としてモックを使った簡単なテストを追加して実行します。

namespace CalcApp.Tests;

using CalcApp;
using NSubstitute;

public class CalculatorTests
{
    ...   

    [Fact]
    public void CalculateWithTax_WithMockedTaxRate10Percent_ReturnsCorrectPrice()
    {
        // Arrange - モックを作成し、税率10%を返すように設定
        var taxRateLoader = Substitute.For<ITaxRateLoader>();
        taxRateLoader.GetTaxRatePercent().Returns(10);

        var calculator = new Calculator(taxRateLoader);

        // Act
        var result = calculator.CalculateWithTax(1000m);

        // Assert
        Assert.Equal(1100m, result);
    }

}

このテストはtaxrate.txtファイルがなくても動作します。この追加したテストを実行し、テストが成功することを確認しましょう。

テストエクスプローラ上でテストを選択して「右クリック>実行」で、個別にテストを実行することができます。

手順3:(1)様々な税率パターンをテスト

次に、[Theory][InlineData]を組み合わせて、様々な税率パターンをテストしてみましょう。

[Theory]
[InlineData(1000, 5, 1050)]    // 税率5%: 1000 → 1050
[InlineData(1000, 8, 1080)]    // 税率8%: 1000 → 1080
[InlineData(1000, 10, 1100)]   // 税率10%: 1000 → 1100
[InlineData(500, 10, 550)]     // 税率10%: 500 → 550
[InlineData(0, 10, 0)]         // 税率10%: 0 → 0
public void CalculateWithTax_WithVariousTaxRates_ReturnsExpected(
    decimal price, int taxRate, decimal expected)
{
    // Arrange
    var taxRateLoader = Substitute.For<ITaxRateLoader>();
    taxRateLoader.GetTaxRatePercent().Returns(taxRate);
    
    var calculator = new Calculator(taxRateLoader);

    // Act
    var result = calculator.CalculateWithTax(price);

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

テストを実行して成功することを確認しましょう。

手順4:(2)ファイルI/Oエラーのテスト

モックを使うと、エラーケースのテストも簡単です。例えば、「税率の読み込みに失敗した場合、例外がスローされる」ことを確認できます。

 [Fact]
 public void CalculateWithTax_WhenTaxRateLoadingFails_ThrowsException()
 {
     // Arrange - 例外をスローするモックを設定
     var taxRateLoader = Substitute.For<ITaxRateLoader>();
     taxRateLoader.GetTaxRatePercent().Returns(x => throw new IOException("ファイル読み込みエラー"));

     var calculator = new Calculator(taxRateLoader);

     // Act & Assert
     Assert.Throws<IOException>(() => calculator.CalculateWithTax(1000m));
 }

本物のファイルでこのテストを行おうとすると、ファイルを削除したり権限を変更したりする必要があります。モックなら、テストコード内で簡単に異常系を再現できます。

プロ太

今回は例外がスローされたことを確認しているだけですね。

実務アプリでは、例外発生に伴う様々な処理(エラーメッセージ出力や後始末)が適切に行われるかをチェックすることになります。

プロ美

モックを使うとことで、様々なパターンの税率でテストや、エラーケースのテストが簡単に作れるようになったよ!

講義2:自動テストを何から始めるか

開発におけるペインの解消

初めて自動テストを導入する場合、自分の開発における「ペイン(苦痛)」を解消するテストから始めるのがおすすめです。

誰しもが感じるペインとしては、例えば「開発で繰り返し行う面倒な作業」があります。

面倒な作業の代表例として「少し修正するたびに、アプリを起動して正常に動くか確認する」があるでしょう。この確認を自動化したものを「スモークテスト」と呼びます。

スモークテストとは、アプリ全体(または中枢部分)が壊れていないことを即座に検知するためのテストです。

プロ太

スモークテストの語源は電子機器の動作確認で「電源を入れて煙が出ないか確認する」ことに由来します。

スモークテストの始め方

スモークテストを書く際は、「壊れていないことを素早く確認できる」層を選ぶのが現実的です。

特徴
UI(E2E)ユーザ操作を忠実に再現できるが、環境構築が大変で壊れやすい
API環境構築が比較的容易で、安定したテストを書きやすい
ビジネスロジック最も安定しており、高速にテストできる

多くの場合、API層ビジネスロジック層から始めるのが現実的です。UIのE2Eテストは環境構築が複雑で、UIの変更に弱いため、最初の一歩としてはハードルが高くなりがちです。

プロ太

Webアプリだったら、主要なユーザシナリオ(正常系)で使われるAPIの動作を確認するテストを1個ずつ用意する、といった方法があります。

スモークテストは「入口」である

スモークテストは網羅性を担保するものではありません。あくまで「壊れていないことの最低限の確認」です。

しかし、テスト自動化の価値を即座に体感できるという点で、最初の一歩として最も重要です。

スモークテストで「ボタン一つで確認できる」体験をすると、自然と次のステップに進みたくなります。

  • 「このケースも自動化したい」→ テストケースの拡充
  • 「この部分は切り離してテストしたい」→ より小さい部品を単体でテスト
  • 「外部依存を排除したい」→ モックの活用
プロ太

スモークテストは、テスト自動化を継続・拡張していくための「入口」なのです。

モックの使いどころを判断する

原則として、どの粒度のテストであっても、本物の外部依存(ファイル、DB等)を使ってテストして構いません。

ただし、本物の外部依存を使うとテストが書きにくくなる場合は、モックの出番です。モックを使うかどうかは、以下の観点で判断します。

困りごとなぜ困るか対応
テストが遅いDB・外部APIなどのI/Oで実行時間がかかる依存部分をモックする
環境構築が大変APIキーや専用DBが必要モックで代替する
再現が難しいエラーやタイミング依存の挙動を再現しづらいモックで意図的に振る舞いを制御する
原因を切り分けたいどこで失敗しているか分かりにくい依存部分をモックして影響範囲を限定する

注意:ただし、テストしたい部分までモック化しないように注意

プロ太

モックはあくまで手段です。「何をテストしたいか」を明確にした上で、必要なら使う、という判断をしましょう。

例えば、メソッドレベルの「いわゆる単体テスト」であっても、データベース・外部API呼びだしをそのまま使うという判断はありえます。
(※テスト実行時間・再現性・環境コストなどを許容できる場合)

まとめ

今回はモックの基本と、自動テストの始め方を学びました。

モックは、テスト対象を外部依存から切り離すための「代役」です。インターフェイスを定義しておけば、手動で偽の実装クラスを作ることでモックを実現できます。

ただし、テストケースごとに異なる振る舞いが必要な場合は、NSubstituteなどのモックライブラリを使うと簡潔に書けます。

自動テストを始めるなら、まずはスモークテストからがおすすめです。「小さな単体テストから積み上げる」のではなく、開発のペインを解消するテストから始めましょう。

スモークテストは、アプリ全体(または中枢部分)が壊れていないことを即座に検知するためのテストです。

網羅性を担保するものではありませんが、テスト自動化の価値を体感し、継続・拡張につなげるための「入口」として最も重要な役割を果たします。

モックを使うかどうかは、テストの速度・原因究明のしやすさ・環境構築コストを考慮して判断します。

「何をテストしたいか」という目的が先にあり、モックはそれを実現するための手段です。

プロ太

引き続き、一緒にC#プログラミングや自動テストについて学んでいきましょう!

ABOUT ME
プロ太
ソフトウェア開発を楽しく、効率的に行う方法を追求しています。 開発者の視点から技術的課題に向き合い、「純粋な技術的興味に基づく探求」と「実践的な課題解決」という二つの柱を両輪として活動しています。