C#のテストフレームワーク (MsTest・NUnit・xUnit) の ClassCleanup における挙動纏め

# 概要
MsTest v2 でユニットテストを実装していた際に、テストクラスの終端処理 (ClassCleanup) の発生タイミングが気になりました。
他のC#テストフレームワークも含めてどのような挙動となっているか、合わせて纏めます。

# 更新履歴
- 2023/03/18：[MsTest](#mstest) に関する記載を更新しました

# 環境
- Windows10
- Visual Studio 2022

# 想定するユニットテストのシナリオ
- UnitTest1 ～ UnitTest3 の 3クラスに、それぞれ 3つ のテストメソッド (合計 3 × 3 = 9テスト) が存在する。
- 各テストケースでは、クラス単位での初期化・終端処理、及びメソッド単位での初期化・終端処理が存在する。
    - データベース等、共有資源を使用しているような状態で、テスト毎に準備を行っているとイメージしていただけると良いかと思います。
- 共有資源を使用しているという上記理由から、テストは直列で実施したい。

# コードサンプル
使用したコードは、下記Githubを参照ください。
https://github.com/tYoshiyuki/dotnet-unit-test-sample

# 実行方法
- Visual Studio の テストエクスプローラ より GUI操作で実行します。
- デバッグ実行し、コンソールに出力される文字列からテストの実行順番をトレースします。

# NUnit
まずは、NUnit から挙動を見ていきます。
テストケースの実装は下記の通りです。

```c#
using System.Diagnostics;
using NUnit.Framework;

namespace UnitTestSample.NUnit
{
    public class UnitTest1
    {
        [OneTimeSetUp]
        public void ClassInitialize()
        {
            Trace.WriteLine("UnitTest1 ClassInitialize");
        }

        [SetUp]
        public void TestInitialize()
        {
            Trace.WriteLine("UnitTest1 TestInitialize");
        }

        [TearDown]
        public void TestCleanup()
        {
            Trace.WriteLine("UnitTest1 TestCleanup");
        }

        [OneTimeTearDown]
        public static void ClassCleanup()
        {
            Trace.WriteLine("UnitTest1 ClassCleanup");
        }

        [Test]
        public void TestMethod1()
        {
            Trace.WriteLine("UnitTest1 TestMethod1");
        }

        [Test]
        public void TestMethod2()
        {
            Trace.WriteLine("UnitTest1 TestMethod2");
        }

        [Test]
        public void TestMethod3()
        {
            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と実装内容が変わっています。

```c#
using System;
using System.Diagnostics;
using Xunit;

namespace UnitTestSample.XUnit
{
    [Collection("Test Collection #1")]
    public class UnitTest1 : IClassFixture<SampleClassFixture>, IDisposable
    {
        public UnitTest1()
        {
            Trace.WriteLine("UnitTest1 TestInitialize");
        }

        public void Dispose()
        {
            Trace.WriteLine("UnitTest1 TestCleanup");
        }

        [Fact]
        public void TestMethod1()
        {
            Trace.WriteLine("UnitTest1 TestMethod1");
        }

        [Fact]
        public void TestMethod2()
        {
            Trace.WriteLine("UnitTest1 TestMethod2");
        }

        [Fact]
        public void TestMethod3()
        {
            Trace.WriteLine("UnitTest1 TestMethod3");
        }
    }
}
```

```c#:SampleClassFixture.cs
using System;
using System.Diagnostics;

namespace UnitTestSample.XUnit
{
    public class SampleClassFixture : IDisposable
    {
        public SampleClassFixture()
        {
            Trace.WriteLine("ClassInitialize");
        }

        public void Dispose()
        {
            Trace.WriteLine("ClassCleanup");
        }
    }
}
```

まず、クラス単位の初期化・終端処理のために SampleClassFixture というクラスを作成しています。
テストケースに IClassFixture<SampleClassFixture> を実装することで、実現が可能です。
更に、メソッド単位の初期化処理はコンストラクタ、終端処理は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 になります。
(※ バージョン 2.2.8 にてアップデートがあり、NUnit・xUnitと同様の挙動に出来るようになりました！神アップデート!!:tada:)

アセンブリ属性 `ClassCleanupExecution(ClassCleanupBehavior.EndOfClass)` にて、`ClassCleanup` の動作タイミングをクラス単位に変更します。

```c#
// NOTE ClassCleanup の 動作タイミングをアセンブリ単位 (全テスト終了時) で無く、クラス単位に変更する。
[assembly: ClassCleanupExecution(ClassCleanupBehavior.EndOfClass)]
```

実装内容は下記の通りです。

```c#
using System.Diagnostics;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTestSample.MsTest
{
    [TestClass]
    public class UnitTest1
    {
        [ClassInitialize]
        public static void ClassInitialize(TestContext testContext)
        {
            Trace.WriteLine("UnitTest1 ClassInitialize");
        }

        [TestInitialize]
        public void TestInitialize()
        {
            Trace.WriteLine("UnitTest1 TestInitialize");
        }

        [TestCleanup]
        public void TestCleanup()
        {
            Trace.WriteLine("UnitTest1 TestCleanup");
        }

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

        [TestMethod]
        public void TestMethod1()
        {
            Trace.WriteLine("UnitTest1 TestMethod1");
        }

        [TestMethod]
        public void TestMethod2()
        {
            Trace.WriteLine("UnitTest1 TestMethod2");
        }

        [TestMethod]
        public void TestMethod3()
        {
            Trace.WriteLine("UnitTest1 TestMethod3");
        }

    }
}
```

ClassInitialize・ClassCleanupでクラス単位の初期化・終端処理、
TestInitialize・TestCleanupでメソッド単位の初期化・終端処理を行います。

実行結果は、以下の通りです。

```
ClassInitialize
UnitTest1 TestInitialize
UnitTest1 TestMethod1
UnitTest1 TestCleanup
UnitTest1 TestInitialize
UnitTest1 TestMethod2
UnitTest1 TestCleanup
UnitTest1 TestInitialize
UnitTest1 TestMethod3
UnitTest1 TestCleanup
ClassCleanup

ClassInitialize
UnitTest2 TestInitialize
UnitTest2 TestMethod1
UnitTest2 TestCleanup
UnitTest2 TestInitialize
UnitTest2 TestMethod2
UnitTest2 TestCleanup
UnitTest2 TestInitialize
UnitTest2 TestMethod3
UnitTest2 TestCleanup
ClassCleanup

ClassInitialize
UnitTest3 TestInitialize
UnitTest3 TestMethod1
UnitTest3 TestCleanup
UnitTest3 TestInitialize
UnitTest3 TestMethod2
UnitTest3 TestCleanup
UnitTest3 TestInitialize
UnitTest3 TestMethod3
UnitTest3 TestCleanup
ClassCleanup
```

~~ClassCleanupが最後に纏めて実行されています。~~
~~MsTestの場合、ClassCleanupの実行タイミングは全テストの完了時になっているようです。~~

~~従って、テストケース単位でClassCleanupを実行しようとしても、~~
~~実際には、全てのテスト完了時に動作し、各テスト開始時には動作しないため注意が必要です。~~



# 所感
MsTestの更新が嬉しい！
ライブラリのアップデートは、まめにチェックしないといけませんね。

~~本件については、MsTestのissuesに挙がっていました。~~
~~https://github.com/microsoft/testfx/issues/580~~

~~個人的には、NUnit や xUnit の挙動が直感的な気がしますので、対応が進むと良いなぁと感じています。~~
~~そもそも、相互で干渉するようなユニットテストを書かないのが吉・・・という説もあるかも知れませんね。~~