テスト

単体テストの作成

対象とするクラスにTestClass属性を、メソッドにTestMethod属性を指定します。

public class MyClass
{
    static public int Method()
    {
        return 0;
    }
}

[TestClass]
public class UnitTest
{
    [TestMethod]
    public void TestMethod1
    {
        int a = MyClass.Method();

        Assert.AreEqual(1, a);
        // Assert.AreEqual に失敗しました。<1> が必要ですが、<0> が指定されました。
    }
}

関連するクラス

クラス テスト対象
Assert 一般
CollectionAssert コレクション
StringAssert 文字列
AssertFailedException 例外
AssertInconclusiveException  
UnitTestAssertException  
Microsoft.VisualStudio.TestTools.UnitTesting Namespace | Microsoft Learn 単体テスト フレームワーク | Microsoft Learn

関連する属性

属性 意味
TestClassAttribute このクラスは、テスト対象である
TestMethodAttribute このメソッドは、テスト対象である
ExpectedExceptionAttribute このテストから例外が投げられることが期待される
IgnoreAttribute このテストは一時的に無視される
TimeoutAttribute タイムアウト期間がある
 
テストの識別および並べ替えのための属性 - 単体テスト フレームワーク | Microsoft Learn

TestMethodAttribute

テスト対象とするメソッドは、以下の条件を満たす必要があります。

  • TestMethodAttributeの属性で印づけられている
  • staticではない
  • publicである
  • 戻り値はvoid
  • 引数を取らない

この条件を満たすメソッドは、次のような形式となります。

[TestMethod]
public void TestMethod()
{
}

このときメソッドがasyncでマークされていると、.NETのバージョンによってはテスト対象と認識されないことがあります。Asynchronous unit tests with .NET 4.0 - Stack Overflow

この場合はテストメソッドからTaskを返すようにするか、asyncを用いず同期で処理します。

[TestMethod]
public void TestMethod()
{
    Task.Delay(1).Wait();
}

ただしテストのスレッドで待機させると問題がある場合には、この方法は使えません。

TimeoutAttribute

たとえば1000ミリ秒以内に完了しないときは失敗とするならば、次のように指定します。

[TestMethod, Timeout(1000)]
public void TestMethod()
{
}

Assertクラス

一般的なオブジェクトをテストできます。

検証内容 メソッド
肯定 否定
オブジェクトが等しいか AreEqual() AreNotEqual()
オブジェクトが同一か AreSame() AreNotSame()
指定の型であるか IsInstanceOfType() IsNotInstanceOfType()
trueであるか IsTrue() IsFlase()
nullであるか IsNull() IsNotNull()
Methods - Assert Class (Microsoft.VisualStudio.TestTools.UnitTesting) | Microsoft Learn

このAssertクラスは、DebugクラスのAssert()メソッドとは異なります。

コレクションの検証にはCollectionAssert、文字列の特殊な検証にはStringAssertを用います。

メソッド 機能
Fail() AssertFailedExceptionを投げる
Inconclusive() AssertInconclusiveExceptionを投げる
ReplaceNullChars() '\0'を"\0"に置き換えられる (V1 frameworkとの互換性を維持するためにある)

AreEqual()

Assert.AreEqualにあるように、多数のオーバーロードがあります。

2つのオブジェクトが、等しいことを検証

public static void AreEqual(
    Object expected, // テストで予期するオブジェクト
    Object actual    // テストで生成したオブジェクト
)
Assert.AreEqual メソッド (Object, Object) (Microsoft.VisualStudio.TestTools.UnitTesting) | MSDN

浮動小数点数は演算時に丸めによって誤差が生じるため、精度を指定できるAreEqual(Double, Double, Double)の形式で検証します。

2つのジェネリック型データが、等しいことを検証

public static void AreEqual<T>(
    T expected, // テストで予期するジェネリック型データ
    T actual    // テストで生成したジェネリック型データ
)
Assert.AreEqual(T) メソッド (T, T) (Microsoft.VisualStudio.TestTools.UnitTesting) | MSDN

2つの倍精度浮動小数点数が、指定された精度内にあることを検証

public static void AreEqual(
    double expected, // テストで予期する値
    double actual,   // テストで生成した値
    double delta     // 精度
)
Assert.AreEqual メソッド (Double, Double, Double) (Microsoft.VisualStudio.TestTools.UnitTesting) | MSDN

2つの文字列が、等しいことを検証

public static void AreEqual(
    string expected, // テストで予期する値
    string actual,   // テストで生成した値
    bool ignoreCase  // trueならば、大文字/小文字を区別しない
)
Assert.AreEqual メソッド (String, String, Boolean) (Microsoft.VisualStudio.TestTools.UnitTesting) | MSDN

