チュートリアル: マネージド コードの単体テストを作成し、実行する

この記事では、マネージド コード用の Microsoft 単体テスト フレームワークと Visual Studio テスト エクスプローラーを使用して一連の単体テストを作成、実行、およびカスタマイズする手順について説明します。 開発中の C# プロジェクトで作業を開始し、そのコードを実行するテストを作成し、テストを実行し、結果を調べます。 次に、プロジェクト コードを変更し、テストを再実行します。 これらの手順を実行する前に、これらのタスクの概念の概要を確認したい場合は、「単体テストの基本」を参照してください。

テストするプロジェクトを作成する

  1. Visual Studio を開きます。

  2. スタート ウィンドウで、 [新しいプロジェクトの作成] を選択します。

  3. .NET 用の C# [コンソール アプリ] プロジェクト テンプレートを検索して選択し、[次へ] をクリックします。

    Note

    [コンソール アプリ] テンプレートが表示されない場合は、 [新しいプロジェクトの作成] ウィンドウからそれをインストールすることができます。 **[お探しの情報が見つかりませんでしたか?]** メッセージで、 **[さらにツールと機能をインストールする]** リンクを選択します。 次に、Visual Studio インストーラーで、 [.NET デスクトップ開発] ワークロードを選択します。

  4. プロジェクトに「Bank」という名前を設定してから、[次へ] をクリックします。

    推奨されるターゲット フレームワーク、または [.NET 8] を選択し、[作成] を選択します。

    Bank プロジェクトが作成され、コード エディターに Program.cs ファイルが開いた状態でソリューション エクスプローラーが表示されます。

    Note

    エディターで Program.cs が開いていない場合は、ソリューション エクスプローラーのファイル Program.cs をダブルクリックして開きます。

  5. Program.cs のコンテンツを BankAccount クラスを定義する次の C# コードで置換します。

    using System;
    
    namespace BankAccountNS
    {
        /// <summary>
        /// Bank account demo class.
        /// </summary>
        public class BankAccount
        {
            private readonly string m_customerName;
            private double m_balance;
    
            private BankAccount() { }
    
            public BankAccount(string customerName, double balance)
            {
                m_customerName = customerName;
                m_balance = balance;
            }
    
            public string CustomerName
            {
                get { return m_customerName; }
            }
    
            public double Balance
            {
                get { return m_balance; }
            }
    
            public void Debit(double amount)
            {
                if (amount > m_balance)
                {
                    throw new ArgumentOutOfRangeException("amount");
                }
    
                if (amount < 0)
                {
                    throw new ArgumentOutOfRangeException("amount");
                }
    
                m_balance += amount; // intentionally incorrect code
            }
    
            public void Credit(double amount)
            {
                if (amount < 0)
                {
                    throw new ArgumentOutOfRangeException("amount");
                }
    
                m_balance += amount;
            }
    
            public static void Main()
            {
                BankAccount ba = new BankAccount("Mr. Bryan Walton", 11.99);
    
                ba.Credit(5.77);
                ba.Debit(11.22);
                Console.WriteLine("Current balance is ${0}", ba.Balance);
            }
        }
    }
    
  6. ソリューション エクスプローラーで右クリックして [名前の変更] を選択し、ファイルの名前を BankAccount.cs に変更します。

  7. [ビルド] メニューの [ソリューションのビルド]をクリックします (または Ctrl + SHIFT + B キーを押します)。

これでテストできるプロジェクトとメソッドが用意されました。 この記事のテストは Debit メソッドに焦点を当てています。 Debit メソッドは、口座から現金が引き出されるときに呼び出されます。

