C#入門編です。今回はC#における自動テストについて解説します。

コードを変更するたびに、手動で動作確認するのが大変…」「いつの間にか動かなくなっていた…」という経験はありませんか?

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

  • テスト自動化の必要性を理解したい方
  • xUnitを使ったテストの書き方を学びたい方
  • 実務で役立つテストの考え方を身につけたい方
プロ太

テスト自動化の本質は、壊れたことを自動で検知できる仕組みを作ることです。まずは1本、テストを書いてみましょう!

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

動画も作成しています。

講義1:テスト自動化とxUnitの基本

手動確認の限界

アプリを開発するとき、「ちゃんと動くかな?」と確認しますよね。

例えば、商品の割引価格を計算するアプリを作ったとします。手動で確認するなら、こんな流れになるでしょう。(これを、様々なパターンで繰り返します。)

  1. アプリを起動する
  2. 価格に「1000」を入力する
  3. 割引後の価格が正しく表示されることを目視で確認する

さらに大変なのは、コードを修正するたびに、この確認を最初からやり直す必要があることです。

修正によって、今まで動いていた部分が壊れてしまうことをリグレッション(デグレード) といいます。

プロ太

アプリの品質を維持しつつ開発を続けていくには、このリグレッションを効率よく発見・修正する必要があるのです。

プロ美

アプリを少し修正するたびにいろいろなパターンで動作確認をする…ってかなり大変だね!

自動テストの価値

手動では大変なテストですが、テストをコードとして書いて自動実行できるようにしておくと、状況が一変します。

  • ボタン一つで自動確認できる
  • コード修正後も、すぐに問題がないか確認できる
  • 再現可能な検証手段がコードとして残る

テスト自動化の価値は、以下です。

既存の期待される振る舞いが壊れたことを自動で検知できること
(結果として、リグレッションを恐れずにアプリケーションを修正できる)

何をテストするか

テストを書くときは、「どのモジュール(または組み合わせ)で、どのような動作を確認したいか」を決めることが重要です。

例えば、割引価格を計算するアプリなら、以下のようなテストが考えられます。

確認したいことテスト対象
アプリ全体が正しく動くかアプリ全体(設定読み込み→計算→出力)
割引計算のロジックは正しいか計算ロジックのみ
設定ファイルから値を読み込めるか設定読み込み部分のみ

テストの「粒度」(どこまでの範囲をテストするか)には、よく以下のような分類が使われます。

  • 単体テスト(Unit Test):(例)メソッドやクラスなど小さな単位
  • 統合テスト(Integration Test):(例)ビジネスロジック+DB+外部API連携 
  • 総合テスト(System Test) :(例)システム全体の動作(性能確認含む)
  • EndToEndテスト:(例)ユーザ操作の流れが通るかの確認

ただし、これらの分類は後付けの整理ラベルに過ぎません。境界は厳密に定義されているわけではなく、プロジェクトによって解釈が異なります。

プロ太

大切なのは分類ではなく、「何を確認したいか」を考えてテストを書くことです。

xUnitとは

C#でテストを書くためのフレームワークはいくつかあります。

  • xUnit: モダンな設計で、シンプルかつ拡張性が高い
  • NUnit: 長い歴史と豊富な属性による柔軟なテスト記述
  • MSTest: Microsoft純正で、Visual Studioなどとの統合に強い
プロ太

自動テストの初心者や新規開発であれば、.NETコミュニティで広く使われていて、シンプルで学びやすいxUnitがおすすめです。

本記事でもxUnitを使います。

「xUnit」という名前に「Unit」が含まれていますが、これは歴史的な経緯でついたものです。

xUnitは単体テストだけでなく、統合テストやE2Eテストなど、あらゆる自動テストに利用できる汎用的なフレームワークです。

xUnitの主要な概念

xUnitでテストを書くために知っておくべき主要な概念を紹介します。

  • [Fact]属性 – 「このメソッドはテストである」ことを示します。
  • [Theory]属性と[InlineData] – 複数のデータでテストを実行したいときに使います。
  • Assertクラス – 結果が期待通りかを検証します。

最もシンプルなテストコードの例を見てみましょう。

[Fact]
public void Add_WithTwoNumbers_ReturnsSum()
{
    // Arrange(準備)
    var calculator = new Calculator();

    // Act(実行)
    var result = calculator.Add(2, 3);

    // Assert(検証)
    Assert.Equal(5, result);
}

