NUnit 2.6 で実装された Action 属性について
リリースされていたので目玉のひとつになりそうな Action 属性についてちょっと調べてみました。というか、公式ページの解説を抄訳しただけともいえますが。
すごく簡単なしくみです。 Action 属性を使うことで、これまで SetUp/TearDown/FixtureSetUp/ FixtureTearDown みたいな属性をつけた上でメソッドとして書いてきた準備とか後始末を、Action クラスにカプセル化して、属性ベースでテストに適用できるようになります。
テストの眼目は当然テストコードにあって、その準備や後始末については、テストの本質ではありません。ただ、どういったコンテキストかでのテストであるかということを示す情報として意味はありますから、完全に隠蔽するのも良くありません。そこで、準備や後始末自体が抽象化され、名前がついた状態で再利用できることはたしかに意味がありそうです。
前述の通り、これまでは SetUp など属性を特定のメソッドにつけて、その中で実際の準備をおこなっていました。メソッドは複数書けますからメソッド名という形でコンテキストに名前をつけて、テストの環境を示すことは確かに出来ます。また、そのメソッドから Global な共通処理メソッドを呼べば複数のテストで共有することも出来るには出来ます。ただ、どうやっても初期化と後片付けのコードが(意味としてセットになっていたとしても)分離してしまいます。よくやるのはSetupでトランザクションを開始して、TearDownでロールバックする、みたいなのでしょうか。これらはどのテストであってもセットで取り回したいのですが、SetUp/TearDown 方式ではセットで書くことを強制できません。そんなわけで、本来テストしたいコード以外の夾雑物が多くなればテストは読みにくくなってしまいますし、テストを書くために気をつけないと行けないことが増えるのもテストを書くことのハードルを上げることになります。
そんなわけで、準備と後片付けがカプセル化される仕組みは欲しかった機能です。「あるテストを走らせるための環境を宣言的に示す」ために、Action属性をテストに付与するという実装は確かにスマートに感じます。
サンプル
マニュアルには次のような例が載っていました。
[TestFixture, ResetServiceLocator] public class MyTests { [Test, CreateTestDatabase] public void Test1() { /* ... */ } [Test, CreateTestDatabase, AsAdministratorPrincipal] public void Test2() { /* ... */ } [Test, CreateTestDatabase, AsNamedPrincipal("charlie.poole")] public void Test3() { /* ... */ } [Test, AsGuestPrincipal] public void Test4() { /* ... */ } }
これはわかりやすい。このテストフィクスチャはResetServiceLocatorを行ってから、開始されます。
テスト1の前は、CreateTestDatabase が前提となるテストです。中野実装はともかく、ようするにデータベースが設定され居なければテストの意味がないことを宣言している。テスト2には AsAdministratorPrincipal とありますから管理者権限で実行されるというのもわかりますね。テスト3も4も前提条件が書かれていることが一目でわかります。
作り方は ITestAction を継承したアクションクラスを使います。
こんなの
public interface ITestAction { void BeforeTest(TestDetails details); void AfterTest(TestDetails details); ActionTargets Targets { get; } }
ActionTargets は、そのアクションがテストクラスに対するものなのか、テストメソッドに対するモノなのかを示すようですね。 ActionTargets.Suite が TestFixtureSetUp/TestFixtureTearDown で、ActionTargets.Test が SetUp/TearDown に対応している感じですね。ActionTargets.Default はどちらにも付与できます。挙動の違いは自分で実装せよってことですね。
で、呼ばれたときの状況を携えて TestDetails クラスが BeforeTest/AfterTest にやってくるので、準備メソッド、後片付けメソッドでそれぞれ利用するわけですね。
例としてこんなクラスがサンプルにあります。
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Assembly, AllowMultiple = true)] public class ConsoleActionAttribute : Attribute, ITestAction { private string _Message; public ConsoleActionAttribute(string message) { _Message = message; } public void BeforeTest(TestDetails details) { WriteToConsole("Before", details); } public void AfterTest(TestDetails details) { WriteToConsole("After", details); } public ActionTargets Targets { get { return ActionTargets.Test | ActionTargets.Suite; } } private void WriteToConsole(string eventMessage, TestDetails details) { Console.WriteLine("{0} {1}: {2}, from {3}.{4}.", eventMessage, details.IsSuite ? "Suite" : "Case", _Message, details.Fixture != null ? details.Fixture.GetType().Name : "{no fixture}", details.Method != null ? details.Method.Name : "{no method}"); } }
(なんかNUnitのページにあるサンプルそのままだとWriteToConsole メソッドの中にミスがあったのでビルド通るよう修正)
単純なテストをつくってみます。
[TestFixture] public class ActionOnTestTests { [Test] [ConsoleAction("Actionからこんにちは!(Testに付与)")] public void SimpleTest() { Console.WriteLine("テスト本体が実行されました"); } }
で、
Before Case: Actionからこんにちは!(Testに付与), from ActionOnTestTests.SimpleTest. テスト本体が実行されました After Case: Actionからこんにちは!(Testに付与), from ActionOnTestTests.SimpleTest.
こんなふうになります。上記は TestCase に属性をつけた場合です。
次に、同じ属性を TestFixture につけた場合は、少し挙動が違います。
[TestFixture] [ConsoleAction("Actionからこんにちは!(Suiteに付与)")] public class ActionOnSuiteTests { [Test] public void SimpleTest() { Console.WriteLine("テスト本体が実行されました"); } }
結果はこう。
Before Suite: Actionからこんにちは!(Suiteに付与), from ActionOnSuiteTests.{no method}. Before Case: Actionからこんにちは!(Suiteに付与), from ActionOnSuiteTests.SimpleTest. テスト本体が実行されました After Case: Actionからこんにちは!(Suiteに付与), from ActionOnSuiteTests.SimpleTest. After Suite: Actionからこんにちは!(Suiteに付与), from ActionOnSuiteTests.{no method}.
こんなふうに、TestFixture でも TestCase でも呼ばれるようになります。これは Action の
public ActionTargets Targets { get { return ActionTargets.Test | ActionTargets.Suite; } }
こう定義しているためです。Action 属性を各テストケースに付与しなくても毎回呼ばれるわけですね。なので、この場合、 BeforeTest/AfterTest で、TestFixutre から呼ばれた場合と TestCase から呼ばれた場合で処理を変える必要があります。全体を通して1回で良いものと、毎 TestCase ごとに初期化し直さないといけないものを、常にセットで扱うことが出来ます。
なお、これを
public ActionTargets Targets { get { return ActionTargets.Suite; } }
とすると、
Before Suite: SuiteOnlyActionからこんにちは!(Suiteに付与), from OnlySuiteActionOnSuiteTests.{no method}. テスト本体が実行されました After Suite: SuiteOnlyActionからこんにちは!(Suiteに付与), from OnlySuiteActionOnSuiteTests.{no method}.
TestFixture に含まれる全テストケースの開始前と終了後によばれます( TestFixtureSetUp/TestFixtureTearDown の挙動と同じ)
もうひとつ強力そうなのが、 Assemby に対して Action を定義できます。
using System; using NUnit.Framework; [assembly: ActionSampleProject.ConsoleAction("Actionからこんにちは!(Assemblyに付与)")] namespace ActionSampleProject { [TestFixture] public class AssemblyActionTest{ [Test] public void SimpleTest() { Console.WriteLine("テストが実行されました"); } } }
これがあると同じアセンブリのすべての TestCase にこの Action 属性を定義したものとおなじ挙動を示すようです。
Before Suite: Actionからこんにちは!(Assemblyに付与), from {no fixture}.{no method}. Before Case: Actionからこんにちは!(Assemblyに付与), from AssemblyActionTest.SimpleTest. テスト1が実行されました After Case: Actionからこんにちは!(Assemblyに付与), from AssemblyActionTest.SimpleTest. After Suite: Actionからこんにちは!(Assemblyに付与), from {no fixture}.{no method}.
静的プロパティなどの初期化によさそうだなとは思いますが、うーん、ちょっと不思議ですね。この場合は、 ActionTargets.Test | ActionTargets.Suite と定義されていてもTestFixutreごとには発火しないようですね。TestDetails#IsSuite が true であって TestDetails#Fixture が null のときは Assembly 単位で呼ばれたと判断できますが……これ将来改善されそうだなあ。
おまけ
ちなみに、Action と従来の SetUp/TearDown とどっちが先に呼ばれるのかなと思って
[TestFixture] [ConsoleAction("Actionからこんにちは!(Suiteに付与)")] public class AllTests { [SetUp] public void SetUp() { Console.WriteLine("SetUpからこんにちは!"); } [TearDown] public void TearDown() { Console.WriteLine("TearDownからこんにちは!"); } [TestFixtureSetUp] public void SetUpFixture() { Console.WriteLine("TestFixtureSetUpからこんにちは!"); } [TestFixtureTearDown] public void TearDownFixture() { Console.WriteLine("TestFixtureTearDownからこんにちは!"); } [Test] [ConsoleAction("Actionからこんにちは!(TestCaseに付与)")] public void SimpleTest() { Console.WriteLine("テスト本体が実行されました"); } }
クラスを作ってみたところ、こんな風になりました(上で試した Assembly 単位の付与もしてある)
Before Suite: Actionからこんにちは!(Assemblyに付与), from {no fixture}.{no method}. TestFixtureSetUpからこんにちは! Before Suite: Actionからこんにちは!(Suiteに付与), from AllTests.{no method}. SetUpからこんにちは! Before Case: Actionからこんにちは!(Assemblyに付与), from AllTests.SimpleTest. Before Case: Actionからこんにちは!(Suiteに付与), from AllTests.SimpleTest. Before Case: Actionからこんにちは!(TestCaseに付与), from AllTests.SimpleTest. テスト本体が実行されました After Case: Actionからこんにちは!(TestCaseに付与), from AllTests.SimpleTest. After Case: Actionからこんにちは!(Suiteに付与), from AllTests.SimpleTest. After Case: Actionからこんにちは!(Assemblyに付与), from AllTests.SimpleTest. TearDownからこんにちは! After Suite: Actionからこんにちは!(Suiteに付与), from AllTests.{no method}. TestFixtureTearDownからこんにちは! After Suite: Actionからこんにちは!(Assemblyに付与), from {no fixture}.{no method}.
ひとつの TestFixture 内では、どうも先に TestFixtureSetUp が呼ばれるようですね。