単体テスト プロジェクトを作成する

  1. [ファイル] メニューで [追加]>[新しいプロジェクト] の順に選択します。

    ヒント

    ソリューション エクスプローラーでソリューションを右クリックし、[追加]>[新しいプロジェクト] の順に選択することもできます。

  2. 検索ボックスに「テスト」と入力し、言語として C# を選択してから、.NET テンプレートの C# [MSTest 単体テスト プロジェクト] を選択し、[次へ] をクリックします。

    Note

    Visual Studio 2019 バージョン 16.9 では、MSTest プロジェクト テンプレートは単体テストプロジェクトです。

  3. プロジェクトに「BankTests」という名前を指定し、[次へ] をクリックします。

  4. 推奨されるターゲット フレームワーク、または [.NET 8] を選択し、[作成] を選択します。

    BankTests プロジェクトが Bank ソリューションに追加されます。

  5. BankTests プロジェクトで、Bank プロジェクトへの参照を追加します。

    ソリューション エクスプローラーで、BankTests プロジェクトの [依存関係] を選択し、右クリック メニューの [参照の追加] (または [プロジェクト参照の追加]) を選択します。

  6. [参照マネージャー] ダイアログ ボックスで、[プロジェクト] を展開し、[ソリューション] を選択し、[Bank] チェックボックスをオンにします。

  7. OK を選択します。

テスト クラスを作成する

BankAccount クラスを検証するテスト クラスを作成します。 プロジェクト テンプレートによって生成された UnitTest1.cs ファイルを使用できますが、ファイルとクラスにはよりわかりやすい名前を付けます。

ファイルとクラスの名前を変更する

  1. ファイルの名前を変更するには、ソリューション エクスプローラーで、BankTests プロジェクトの UnitTest1.cs ファイルを選択します。 右クリック メニューの [名前の変更] を選択し (または F2 キーを押し)、ファイルの名前を BankAccountTests.cs に変更します。

  2. クラスの名前を変更するには、コード エディター内で UnitTest1 にカーソルを合わせ、右クリックして、[名前の変更] を選択します (または F2 キーを押します)。 「BankAccountTests」と入力し、Enter を押します。

BankAccountTests.cs ファイルには次のコードが含まれるようになりました。

// The 'using' statement for Test Tools is in GlobalUsings.cs
// using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace BankTests
{
    [TestClass]
    public class BankAccountTests
    {
        [TestMethod]
        public void TestMethod1()
        {
        }
    }
}

using ステートメントを追加する

using ステートメントをクラスに追加し、完全修飾名を使用せずにテスト対象のプロジェクトに呼び出すことができるようにします。 クラス ファイルの先頭に次のように追加します。

using BankAccountNS;

テスト クラスの要件

テスト クラスの最小要件は次のとおりです。

  • テスト エクスプローラーで実行する単体テスト メソッドを含むすべてのクラスについて、[TestClass] 属性が必要です。

  • テスト エクスプローラーで認識する各テスト メソッドには、[TestMethod] 属性が必要です。

単体テスト プロジェクトで [TestClass] 属性がない別のクラスを使用することができます。また、テスト クラスで [TestMethod] 属性がない別のメソッドを使用することもできます。 こうした別のクラスやメソッドをテスト メソッドから呼び出すことができます。

最初のテスト メソッドを作成する

この手順では、BankAccount クラスの Debit メソッドの動作を検証する単体テスト メソッドを記述します。

確認する必要がある動作は少なくとも 3 つあります。

  • 引き落とし金額が残高を上回る場合、このメソッドは ArgumentOutOfRangeException をスローします。

  • 引き落とし金額が 0 未満の場合、このメソッドは ArgumentOutOfRangeException をスローします。

  • 引き落とし金額が有効な場合、このメソッドは口座残高から借方金額を減算します。

ヒント

このチュートリアルでは使用しないため、既定の TestMethod1 メソッドを削除できます。

テスト メソッドを作成するには

最初のテストでは、正しい金額 (つまり、口座残高未満かつ 0 を上回る金額) によって口座から正しい金額が引き出されることが確認されます。 次のメソッドを BankAccountTests クラスに追加します。

[TestMethod]
public void Debit_WithValidAmount_UpdatesBalance()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 4.55;
    double expected = 7.44;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    account.Debit(debitAmount);

    // Assert
    double actual = account.Balance;
    Assert.AreEqual(expected, actual, 0.001, "Account not debited correctly");
}

このメソッドは単純です。期首残高を含む新しい BankAccount オブジェクトを設定し、有効な金額を引き出します。 期末残高が予想どおりであることを確認するには、Assert.AreEqual メソッドを使用します。 Assert.AreEqualAssert.IsTrue などのメソッドは、単体テストで頻繁に使用されます。 単体テストの作成に関する概念について詳しくは、「テストを作成する」を参照してください。