テストメソッドの命名には「テスト対象名_条件_期待結果」というパターンがよく使われます。(テスト対象は粒度によってメソッド・機能・シナリオと色々あります)

上の例では「Addメソッドが、2つの数値を受け取ったとき、合計を返す」ことを表しています。この命名規則により、テストが失敗したときに何が問題かすぐにわかります。

また、テストコードはこのようにAAAパターン(Arrange-Act-Assert)で書くのが一般的です。

フェーズ説明
Arrange(準備)テストに必要なオブジェクトやデータを用意する
Act(実行)テスト対象の処理を呼び出す
Assert(検証)結果が期待通りか確認する
プロ太

それでは演習で、実際にテストを書いてみましょう!

演習:計算機アプリのテストを書こう

演習として計算機アプリのテストを書いてみます。

概要

以下の機能を持つ計算機クラスを作成し、テストを書きます。

  1. Add – 足し算
  2. Divide – 割り算(0除算時は例外)
  3. CalculateWithTax – 設定ファイルから税率を読み込み、税込価格を計算

テストとしては、「設定ファイルからの読み込みを含めたアプリ全体の動作」を確認するものを作ります。

手順1:プロジェクト構成

Visual Studioで以下の手順でプロジェクトを作成します。

  • 1.コンソールアプリプロジェクト CalcApp を作成
  • 2.ソリューションを右クリック →「追加」→「新しいプロジェクト」
  • 3.「xUnit テスト プロジェクト」を選択し、CalcApp.Tests という名前で作成
  • 4.CalcApp.Testsを右クリック →「追加」→「プロジェクト参照」→ CalcAppにチェック

この手順で次のような構成になります。

プロ美

テストコードとプロダクトコードは、別のプロジェクトに分けるんだね!

手順2:プロダクトコード作成

CalcAppプロジェクトの直下で以下のファイルを作成し、計算機アプリを構成するクラスを作成します。

ITaxRateLoader.cs – 税率読み込み機能のインターフェイス

namespace CalcApp;

public interface ITaxRateLoader
{
    int GetTaxRatePercent();
}

TaxRateLoader.cs – 設定ファイルから税率を読み込む


namespace CalcApp;

public class TaxRateLoader : ITaxRateLoader
{
    private readonly string _filePath;

    public TaxRateLoader(string filePath = "taxrate.txt")
    {
        _filePath = filePath;
    }

    public int GetTaxRatePercent()
    {
        var text = File.ReadAllText(_filePath);
        return int.Parse(text.Trim());
    }
}

Calculator.cs – 計算機本体

namespace CalcApp;

public class Calculator
{
    private readonly ITaxRateLoader _taxRateLoader;

    public Calculator(ITaxRateLoader taxRateLoader)
    {
        _taxRateLoader = taxRateLoader;
    }

    public int Add(int a, int b)
    {
        return a + b;
    }

    public int Divide(int a, int b)
    {
        if (b == 0)
            throw new DivideByZeroException();
        return a / b;
    }

    public decimal CalculateWithTax(decimal price)
    {
        var taxRate = _taxRateLoader.GetTaxRatePercent();
        return price * (100 + taxRate) / 100;
    }
}

taxrate.txtを作成し、内容を 10 とします(税率10%)。

10

プロジェクトファイル(CalcApp.csproj)に以下を追加して、ビルド時に出力フォルダへコピーされるようにします。

...
<ItemGroup>
  <None Update="taxrate.txt">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </None>
</ItemGroup>
...

テストプロジェクトからCalcAppを参照しているので、この設定だけでテスト実行時にもtaxrate.txtが利用できます。

これでプロダクトコードは完成です。次にテストを書いていきましょう!

手順3:はじめてのテストを書いて実行

CalcApp.TestsプロジェクトにあるUnitTest1.csCalculatorTests.csにリネームし、以下のように編集します。

まずは各機能に対して1つずつ、基本的なテストを書いてみましょう。

namespace CalcApp.Tests;

using CalcApp;

public class CalculatorTests
{
    [Fact]
    public void Add_WithTwoNumbers_ReturnsSum()
    {
        // Arrange(準備)
        var taxRateLoader = new TaxRateLoader();
        var calculator = new Calculator(taxRateLoader);

        // Act(実行)
        var result = calculator.Add(2, 3);

        // Assert(検証)
        Assert.Equal(5, result);
    }

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