第3引数を指定しないとobjectとして比較されるため、ZERO WIDTH NO-BREAK SPACEのような特殊文字を含むときに異なる結果となります。

このオーバーロードではカルチャに依存した結果となるため、非依存としたければ第3引数を指定せずobjectとして比較します。

string str1 = new String(new char[] { 'A' });
string str2 = new String(new char[] { 'A', '\uFEFF' }); // U+FEFF (ZERO WIDTH NO-BREAK SPACE)

string expected = "A";
Assert.AreEqual(expected, str1, false); // 成功
Assert.AreEqual(expected, str2, false); // 成功 (特殊文字が無視されている)

Assert.AreEqual(expected, str1); // 成功
Assert.AreEqual(expected, str2); // Assert.AreEqual に失敗しました。<A> が必要ですが、<A> が指定されました。

これはStringの既定の比較方法である、StringComparison.Ordinalと同じ結果です。

bool r1 = string.Equals(expected, str2, StringComparison.InvariantCulture); // true
bool r2 = string.Equals(expected, str2, StringComparison.Ordinal); // false

カルチャを指定しないオーバーロードではインバリアント カルチャが使用され、次のように指定するのと同じです。

Assert.AreEqual(expected, str2, false, CultureInfo.InvariantCulture);

CollectionAssertクラス

コレクションをテストできます。

検証内容 メソッド
肯定 否定
同一であるか (同じ値の要素が同じ順番で同じ数ある) AreEqual() AreNotEqual()
等価であるか (同じ値の要素が同じ数ある) AreEquivalent() AreNotEquivalent()
指定の要素を含むか Contains() DoesNotContain()
別のコレクションのサブセット (部分集合) であるか IsSubsetOf() IsNotSubsetOf()
検証内容 メソッド
すべての要素が指定の型であるか AllItemsAreInstancesOfType()
すべての要素がnullでない AllItemsAreNotNull()
すべての要素が一意であるか (同一の要素がない) AllItemsAreUnique()
Methods - CollectionAssert Class (Microsoft.VisualStudio.TestTools.UnitTesting) | Microsoft Learn

AreEqual()

public static void AreEqual (
    System.Collections.ICollection expected,
    System.Collections.ICollection actual
    );
AreEqual(ICollection, ICollection) - CollectionAssert.AreEqual Method (Microsoft.VisualStudio.TestTools.UnitTesting) | Microsoft Learn
int[] actual = Method();

int[] expected = new int[]{ 1, 2, 3 };
CollectionAssert.AreEqual(expected, actual);

ICollectionを実装していれば良いため、Dictionary<TKey,TValue>なども対象とできます。

Dictionary<int, string> expected = new Dictionary<int, string>
{
    [0] = "a",
    [3] = "b",
    [5] = "c",
};

CollectionAssert.AreEqual(expected, actual);

複雑なコレクションは、IComparerを引数に取るメソッドで検証します。

public static void AreEqual (
    System.Collections.ICollection expected,
    System.Collections.ICollection actual,
    System.Collections.IComparer comparer
    );

この場合には比較用に、IComparerを実装したクラスを用意します。

public class MyComparer : IComparer
{
    int IComparer.Compare(Object x, Object y)
    {
        return (x == y) ? 0 : 1;
    }
}

AllItemsAreNotNull()

コレクションにnullの要素があるか、コレクションがnullのときに失敗します。

object[] c1 = new[] { new object(), new object() };
CollectionAssert.AllItemsAreNotNull(c1); // ok

object[] c2 = new[] { new object(), null };
CollectionAssert.AllItemsAreNotNull(c2); // Collectionassert.AllItemsAreNotNull に失敗しました。

object[] c3 = null;
CollectionAssert.AllItemsAreNotNull(c3); // CollectionAssert.AllItemsAreNotNull に失敗しました。パラメーター 'collection' は無効です。値を null にすることはできません。。

StringAssertクラス

文字列をテストできます。

このクラスは多様な条件で文字列を評価するためのものであり、単純に等しいかどうかを検証するだけならば、AssertクラスのAreEqual()を用います。

検証方法 メソッド
肯定 否定
指定の正規表現に一致するか Matches() DoesNotMatch()
検証方法 メソッド
指定の文字列を含むか Contains()
指定の文字列で始まるか StartsWith()
指定の文字列で終わるか EndsWith()
メソッド - StringAssert クラス (Microsoft.VisualStudio.TestTools.UnitTesting) | MSDN

Contains()

public static void Contains (
    string value,    // substringを含むことが予期される文字列
    string substring // value内に現れることが予期される文字列
    );
Contains(String, String) - StringAssert.Contains Method (Microsoft.VisualStudio.TestTools.UnitTesting) | Microsoft Learn

valueにはテストで生成した値を、substringにはテストの結果として予期する値を指定します。これはAreEqual()などの引数とは順が異なるため注意が必要です。