テスト メソッドの要件

テスト メソッドは次の条件を満たしている必要があります。

  • [TestMethod] 属性で修飾されています。

  • void を返します。

  • パラメーターを含むことができません。

テストをビルドして実行する

  1. [ビルド] メニューの [ソリューションのビルド]を選択します (または Ctrl + Shift + B キーを押します)。

  2. テスト エクスプローラーが開いていない場合、それを開くには、上部のメニュー バーで [テスト]>[テスト エクスプローラー] (または [テスト]>[Windows]>[テスト エクスプローラー]) の順に選択します (または、Ctrl + ET キーの順に押します)。

  3. [すべて実行] を選択してテストを実行します (または Ctrl + RV キーを押します)。

    テストの実行中は、[テスト エクスプローラー] ウィンドウの上部にあるステータス バーがアニメーション化されます。 テストの実行の終了時に、すべてのテスト メソッドが成功した場合はステータス バーが緑色に変わり、いずれかのテストが失敗した場合は赤色に変わります。

    この場合は、テストが失敗します。

  4. テスト エクスプローラーでメソッドを選択すると、ウィンドウの下部に詳細が表示されます。

コードを修正してテストを再実行する

テスト結果には失敗を示すメッセージが含まれています。 このメッセージを表示するには、ドリルダウンが必要な場合があります。 AreEqual メソッドの場合、メッセージには、予想された内容と実際に受け取られた内容が表示されます。 残高が減少することを予想していましたが、代わりに引き出し額の分だけ増加していました。

単体テストにより、引き出し額は、減算する必要があるときに口座残高に追加されます。単体テストではバグがわかりました。

バグを修正する

エラーを修正するには、BankAccount.cs ファイルで次の行を

m_balance += amount;

次の内容に置き換えます。

m_balance -= amount;

テストを再実行する

テスト エクスプローラーで、[すべて実行] を選択してテストを再実行します (または Ctrl + RV キーを押します)。 赤/緑のバーが緑に変わり、テストに合格したことを示します。

Test Explorer in Visual Studio 2019 showing passed test

Test Explorer in Visual Studio 2019 showing passed test

単体テストを使用してコードを改良する

このセクションでは、分析の反復処理、単体テストの進展、およびリファクタリングが、実稼働のコードの堅牢性と有効性を高めるうえでどのように役立つかを説明します。

問題を分析する

有効な金額が Debit メソッドで正しく差し引かれているかどうかを確認するテスト メソッドを作成しました。 引き落とし金額が次のいずれかである場合、メソッドが ArgumentOutOfRangeException をスローすることを確認します。

  • 残高よりも大きい、または
  • 0 未満。

新しいテスト メソッドを作成し、実行する

引き落とし金額が 0 未満の場合の正しい動作を検証するテスト メソッドを作成します。

[TestMethod]
public void Debit_WhenAmountIsLessThanZero_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = -100.00;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act and assert
    Assert.ThrowsException<System.ArgumentOutOfRangeException>(() => account.Debit(debitAmount));
}

ThrowsException メソッドを使用して、正しい例外がスローされたことをアサートします。 このメソッドを使用した場合、ArgumentOutOfRangeException がスローされない限り、テストは失敗します。 引き落とし金額が 0 未満の場合、より汎用的な ApplicationException をスローするようにテスト対象のメソッドを一時的に変更すると、テストは正しく動作します。つまり失敗します。

引き出し金額が残高を上回るケースをテストするには、次の手順を実行します。

  1. Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRangeという新しいテスト メソッドを作成します。

  2. メソッド本体を Debit_WhenAmountIsLessThanZero_ShouldThrowArgumentOutOfRange から新しいメソッドにコピーします。

  3. debitAmount を、残高を上回る数値に設定します。

2 つのテストを実行し、合格することを確認します。

分析を継続する

テスト対象のメソッドは、さらに改善できます。 現在の実装では、テスト中にスローされた例外を招いた条件 (amount > m_balance または amount < 0) を把握する方法はありません。 メソッドのどこかで ArgumentOutOfRangeException がスローされたことしかわかりません。 メソッドでその引数が正しくサニティ チェックされていることを確信できるように、BankAccount.Debit 内の例外をスローさせた条件 (amount > m_balance または amount < 0) がわかる方がよいと考えます。

