概要
MsTest v2 でユニットテストを実装していた際に、テストクラスの終端処理 (ClassCleanup) の発生タイミングが気になりました。
他のC#テストフレームワークも含めてどのような挙動となっているか、合わせて纏めます。
環境
- Windows10
- Visual Studio 2019
想定するユニットテストのシナリオ
- UnitTest1 ~ UnitTest3 の 3クラスに、それぞれ 3つ のテストメソッド (合計 3 × 3 = 9テスト) が存在する。
- 各テストケースでは、クラス単位での初期化・終端処理、及びメソッド単位での初期化・終端処理が存在する。
- データベース等、共有資源を使用しているような状態で、テスト毎に準備を行っているとイメージしていただけると良いかと思います。
- 共有資源を使用しているという上記理由から、テストは直列で実施したい。
コードサンプル
使用したコードは、下記Githubを参照ください。
https://github.com/tYoshiyuki/dotnet-unit-test-sample
実行方法
- Visual Studio の テストエクスプローラ より GUI操作で実行します。
- デバッグ実行し、コンソールに出力される文字列からテストの実行順番をトレースします。
NUnit
まずは、NUnit から挙動を見ていきます。
テストケースの実装は下記の通りです。
usingSystem.Diagnostics;usingNUnit.Framework;namespaceUnitTestSample.NUnit{publicclassUnitTest1{[OneTimeSetUp]publicvoidClassInitialize(){Trace.WriteLine("UnitTest1 ClassInitialize");}[SetUp]publicvoidTestInitialize(){Trace.WriteLine("UnitTest1 TestInitialize");}[TearDown]publicvoidTestCleanup(){Trace.WriteLine("UnitTest1 TestCleanup");}[OneTimeTearDown]publicstaticvoidClassCleanup(){Trace.WriteLine("UnitTest1 ClassCleanup");}[Test]publicvoidTestMethod1(){Trace.WriteLine("UnitTest1 TestMethod1");}[Test]publicvoidTestMethod2(){Trace.WriteLine("UnitTest1 TestMethod2");}[Test]publicvoidTestMethod3(){Trace.WriteLine("UnitTest1 TestMethod3");}}}OneTimeSetUp・OneTimeTearDownでクラス単位の初期化・終端処理、
SetUp・TearDownでメソッド単位の初期化・終端処理を行います。
実行結果は、下記の通りです。(※) 分かり易く改行を加えて加工しています。
UnitTest1 -> UnitTest2 -> UnitTest3 と直列に実行されています。
また、ClassInitialize と ClassCleanup はそれぞれのテストケースの開始・終了時に実行されています。
UnitTest1 ClassInitialize
UnitTest1 TestInitialize
UnitTest1 TestMethod1
UnitTest1 TestCleanup
UnitTest1 TestInitialize
UnitTest1 TestMethod2
UnitTest1 TestCleanup
UnitTest1 TestInitialize
UnitTest1 TestMethod3
UnitTest1 TestCleanup
UnitTest1 ClassCleanup
UnitTest2 ClassInitialize
UnitTest2 TestInitialize
UnitTest2 TestMethod1
UnitTest2 TestCleanup
UnitTest2 TestInitialize
UnitTest2 TestMethod2
UnitTest2 TestCleanup
UnitTest2 TestInitialize
UnitTest2 TestMethod3
UnitTest2 TestCleanup
UnitTest2 ClassCleanup
UnitTest3 ClassInitialize
UnitTest3 TestInitialize
UnitTest3 TestMethod1
UnitTest3 TestCleanup
UnitTest3 TestInitialize
UnitTest3 TestMethod2
UnitTest3 TestCleanup
UnitTest3 TestInitialize
UnitTest3 TestMethod3
UnitTest3 TestCleanup
UnitTest3 ClassCleanup
xUnit
次に xUnit になります。
若干、NUnitと実装内容が変わっています。
usingSystem;usingSystem.Diagnostics;usingXunit;namespaceUnitTestSample.XUnit{[Collection("Test Collection #1")]publicclassUnitTest1:IClassFixture<SampleClassFixture>,IDisposable{publicUnitTest1(){Trace.WriteLine("UnitTest1 TestInitialize");}publicvoidDispose(){Trace.WriteLine("UnitTest1 TestCleanup");}[Fact]publicvoidTestMethod1(){Trace.WriteLine("UnitTest1 TestMethod1");}[Fact]publicvoidTestMethod2(){Trace.WriteLine("UnitTest1 TestMethod2");}[Fact]publicvoidTestMethod3(){Trace.WriteLine("UnitTest1 TestMethod3");}}}usingSystem;usingSystem.Diagnostics;namespaceUnitTestSample.XUnit{publicclassSampleClassFixture:IDisposable{publicSampleClassFixture(){Trace.WriteLine("ClassInitialize");}publicvoidDispose(){Trace.WriteLine("ClassCleanup");}}}まず、クラス単位の初期化・終端処理のために SampleClassFixture というクラスを作成しています。
テストケースに IClassFixture を実装することで、実現が可能です。
更に、メソッド単位の初期化処理はコンストラクタ、終端処理はDisposeで実行します。
[Collection("Test Collection #1")] のアノテーションは、テストケースを直列実行するために設定しています。
xUnitでは、デフォルトで各テストケースが並列実行されるため、その対策として設定しています。
実行結果は、下記の通りです。
NUnitと同じような結果になりました。
ClassInitialize
UnitTest1 TestInitialize
UnitTest1 TestMethod2
UnitTest1 TestCleanup
UnitTest1 TestInitialize
UnitTest1 TestMethod3
UnitTest1 TestCleanup
UnitTest1 TestInitialize
UnitTest1 TestMethod1
UnitTest1 TestCleanup
ClassCleanup
ClassInitialize
UnitTest2 TestInitialize
UnitTest2 TestMethod1
UnitTest2 TestCleanup
UnitTest2 TestInitialize
UnitTest3 TestMethod3
UnitTest2 TestCleanup
UnitTest2 TestInitialize
UnitTest2 TestMethod2
UnitTest2 TestCleanup
ClassCleanup
ClassInitialize
UnitTest3 TestInitialize
UnitTest3 TestMethod1
UnitTest3 TestCleanup
UnitTest3 TestInitialize
UnitTest3 TestMethod3
UnitTest3 TestCleanup
UnitTest3 TestInitialize
UnitTest3 TestMethod2
UnitTest3 TestCleanup
ClassCleanup
MsTest
最後に、MsTest になります。
まずは、実装内容を確認します。
usingSystem.Diagnostics;usingMicrosoft.VisualStudio.TestTools.UnitTesting;namespaceUnitTestSample.MsTest{[TestClass]publicclassUnitTest1{[ClassInitialize]publicstaticvoidClassInitialize(TestContexttestContext){Trace.WriteLine("UnitTest1 ClassInitialize");}[TestInitialize]publicvoidTestInitialize(){Trace.WriteLine("UnitTest1 TestInitialize");}[TestCleanup]publicvoidTestCleanup(){Trace.WriteLine("UnitTest1 TestCleanup");}[ClassCleanup]publicstaticvoidClassCleanup(){Trace.WriteLine("UnitTest1 ClassCleanup");}[TestMethod]publicvoidTestMethod1(){Trace.WriteLine("UnitTest1 TestMethod1");}[TestMethod]publicvoidTestMethod2(){Trace.WriteLine("UnitTest1 TestMethod2");}[TestMethod]publicvoidTestMethod3(){Trace.WriteLine("UnitTest1 TestMethod3");}}}ClassInitialize・ClassCleanupでクラス単位の初期化・終端処理、
TestInitialize・TestCleanupでメソッド単位の初期化・終端処理を行います。
さて、実行結果を見てみます。
UnitTest1 ClassInitialize
UnitTest1 TestInitialize
UnitTest1 TestMethod1
UnitTest1 TestCleanup
UnitTest1 TestInitialize
UnitTest1 TestMethod2
UnitTest1 TestCleanup
UnitTest1 TestInitialize
UnitTest1 TestMethod3
UnitTest1 TestCleanup
UnitTest2 ClassInitialize
UnitTest2 TestInitialize
UnitTest2 TestMethod1
UnitTest2 TestCleanup
UnitTest2 TestInitialize
UnitTest2 TestMethod2
UnitTest2 TestCleanup
UnitTest2 TestInitialize
UnitTest2 TestMethod3
UnitTest2 TestCleanup
UnitTest3 ClassInitialize
UnitTest3 TestInitialize
UnitTest3 TestMethod1
UnitTest3 TestCleanup
UnitTest3 TestInitialize
UnitTest3 TestMethod2
UnitTest3 TestCleanup
UnitTest3 TestInitialize
UnitTest3 TestMethod3
UnitTest3 TestCleanup
UnitTest1 ClassCleanup
UnitTest2 ClassCleanup
UnitTest3 ClassCleanup
ClassCleanupが最後に纏めて実行されています。
MsTestの場合、ClassCleanupの実行タイミングは全テストの完了時になっているようです。
従って、テストケース単位でClassCleanupを実行しようとしても、
実際には、全てのテスト完了時に動作し、各テスト開始時には動作しないため注意が必要です。
所感
本件については、MsTestのissuesに挙がっていました。
https://github.com/microsoft/testfx/issues/580
個人的には、NUnit や xUnit の挙動が直感的な気がしますので、対応が進むと良いなぁと感じています。
そもそも、相互で干渉するようなユニットテストを書かないのが吉・・・という説もあるかも知れませんね。