StartsWith

public static void StartsWith (
    string value,    // substringで始まることが予期される文字列
    string substring // valueの接頭辞として予期される文字列
    );
StartsWith(String, String) - StringAssert.StartsWith Method (Microsoft.VisualStudio.TestTools.UnitTesting) | Microsoft Learn

ExpectedExceptionAttribute

例外の検証には、ExpectedExceptionAttribute属性に指定します。ExpectedExceptionAttribute クラス (Microsoft.VisualStudio.TestTools.UnitTesting) | MSDN

この属性が付けられたメソッドでは、指定の例外が投げられないとテストに失敗します。

[TestMethod]
[ExpectedException(typeof(System.Exception))]
public void TestMethod1()
{
}

たとえばこの場合は、「テスト メソッド TestMethod1 は例外をスローしませんでした。」「テスト メソッドで定義されている属性 ExpectedExceptionAttribute で例外が予期されていました。」として失敗します。

[TestMethod]
[ExpectedException(typeof(System.Exception))]
public void TestMethod2()
{
    throw new System.Exception();
}

一方で例外が投げられれば、テストに成功します。このとき例外はメソッドの外に投げられる必要があるため、メソッド内で捕捉しては失敗となります。

[TestMethod]
[ExpectedException(typeof(System.Exception))]
public void TestMethod3()
{
    try
    {
        throw new System.Exception();
    }
    catch (Exception) { }
}

また例外が投げられることを期待する処理は、テストメソッドの最後にします。さもなくばそれ以降の処理が評価されないまま、テストに成功してしまうことになります。

[TestMethod]
[ExpectedException(typeof(System.Exception))]
public void TestMethod2()
{
    throw new System.Exception();

    Assert.Fail(); // ここが評価されないまま、テストに成功する
}

指定の例外から派生している型も含めるならば、AllowDerivedTypesプロパティをtrueとします。ExpectedExceptionAttribute.AllowDerivedTypes Property (Microsoft.VisualStudio.TestTools.UnitTesting) | Microsoft Learn

[ExpectedException(typeof(System.Exception), AllowDerivedTypes = true)]

例外が投げられなかったことでテストが失敗したときに、テスト結果に含めるメッセージを指定できるオーバーロード、

public ExpectedExceptionAttribute (Type exceptionType, string noExceptionMessage);
ExpectedExceptionAttribute(Type, String) - ExpectedExceptionAttribute Constructor (Microsoft.VisualStudio.TestTools.UnitTesting) | Microsoft Learn

がありますが、そのメッセージの確認方法は不明です。c# - Where does the string in ExpectedException.NoExceptionMessage appear? - Stack Overflow

テスト対象の選択

IgnoreAttribute

Ignore属性をテストメソッドに追加することで、そのテストを一時的に無効にできます。

[TestMethod]
[Ignore]
public void TestMethod1()
{
}
IgnoreAttribute クラス (Microsoft.VisualStudio.TestTools.UnitTesting) | MSDN

無効化したテストを再び有効にするには、Ignore属性を削除するか、コメントアウトします。方法 : テストを無効または有効にする | MSDN

TestMethod属性を削除することでもテストを無効にできますが、それではテストの存在そのものが隠されてしまうため、Ignore属性を用いるようにします。

複数テストの有効・無効を一括して切り替えるには、#ifディレクティブでIgnore属性の指定を制御するか、TestCategory属性でテストをまとめます。c# - Ignore IgnoreAttribute - Stack Overflow

TestCategoryAttribute

テスト対象に共通の名前を付加することで、それらをグループ化して一括して選択できます。TestCategoryAttribute Class (Microsoft.VisualStudio.TestTools.UnitTesting) | Microsoft Learn

この属性はAttributeUsageでAttributeTargets.Methodが指定されており、メソッドだけに指定できます。

[TestMethod]
[TestCategory("CategoryName")]
public void TestMethod1()
{
}

この指定に対して、Visual StudioではTrait:"CategoryName"のようにフィルタを指定することで、CategoryNameだけのテストを抽出できます。

PriorityAttribute

テストに優先順位を付けることができます。PriorityAttribute Class (Microsoft.VisualStudio.TestTools.UnitTesting) | Microsoft Learn

これは順序指定テスト (ordered test) や、順位によるフィルタとして利用できます。What is PriorityAttribute Used for in MSTEST / Visual Studio - Stack Overflow

初期化と終了処理

下表の属性を付加することで、テストに共通して必要な処理を行うメソッドを定義できます。