        // Act
        var result = calculator.Divide(10, 2);

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

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

        // Act & Assert
        Assert.Throws<DivideByZeroException>(() => calculator.Divide(10, 0));
    }

    [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
    }
}

ここまでで以下のような構成になります。

Visual Studioの「テスト」メニュー →「テストエクスプローラー」を開きます。

「すべてのテストを実行」(Ctrl+R, A)をクリックすると、テストが実行されます。

すべてのテストが緑色になることを確認しましょう。

プロ美

4つのテストが全部緑になった!

プロ太

ボタン一つで全テストを実行できます。コードを修正したら、すぐにテストを実行する習慣をつけられるとよいですね。

Gitによる版管理と組み合わせることで、安全にコード修正を行えます!

手順4:[Theory]でテストを発展させる

基本テストが書けたら、複数のパターンをテストしたくなりますよね。

[Theory][InlineData]を使うと、1つのメソッドで複数のデータをテストできます。

AddCalculateWithTaxのテストを発展させてみましょう。以下のテストをCalculatorTests.csに追加します。

...
[Theory]
[InlineData(2, 3, 5)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
[InlineData(100, 200, 300)]
public void Add_WithVariousInputs_ReturnsExpectedSum(
    int a, int b, int expected)
{
    // Arrange
    var taxRateLoader = new TaxRateLoader();
    var calculator = new Calculator(taxRateLoader);

    // Act
    var result = calculator.Add(a, b);

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

...

[Theory]
[InlineData(1000, 1100)]   // 1000円 → 1100円
[InlineData(500, 550)]     // 500円 → 550円
[InlineData(0, 0)]         // 0円 → 0円
public void CalculateWithTax_WithVariousPrices_ReturnsExpected(
    decimal price, decimal expected)
{
    // Arrange
    var taxRateLoader = new TaxRateLoader();
    var calculator = new Calculator(taxRateLoader);

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

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

テストを実行すると、テストエクスプローラーでは各[InlineData]が個別のテストとして表示されます。

プロ美

1つのメソッドで複数パターンをテストできるんだ!これは便利!

手順5:リグレッションを検出してみる

テストの威力を体感するため、わざとバグを仕込んでみましょう。Calculator.csAddメソッドを誤った実装にしてみましょう。

public int Add(int a, int b)
{
    return a - b;  // + を - に変更(バグ)
}

テストを実行すると…赤い×マークが表示され、テストが失敗します。

プロ太

このように、コードを壊してしまっても、テストがすぐに教えてくれます。これが「壊れたら気づける」仕組みです!
(確認できたら、コードを元に戻しておきましょう。)

演習の振り返りと次回予告

今回は、以下のようなテストを作成しました。

テスト対象外部依存
Add(足し算)なし
Divide(割り算)なし
CalculateWithTax(税込計算)ファイル(taxrate.txt)

AddDivideは外部依存がないため、テストが書きやすく、高速に実行できます。

一方、CalculateWithTaxtaxrate.txtを読み込むため、ファイルがないとテストが動きません

また、税率5%や20%など様々なケースをテストしたい場合、ファイルの内容を毎回変更する必要があり不便です。

プロ美

たしかに、ファイルに依存するテストは不便だね…。どうすればいいの?

プロ太

この問題を解決するのが「モック」というテクニックです。次回、詳しく解説します。

あわせて、初心者が最初に自動テストを導入する際に何からはじめればよいか?」についても説明します!

まとめ

今回は自動テストの基本を学びました。

  • テスト自動化の価値は「壊れたことを自動で検知できる」こと
  • xUnitは.NETで広く使われる汎用的なテストフレームワーク
  • テストとは「どのモジュールで、どのような動作を確認したいか」を決めて実施するもの
  • [Fact] で単一のテスト、[Theory][InlineData] でパラメータ化テスト
  • AAAパターン(Arrange-Act-Assert)でテストを構造化

次回は「モック」を使って、ファイルやDBなどの外部依存を切り離してテストする方法を学びます。これにより、より柔軟にテストを書けるようになりますよ!

プロ太

引き続き、一緒にC#プログラミングについて学んでいきましょう。

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