NUnit を用いた C# のユニットテスト入門

~ TaxCalculator をテストするチュートリアル ~

以下は、上記の TaxCalc アプリ(Windows Forms アプリケーション)のコードがすでに存在する前提で、NUnit を用いたユニットテスト入門資料の例です。


1. はじめに

ユニットテストは、プログラムの各部品(ユニット)が意図したとおりに動作するかどうかを自動で検証する手法です。
本資料では、Windows Forms を利用した TaxCalc アプリケーション(税抜価格から税込価格を計算する機能)を対象に、NUnit を使ってテストを実施する方法を解説します。
テストを導入することで、バグの早期発見やプログラム品質の向上、保守性の向上が期待できます。


2. TaxCalc アプリケーションの既存コードの概要

以下は、すでに存在する TaxCalc アプリケーションの一部コードです。
このアプリケーションは、ユーザーが TextBox に入力した税抜価格を元に、消費税(10%)を計算し、結果を別の TextBox に表示するシンプルな Windows Forms アプリです。

using System;
using System.Windows.Forms;

namespace TaxCalc
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void CalcButtonClicked(object sender, EventArgs e)
        {
            int price;
            bool success = int.TryParse(this.priceBox.Text, out price);

            if (success)
            {
                // 消費税を計算する
                int textPrice = (int)(price * 1.1);
                this.taxPriceBox.Text = textPrice.ToString();
            }
            else
            {
                // エラーメッセージの表示
                MessageBox.Show("税抜価格を正しく入力してください");
            }
        }
    }
}

※ 注意
このコードでは、UI コントロール(priceBox や taxPriceBox)がデザイナーで配置されています。
ユニットテストでは UI の操作は難しいため、計算ロジックそのもの(「税抜価格×1.1」の計算部分)を別クラスへ分離することが望ましいです。


3. ユニットテストの目的とメリット

  • バグの早期発見: 変更による影響範囲を素早く把握できます。
  • 品質保証: 各ユニットが正しく動作していることを自動で検証可能。
  • リファクタリングの安心感: ロジック変更時に既存の動作が保たれているか確認できます。

4. テスト対象のコードをテストしやすくするためのリファクタリング

UI 部分と計算ロジックが同じクラスにある場合、テストが難しくなります。
そこで、計算ロジックを別クラス(例えば、TaxCalculator クラス)に切り出す方法を推奨します。

例として、以下のように計算ロジックを分離します。

public class TaxCalculator
{
    private const double TaxRate = 1.1;

    public int CalculateTaxIncludedPrice(int price)
    {
        return (int)(price * TaxRate);
    }
}

そして、UI 側(Form1)の CalcButtonClicked 内では、この TaxCalculator クラスを利用するように変更します。
これにより、TaxCalculator の動作を単体でテストできるようになります。

以下は、計算ロジックを TaxCalculator クラスに分離した後の Form1 のコード例です。TaxCalculator クラスは別ファイルに配置することを想定しています。

using System;
using System.Windows.Forms;

namespace TaxCalc
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void CalcButtonClicked(object sender, EventArgs e)
        {
            if (int.TryParse(this.priceBox.Text, out int price))
            {
                TaxCalculator calculator = new TaxCalculator();
                int taxIncludedPrice = calculator.CalculateTaxIncludedPrice(price);
                this.taxPriceBox.Text = taxIncludedPrice.ToString();
            }
            else
            {
                MessageBox.Show("税抜価格を正しく入力してください");
            }
        }
    }
}

このように、Form1 内では TaxCalculator の CalculateTaxIncludedPrice メソッドを呼び出すだけにすることで、計算ロジックを独立してユニットテストできるようになります。


5. NUnit 環境の構築とプロジェクト設定

5.1 Visual Studio のインストール

  • Visual Studio Community(公式サイトからダウンロード)をインストールします。
  • インストール時に「.NET デスクトップ開発」を選択してください。