属性 用途 適用回数 適用条件 実行時機
AssemblyInitializeAttribute アセンブリの初期化 アセンブリ内で1つだけ staticメソッドで、TestContextを引数にとる アセンブリのすべてのクラスの読み込み前
ClassInitializeAttribute クラスの初期化 クラス内で1つだけ クラスのすべてのテストメソッドの読み込み前 ※1
TestInitializeAttribute テスト対象の初期化 制限なし なし クラスのテストメソッドの読み込み前
TestCleanupAttribute テスト対象の終了処理 クラスのテストメソッドの実行後
ClassCleanupAttribute クラスの終了処理 クラス内で1つだけ staticメソッド クラスのすべてのテストメソッドの実行後 ※1
AssemblyCleanupAttribute アセンブリの終了処理 アセンブリ内で1つだけ アセンブリのすべてのクラスの実行後

※1 そのクラスのTestMethodが付加されているメソッドの実行前後にのみ呼ばれる。つまりTestMethodが付加されていないpublic staticなメソッドが他のクラスから実行されても、呼ばれない。

たとえば次のようにテスト対象のクラスが定義されているとき、

[TestClass]
public class UnitTest
{
    // 初期化
    [AssemblyInitialize]
    public static void AssemblyInit(TestContext context) { Debug.WriteLine("AssemblyInitialize"); }

    [ClassInitialize]
    public static void ClassInit(TestContext context) { Debug.WriteLine("ClassInitialize"); }

    [TestInitialize]
    public void TestInit() { Debug.WriteLine("TestInitialize"); }

    // 終了処理
    [TestCleanup]
    public void TestCleanup() { Debug.WriteLine("TestCleanup"); }

    [ClassCleanup]
    public static void ClassCleanup() { Debug.WriteLine("ClassCleanup"); }

    [AssemblyCleanup]
    public static void AssemblyCleanup() { Debug.WriteLine("AssemblyCleanup"); }

    // テスト
    [TestMethod]
    public void Test1() { Debug.WriteLine("TestMethod1"); }

    [TestMethod]
    public void Test2() { Debug.WriteLine("TestMethod2"); }
}

テスト対象のメソッドTest1()を実行すると、

  1. AssemblyInitialize
  2. ClassInitialize
  3. TestInitialize
  4. TestMethod1
  5. TestCleanup
  6. ClassCleanup
  7. AssemblyCleanup

の順で呼ばれます。またTest1()とTest2()を同時に実行したときには、

  1. TestInitialize
  2. TestMethod1
  3. TestCleanup
  4. ClassCleanup
  5. AssemblyCleanup
  1. AssemblyInitialize
  2. ClassInitialize
  3. TestInitialize
  4. TestMethod2
  5. TestCleanup

のように、アセンブリとクラスの処理はいずれかで1度だけ呼ばれます。

AssemblyInitializeAttribute

[AssemblyInitialize]
public static void AssemblyInit(TestContext context)
{
}
AssemblyInitializeAttribute Class (Microsoft.VisualStudio.TestTools.UnitTesting) | Microsoft Learn

この属性を付けたメソッドは、アセンブリ内で1つしか認められません。複数あると「UTA013: ***: 1 つのアセンブリ内で、AssemblyInitialize 属性を伴う 2 つ以上のメソッドを定義することはできません。」として実行時にエラーとなります。

テストの検出

テストから実行されたかどうか

アプリケーションを開始した実行可能ファイルのパスから、テストから実行されたことを検出できます。

if (Application.StartupPath.EndsWith("TESTWINDOW"))
{
    // テストから実行された
}
バージョン パス
Visual Studio 2015 C:\PROGRAM FILES (X86)\MICROSOFT VISUAL STUDIO 14.0\COMMON7\IDE\COMMONEXTENSIONS\MICROSOFT\TESTWINDOW
Visual Studio 2022 C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\Extensions\TestPlatform

[テストの実行]か[テストのデバッグ]か

Debugger.IsAttachedで、[テストのデバッグ]で実行されていることを検出できます。

トラブル対処法

Visual Studioでのトラブル対処法

テストの実行を取り消すことができない

テストの終了後にSystem.InvalidOperationExceptionが投げられ、「進行中のテストの実行がないので、テストの実行を取り消すことができません。(The cancelation of the test run is not possible as there is no test run which is in progress.)」と報告されることがあります。これはデバッグの設定で[マイ コードのみを有効にする]を有効にすることで解決できます。visual studio 2012 - Unit Test: The cancelation of the test run is not possible as there is no test run which is in progress - Stack Overflow

Formが表示されない

Visual Studioでテストをデバッグ実行したときにFormが表示されないときは、FormのShowInTaskbarをfalseに設定します。winforms - Displaying Windows Forms inside unit test methods - Stack Overflow

Form form = new Form();

if (System.Diagnostics.Debugger.IsAttached)
{
    form.ShowInTaskbar = false;
}

form.ShowDialog();
Microsoft Learnから検索