この文書について
他のテストフレームワークを使った経験のある人が、はじめてNUnitを使うことになったときのための文書である。
NUnitの特徴
NUnitを使いはじめるまえに、他のテストフレームワークとは思想の異なる点がひとつ存在することを知っておくと役に立つ。
ここで私が言いたいのは、
- Assertの書き方を比較してこうした特徴があるよ
- テストメソッドごとの初期化、後始末のメソッドはこうやって書くよ
ということではない。
もっと基本的なテストフレームワークの動作、仕様についてである。
すなわち、NUnitは(デフォルトでは)、テストメソッドごとにテストクラスの新しいインスタンスを作成しない。言い換えると、NUnitは、そのテストクラスに含まれるすべてのテストメソッドを単一のインスタンスを使用して実行する。
実際のテストコード
上記のような特徴があるので、以下のテストは失敗する。
TestFixture1
は一度しかインスタンスが作成されないので、Test1
とTest2
とで後に実行されるメソッドではCount
の値が1にならない。
// For NUnit
public class TestFixture1
{
int Count { get; set; } = 0;
[Test]
public void Test1()
{
Count++;
Assert.That(Count, Is.EqualTo(1));
}
[Test]
public void Test2()
{
Count++;
Assert.That(Count, Is.EqualTo(1));
}
}
以下は同じことをMSTestでおこなったコードであるが、このテストは成功する。
MSTestはTestMethod1
, TestMethod2
を異なるインスタンスを使用して実行するのだ。
コードはあげないが、xUnit.netでも同様だ。
// For MSTest
[TestClass]
public class MSTest1
{
int Count { get; set; } = 0;
[TestMethod]
public void TestMethod1()
{
Count++;
Assert.AreEqual(1, Count);
}
[TestMethod]
public void TestMethod2()
{
Count++;
Assert.AreEqual(1, Count);
}
}
対応方法
NUnit3.12までは、インスタンス変数を初期化するため、NUnitユーザは逐一、
// For NUnit
public class TestFixture1
{
int Count { get; set; } = 0;
[SetUp]
public void Init() => Count = 0;
...
}
といったように、初期化のためのSetUpメソッドを書いていた(多分)。
SetUp
属性を持つメソッドは、そのクラスのすべてのテストメソッドの前に実行される。
NUnit3.13とそれ以降では、以下のように、テストメソッドごとにテストクラスの新しいインスタンスを作成するように設定できるようになった。
// For NUnit 3.13-
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public class TestFixture1
{
...
}
このFixtureLifeCycle
属性はアセンブリ単位で指定することもできる。
[assembly: FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
こうしておけば、もうMSTestやxUnit.netの動作と大差がないことになる。
この設定は、テストを並列実行したい場合にも有用なものになるだろう。
NUnit開発者はどう考えているか
この点は、他のテストフレームワークに習熟していればいるほど、NUnitを使う上ではまるポイントだと思う。
MSTestやxUnit.netはもちろん、JUnitなどとも異なる仕様だからだ。
まさにこの仕様について、初期バージョンの開発者のひとりであるJames Newkirk氏が「最大の失敗のひとつ(one of the biggest screw-up)」「私の誤り(I think this one was my fault.)」と語っている。
https://learn.microsoft.com/ja-jp/archive/blogs/jamesnewkirk/why-variables-in-nunit-testfixture-classes-should-be-static
氏はxUnit.netのプロジェクトの立ち上げにも関係しているが、そこでは「Single Object Instance per Test Method.」と宣言されている。
https://jamesnewkirk.typepad.com/posts/2007/09/announcing-xuni.html
JUnitはなぜテストクラスのインスタンスを使いまわさなかったのか
JUnitはテストメソッドごとにテストクラスの新しいインスタンスを作成する。
MSTestやxUnit.netが同様の動きをするのはその影響だ。
では、どうしてJUnitはテストクラスのインスタンスを使い回さなかったのだろう。
それは、テスト間がお互いに影響することのない状態、「分離」を実現するためだ。
詳しくはMartin Fowler氏のブログを読んで欲しい。
https://martinfowler.com/bliki/JunitNewInstance.html
この和訳が以下で読める。
https://bliki-ja.github.io/JunitNewInstance
参考文書
この文書のNUnitのFixtureLifeCycleについては、以下のドキュメントを参考にしている。
https://docs.nunit.org/articles/nunit/writing-tests/attributes/fixturelifecycle.html