5.2 NUnit のインストール(今回のチュートリアルでは、自動インストールされます

TaxCalculator のプロジェクトに対して、NUnit と NUnit3TestAdapter を NuGet 経由でインストールします。

【手順】

  1. ソリューションエクスプローラーでプロジェクトを右クリック → [NuGet パッケージの管理]
  2. “NUnit” を検索し、NUnitNUnit3TestAdapter をインストール

または、パッケージマネージャーコンソールで以下を実行:

Install-Package NUnit
Install-Package NUnit3TestAdapter

6. テストプロジェクトの作成

  1. Visual Studioソリューションエクスプローラー を開く。
  2. ソリューションを右クリック → [追加] → [新しいプロジェクト] を選択
  3. [NUnit 3 テスト プロジェクトを選択し、TaxCalc.Testsとして作成
  4. テストプロジェクトをメインのプロジェクトに参照追加
    • テストプロジェクトを右クリック → [追加][プロジェクト参照] → メインプロジェクト(TaxCalculatorがあるプロジェクト)を選択

6.1 テストコードの記述

作成したテストプロジェクトに TaxCalculatorTests.cs という新しいクラスを作成し、以下のように記述します。

テストプロジェクト内に TaxCalculatorTests.cs を作成し、以下のテストコードを記述します。

using NUnit.Framework;

namespace TaxCalc.Tests
{
    [TestFixture] // テストクラスであることを示す
    public class TaxCalculatorTests
    {
        [Test] // テストメソッド
        public void CalculateTaxIncludedPrice_WithValidPrice_ReturnsCorrectValue()
        {
            // Arrange: テストの準備
            var taxCalculator = new TaxCalculator();
            int price = 100;
            int expectedTaxIncludedPrice = 110; // 100 * 1.1

            // Act: メソッドの実行
            int actualTaxIncludedPrice = taxCalculator.CalculateTaxIncludedPrice(price);

            // Assert: 結果の検証
            Assert.That(actualTaxIncludedPrice, Is.EqualTo(expectedTaxIncludedPrice));
        }

        [Test]
        public void CalculateTaxIncludedPrice_WithZeroPrice_ReturnsZero()
        {
            // Arrange
            var taxCalculator = new TaxCalculator();
            int price = 0;
            int expectedTaxIncludedPrice = 0;

            // Act
            int actualTaxIncludedPrice = taxCalculator.CalculateTaxIncludedPrice(price);

            // Assert
            Assert.That(actualTaxIncludedPrice, Is.EqualTo(expectedTaxIncludedPrice));
        }
    }
}

ポイント:

  • [TestFixture] でテストクラスであることを示し、各テストメソッドに [Test] 属性を付けます。
  • Assert.That(実際の値, Is.EqualTo(期待値)) を用いて、結果を検証します。

このコードは、C# のユニットテストフレームワークである NUnit を使って、TaxCalculator クラスの CalculateTaxIncludedPrice メソッドが正しく動作するかどうかを確認するテストコードです。初学者向けに各部分を詳しく解説します。


1. NUnit とテストクラスの設定

  • using NUnit.Framework; (global usingsで登録されている場合は不要)
    NUnit の機能(テスト用の属性やアサーションなど)を使用するための宣言です。
  • [TestFixture] 属性
    この属性は、そのクラスがテストクラスであることを示します。NUnit はこの属性が付いているクラス内のテストメソッドを実行します。
  • クラス名: TaxCalculatorTests
    テスト対象である TaxCalculator クラスのテストをまとめるためのクラスです。名前の通り、「TaxCalculator のテスト」という意味になります。

2. テストメソッドとその構造

テストメソッドは、実際に動作を検証するためのメソッドで、各メソッドには [Test] 属性 が付いています。これにより、NUnit はこのメソッドをテストとして認識し、実行します。

各テストメソッドは、一般的に「Arrange」「Act」「Assert」という3つのステップで構成されます。

Arrange(準備)

  • テスト対象となるオブジェクトの作成や、テストに必要な変数の設定を行います。
  • 例として、TaxCalculator のインスタンスを作成し、テストに使う price(価格)の値や、期待される結果 expectedTaxIncludedPrice を設定しています。

Act(実行)

  • 実際にテスト対象のメソッドを呼び出し、結果を取得します。
  • ここでは、taxCalculator.CalculateTaxIncludedPrice(price) を実行して、返ってくる値を actualTaxIncludedPrice に代入しています。

Assert(検証)

  • 取得した結果が、期待した結果と一致するかどうかを検証します。
  • Assert.That(actualTaxIncludedPrice, Is.EqualTo(expectedTaxIncludedPrice)); という形で、実際の値が期待値と等しいかチェックしています。もし値が異なれば、このテストは失敗と判断されます。

3. 各テストケースの詳細

テストケース1: CalculateTaxIncludedPrice_WithValidPrice_ReturnsCorrectValue

  • 目的: 正常な価格(この場合は 100)が与えられたとき、税込み価格が正しく計算されるかを確認する。
  • 内容:
    • 価格 price を 100 と設定。
    • 期待される税込み価格は 110(100 の 10% 増し)。
    • CalculateTaxIncludedPrice メソッドを呼び出し、結果が 110 になっているかをチェックする。

テストケース2: CalculateTaxIncludedPrice_WithZeroPrice_ReturnsZero

  • 目的: 価格が 0 の場合、税込み価格も 0 になるかを確認する。
  • 内容:
    • 価格 price を 0 と設定。
    • 期待される税込み価格は 0。
    • メソッド実行後、結果が 0 であることを確認する。

4. まとめ

このテストコードは、TaxCalculator クラスの CalculateTaxIncludedPrice メソッドが以下の点で正しく動作しているかを検証しています。

  • 正常な値(例えば 100)を入力した場合、税込み価格が正しく計算される(ここでは 10% の税が加算され、結果は 110)。
  • 特殊ケースとして、0 の値を入力した場合も正しく 0 を返す。

NUnit を使うことで、コードの変更があったときに自動でテストを実行し、機能が壊れていないかどうかを確認できるため、信頼性の高いソフトウェア開発が可能になります。

このようなテストコードを書く習慣は、プログラムの品質向上にとても重要です。


6.2 プロジェクトファイルの変更

ソリューションエクスプローラーで、テストプロジェクトをダブルクリックし、プロジェクトファイルを開きます

変更前

<PropertyGroup>
  <TargetFramework>net8.0</TargetFramework>
  <ImplicitUsings>enable</ImplicitUsings>
  <Nullable>enable</Nullable>
  <IsPackable>false</IsPackable>
  <IsTestProject>true</IsTestProject>
</PropertyGroup>

変更

<PropertyGroup>
  <TargetFramework>net48</TargetFramework>
  <LangVersion>10.0</LangVersion>
  <ImplicitUsings>enable</ImplicitUsings>
  <Nullable>enable</Nullable>
  <IsPackable>false</IsPackable>
  <IsTestProject>true</IsTestProject>
</PropertyGroup>

変更理由

  • TargetFramework の変更(net8.0 から net48 へ)
    税計算ロジックのテストでは、Visual Studio のテストエクスプローラーや NUnit3TestAdapter の互換性を確保するために、安定した .NET Framework バージョン(net48)をターゲットとしています。
    .NET Framework 4.8 は、テスト実行時の環境との整合性が高く、特に既存のライブラリやツールとの相性が良いため、この変更を行っています。
  • LangVersion の追加(10.0)
    言語機能として最新の C# の構文や機能を利用できるようにするため、LangVersion を明示的に 10.0 に設定しました。これにより、コードの記述や今後の拡張において最新の機能が利用できるようになり、保守性や可読性が向上します。

7. テストを実行

ここまでの準備が完了したら、実際にテストを実行してみましょう。

7.1 ソリューションをビルド

一度、ソリューションをビルドし、エラーがないことを確認します

7.2 Visual Studio でテストを実行

  1. [テスト] メニュー → [テスト エクスプローラー] を開く
  2. テストエクスプローラーで “TaxCalc.Tests" が表示されていることを確認
  3. [すべて実行] をクリック
  4. 緑のチェックマークが表示されれば成功!
  • 失敗した場合は、テストエクスプローラーでエラー内容を確認し、バグを修正する。

8. まとめ

このチュートリアルでは、C# の NUnit を使って 税計算ロジックのユニットテスト を作成し、テストを実行しました。

学んだこと

  1. NUnit の環境構築(NuGet で NUnit をインストール)
  2. TaxCalculator クラスの作成
  3. NUnit のテストコードの書き方
    • [TestFixture] … テストクラス
    • [Test] … テストメソッド
    • Assert.That(実際の値, Is.EqualTo(期待値)) … 結果の検証
  4. Visual Studio で テストを実行する方法

9. 1つのテストメソッドで複数のパラメータでテストしたい

以下は、1つのテストメソッドで複数のパラメータテストを実施する方法の一例です。NUnit の [TestCase] 属性を使うと、異なる入力値と期待値を簡潔に指定できます。

using NUnit.Framework;

namespace TaxCalc.Tests
{
    [TestFixture]
    public class TaxCalculatorTests
    {
        // 複数のテストケースを [TestCase] 属性で定義することで、
        // 1 つのメソッドでさまざまなパラメータでテストを実行できます。
        [TestCase(100, 110)] // 100 の場合、110 が期待される(100 * 1.1 = 110)
        [TestCase(0, 0)]     // 0 の場合、0 が期待される
        [TestCase(99, 108)]  // 99 の場合、99 * 1.1 = 108.9 だが、キャストにより 108 になる
        public void CalculateTaxIncludedPrice_WithVariousPrices_ReturnsCorrectValue(int price, int expectedTaxIncludedPrice)
        {
            // Arrange
            var taxCalculator = new TaxCalculator();

            // Act
            int actualTaxIncludedPrice = taxCalculator.CalculateTaxIncludedPrice(price);

            // Assert
            Assert.That(actualTaxIncludedPrice, Is.EqualTo(expectedTaxIncludedPrice));
        }
    }
}

説明

  • [TestCase] 属性
    各テストケースに対して、入力値(price)と期待される結果(expectedTaxIncludedPrice)を指定します。
  • 単一のテストメソッドで複数のパラメータをテスト
    この方法により、同じテストロジックを複数の条件で実行でき、コードの重複を避けることができます。
  • テスト実行時
    NUnit は各 TestCase 属性ごとにテストを個別に実行し、それぞれの結果を検証します。

これで、1つのテストメソッドで複数のパラメータテストを簡潔に実装できるようになります。

10. 次のステップ

これで NUnit の基本が理解できました!
今後は、次のような応用的なテストを書いてみましょう。

テストケースを増やす

  • CalculateTaxIncludedPrice(-1) の場合、どうなるべきか?
  • 端数が出る場合(例: CalculateTaxIncludedPrice(99))はどうなるか?

パラメータ化テスト

  • 複数の入力値をまとめてテストする [TestCase] を使う。

例外のテスト

  • 不正な入力時に例外が発生するかどうかAssert.Throws<ExceptionType>() で確認。

NUnit を活用して、より信頼性の高いプログラムを書いていきましょう! 🚀

例外のテストでは、予期しない入力や状態の場合に、メソッドが適切な例外を投げるかどうかを検証します。NUnit では、たとえば以下のように Assert.Throws() を用いることで、特定の例外が発生することを確認できます。

以下は、仮に TaxCalculator クラスの CalculateTaxIncludedPrice メソッドに不正な入力(例えば負の値)が渡された場合、ArgumentException をスローする実装を追加したと仮定した例です。

public class TaxCalculator
{
    private const double TaxRate = 1.1;

    public int CalculateTaxIncludedPrice(int price)
    {
        if (price < 0)
            throw new ArgumentException("負の値は許容されません", nameof(price));
        return (int)(price * TaxRate);
    }
}

この場合、例外が正しく発生するかどうかのテストは以下のように記述できます。

using NUnit.Framework;
using System;

namespace TaxCalc.Tests
{
    [TestFixture]
    public class TaxCalculatorTests
    {
        [Test]
        public void CalculateTaxIncludedPrice_WithNegativePrice_ThrowsArgumentException()
        {
            // Arrange
            var taxCalculator = new TaxCalculator();

            // Act & Assert:
            // 負の値を渡すことで、ArgumentException がスローされることを確認
            Assert.Throws<ArgumentException>(() => taxCalculator.CalculateTaxIncludedPrice(-1));
        }
    }
}

ポイントは以下の通りです:

  • Assert.Throws<T>() の利用:
    このメソッドは、指定したラムダ式内の処理が T 型の例外を投げるかどうかを検証します。例外が投げられなかった場合、テストは失敗します。
  • 例外メッセージの確認:
    必要に応じて、例外オブジェクトを変数に格納して、メッセージやその他のプロパティをさらに検証することもできます。
var ex = Assert.Throws<ArgumentException>(() => taxCalculator.CalculateTaxIncludedPrice(-1));
Assert.That(ex.Message, Is.EqualTo("負の値は許容されません\r\nパラメーター名:price));

このように例外テストを行うことで、不正な入力に対してプログラムが安全に動作し、期待通りのエラーハンドリングが実装されていることを確認できます。

補足

以下は、TaxCalculator クラス(負の値の場合に ArgumentException をスローする実装)を利用し、例外処理を加えた Form1 クラスのサンプルコードです。

using System;
using System.Windows.Forms;

namespace TaxCalc
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void CalcButtonClicked(object sender, EventArgs e)
        {
            if (int.TryParse(this.priceBox.Text, out int price))
            {
                try
                {
                    TaxCalculator calculator = new TaxCalculator();
                    int taxIncludedPrice = calculator.CalculateTaxIncludedPrice(price);
                    this.taxPriceBox.Text = taxIncludedPrice.ToString();
                }
                catch (ArgumentException ex)
                {
                    // TaxCalculator で負の値が渡された場合、例外が発生するのでメッセージを表示
                    MessageBox.Show(ex.Message);
                }
            }
            else
            {
                MessageBox.Show("税抜価格を正しく入力してください");
            }
        }
    }
}

このコードでは、

  • 入力値の検証: priceBox のテキストを整数に変換できるかチェックします。
  • 計算処理: 変換が成功した場合、TaxCalculator の CalculateTaxIncludedPrice を呼び出し、結果を taxPriceBox に表示します。
  • 例外処理: 負の値が入力された場合、TaxCalculator で ArgumentException がスローされるため、try-catch ブロックで捕捉し、エラーメッセージを表示します。

参考

C#,テスト

Posted by hidepon