テスト対象のメソッド (BankAccount.Debit) をもう一度確認すると、引数の名前をパラメーターとして使用する ArgumentOutOfRangeException コンストラクターが両方の条件ステートメントで使用されていることがわかります。

throw new ArgumentOutOfRangeException("amount");

より豊富な情報を報告するコンストラクターがあります: ArgumentOutOfRangeException(String, Object, String) には、引数の名前、引数の値、およびユーザー定義メッセージが含まれます。 このコンストラクターを使用するようにテスト対象のメソッドをリファクタリングできます。 さらに、一般に公開されている型メンバーを使用して、エラーを指定できます。

テスト対象のコードをリファクタリングする

最初に、エラー メッセージの 2 つの定数をクラス スコープで定義します。 テスト対象のクラス、BankAccount に定義を配置します:

public const string DebitAmountExceedsBalanceMessage = "Debit amount exceeds balance";
public const string DebitAmountLessThanZeroMessage = "Debit amount is less than zero";

次に、Debit メソッドの 2 つの条件ステートメントを変更します。

if (amount > m_balance)
{
    throw new System.ArgumentOutOfRangeException("amount", amount, DebitAmountExceedsBalanceMessage);
}

if (amount < 0)
{
    throw new System.ArgumentOutOfRangeException("amount", amount, DebitAmountLessThanZeroMessage);
}

テスト メソッドをリファクタリングする

Assert.ThrowsException への呼び出しを削除して、テスト メソッドをリファクタリングします。 try/catch ブロックで Debit() への呼び出しをラップし、期待される特定の例外をキャッチして、それに関連したメッセージを確認します。 Microsoft.VisualStudio.TestTools.UnitTesting.StringAssert.Contains メソッドには、2 つの文字列を比較する機能があります。

Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange は次のようになります。

[TestMethod]
public void Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 20.0;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    try
    {
        account.Debit(debitAmount);
    }
    catch (System.ArgumentOutOfRangeException e)
    {
        // Assert
        StringAssert.Contains(e.Message, BankAccount.DebitAmountExceedsBalanceMessage);
    }
}

再テストする、書き換える、再分析する

現時点では、テスト メソッドで、必要なすべてのケースが処理されるわけではありません。 テスト対象のメソッドである Debit メソッドで、debitAmount が残高より大きい (または 0 未満) 場合に ArgumentOutOfRangeException をスローできなかった場合、テスト メソッドは成功します。 例外がスローされない場合にテスト メソッドを失敗させる必要があるため、このシナリオは適していません。

この結果は、テスト メソッドのバグです。 この問題を解決するには、テスト メソッドの最後に Assert.Fail アサートを追加して、例外がスローされないケースを処理するようにします。

テストを再実行すると、正しい例外がキャッチされた場合にテストが失敗したことが示されます。 catch ブロックは例外をキャッチしますが、メソッドは引き続き実行され、新しい Assert.Fail アサート時に失敗します。 この問題を解決するには、catch ブロックの StringAssert の後に return ステートメントを追加します。 テストを再実行して、この問題が解決したことを確認します。 Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange の最終バージョンは、次のようになります。

[TestMethod]
public void Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 20.0;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    try
    {
        account.Debit(debitAmount);
    }
    catch (System.ArgumentOutOfRangeException e)
    {
        // Assert
        StringAssert.Contains(e.Message, BankAccount.DebitAmountExceedsBalanceMessage);
        return;
    }

    Assert.Fail("The expected exception was not thrown.");
}

まとめ

テスト コードを改善することで、より堅牢で有益なテスト方法になりました。 ただし、もっと重要な点は、テスト対象のコードも改善されたことです。

ヒント

このチュートリアルでは、マネージド コード用の Microsoft 単体テスト フレームワークを使用します。 また、テスト エクスプローラー用のアダプターを備えたサード パーティの単体テスト フレームワークからテスト エクスプローラーを実行することもできます。 詳細については、「サードパーティ製の単体テスト フレームワークをインストールする」をご覧ください。

コマンド ラインからテストを実行する方法については、「VSTest.Console.exe のコマンド ライン オプション」を参照してください。