Action Attributes (NUnit 2.6)
Note: This is an experimental feature in the NUnit 2.6 Beta release and may change in the final release.
Action Attributes are a feature of NUnit designed to better enable composability of test logic. Often when writing unit tests we have logic that we want to run upon certain events in the test cycle (e.g. SetUp, TearDown, FixtureSetUp, FixtureTearDown, etc.). NUnit has had the ability to execute code upon these events by decorating fixture classes and methods with the appropriate NUnit- provided attributes. Action Attributes allow the user to create custom attributes to encapsulate specific actions for use before or after any test is run.
The Problem of Composability
Suppose we have some tests in multiple fixtures that need the same in-memory test database to be created and destroyed on each test run. We could create a base fixture class and derive each fixture that depends on the test from that class. Alternatively, we could create a SetUpFixture class at the level of a common namespace shared by each fixture.
This works fine, until we need some other reusable functionality, say the ability to configure or reset a ServiceLocator. We could put that functionality in the base fixture class or setup fixture, but now we're mixing two different responsibilities into the base class. In the case of a setup fixture, this only works if all classes requiring both features are located in a common namespace. In some cases we may *not* want the test database, but we do want ServiceLocator configuration; and sometimes we want the opposite. Still other times we'll want both - so we'd have to make the base class configurable.
If we now discover a third piece of functionality we need to reuse, like configuring the Thread's CurrentPrincipal in arbitrary ways, the complexity of the solution very quickly. We've violated the Single Responsibility Principle and are suffering for it. What we really want is the ability to separate the different pieces of resuable test logic and compose them together as our tests need them.
Resolving the Problem
Action Attributes get us out of our bind. Consider this example:
[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() { /* ... */ } }
Here we have used attributes to identify five different actions that we want to compose together in different ways for different tests:
- ResetServiceLocator
- CreateTestDatabase
- AsAdministratorPrincipal
- AsNamedPrincipal
- AsGuestPrincipal
Implementing an Action Attribute
Any attribute can be an action attribute. To create an action attribute, create a class that inherits from System.Attribute and implement either ITestSuiteAction or ITestCaseAction. Each interface has a Before and After method that must be implemented.
public interface ITestAction { } public interface ITestSuiteAction : ITestAction { void BeforeTestSuite(object fixture, MethodInfo method); void AfterTestSuite(object fixture, MethodInfo method); } public interface ITestCaseAction : ITestAction { void BeforeTestCase(object fixture, MethodInfo method); void AfterTestCase(object fixture, MethodInfo method); }
When an attribute that implements ITestSuiteAction is applied to either a class or a parameterized method, NUnit will execute the attribute's BeforeTestSuite method prior to executing the test suite and then execute the AfterTestSuite method after the test suite has finished executing. This is similar to how the TestFixtureSetUp and TestFixtureTearDown attributes work.
Likewise, when an attribute that implements ITestCaseAction is applied to either a class or method (that represents a test case), NUnit will execute the attribute's BeforeTestCase method prior to executing the test case, and then execute the AfterTestCase method after the test case has finished executing. This is similar to how the SetUp and TearDown attributes work.
Examples
The examples that follow all use the following sample Action Attribute:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Assembly, AllowMultiple = true)] public class ConsoleActionAttribute : Attribute, ITestSuiteAction, ITestCaseAction { private string _Message; public ConsoleActionAttribute(string message) { _Message = message; } public void BeforeTestSuite(object fixture, MethodInfo method) { WriteToConsole("Before Suite", fixture, method); } public void AfterTestSuite(object fixture, MethodInfo method) { WriteToConsole("After Suite", fixture, method); } public void BeforeTestCase(object fixture, MethodInfo method) { WriteToConsole("Before Case", fixture, method); } public void AfterTestCase(object fixture, MethodInfo method) { WriteToConsole("After Case", fixture, method); } private void WriteToConsole(string eventMessage, object fixture, MethodInfo method) { Console.WriteLine("{0}: {1}, from {2}.{3}.", eventMessage, _Message, fixture != null ? fixture.GetType().Name : "{no fixture}", method != null ? method.Name : "{no method}"); } }
Note that the above Action Attribute implements both ITestSuiteAction and ITestCaseAction. This is permitted, but will probably not be the normal case. It is done here so we can reuse the attribute in multiple examples. The attribute takes a single constructor argument, a message, that will be used to write output to the console. All of the Before and After methods write output via the WriteToConsole method.
Method Attached Actions
Example 1 (applied to simple test method):
[TestFixture] public class ActionAttributeSampleTests { [Test][ConsoleAction("Hello")] public void SimpleTest() { Console.WriteLine("Test ran."); } }
Console Output:
Before Case: Hello, from ActionAttributeSampleTests.SimpleTest. Test ran. After Case: Hello, from ActionAttributeSampleTests.SimpleTest.
Example 2 (applied action twice to test method):
[TestFixture] public class ActionAttributeSampleTests { [Test] [ConsoleAction("Hello")] [ConsoleAction("Greetings")] public void SimpleTest() { Console.WriteLine("Test run."); } }
Console Output:
Before Case: Greetings, from ActionAttributeSampleTests.SimpleTest. Before Case: Hello, from ActionAttributeSampleTests.SimpleTest. Test run. After Case: Hello, from ActionAttributeSampleTests.SimpleTest. After Case: Greetings, from ActionAttributeSampleTests.SimpleTest.
Remarks
You are permitted to apply the same attribute multiple times. Note that the order in which attributes are applied is indeterminate, although it will generally be stable for a single release of .NET.Example 3 (applied to a test method with test cases):
[TestFixture] public class ActionAttributeSampleTests { [Test] [ConsoleAction("Hello")] [TestCase("02")] [TestCase("01")] public void SimpleTest(string number) { Console.WriteLine("Test run {0}.", number); } }
Console Output:
Before Suite: Hello, from ActionAttributeSampleTests.SimpleTest. Before Case: Hello, from ActionAttributeSampleTests.SimpleTest. Test run 01. After Case: Hello, from ActionAttributeSampleTests.SimpleTest. Before Case: Hello, from ActionAttributeSampleTests.SimpleTest. Test run 02. After Case: Hello, from ActionAttributeSampleTests.SimpleTest. After Suite: Hello, from ActionAttributeSampleTests.SimpleTest.
Remarks
When one or more [TestCase] attributes are applied to a method, NUnit treats the method as a test suite. You'll notice that BeforeTestSuite was run once before both of the test cases, and AfterTestSuite was run once after both of the test cases. BeforeTestCase and AfterTestCase is run for each test case. Note that the order in which test cases are executed is indeterminate.Type Attached Actions
Example 1:
[TestFixture] [ConsoleAction("Hello")] public class ActionAttributeSampleTests { [Test] public void SimpleTestOne() { Console.WriteLine("Test One."); } [Test] public void SimpleTestTwo() { Console.WriteLine("Test Two."); } }
Console Output:
Before Suite: Hello, from ActionAttributeSampleTests.{no method}. Before Case: Hello, from ActionAttributeSampleTests.SimpleTestOne. Test ran. After Case: Hello, from ActionAttributeSampleTests.SimpleTestOne. Before Case: Hello, from ActionAttributeSampleTests.SimpleTestTwo. Test ran. After Case: Hello, from ActionAttributeSampleTests.SimpleTestTwo. After Suite: Hello, from ActionAttributeSampleTests.{no method}.
Remarks
In this case, the class is the test suite. BeforeTestSuite and AfterTestSuite are run only once for this class. BeforeTestCase and AfterTestCase are run for each test.Example 2 (attached to interface):
[ConsoleAction("Hello")] public interface IHaveAnAction { } [TestFixture] public class ActionAttributeSampleTests : IHaveAnAction { [Test] public void SimpleTest() { Console.WriteLine("Test run."); } }
Console Output:
Before Suite: Hello, from ActionAttributeSampleTests.{no method}. Before Case: Hello, from ActionAttributeSampleTests.SimpleTest. Test run. After Case: Hello, from ActionAttributeSampleTests.SimpleTest. After Suite: Hello, from ActionAttributeSampleTests.{no method}.
Remarks
Action attributes can be applied to an interface. If a class marked with [TestFixture] implements an interface that has an action attribute applied to the interface, the class inherits the action attribute from the interface. It behaves as if you applied the action attribute to the class itself.Example 3 (action attribute is applied to interface and attribute uses interface to provide data to tests):
[AttributeUsage(AttributeTargets.Interface)] public class InterfaceAwareActionAttribute : Attribute, ITestCaseAction { private readonly string _Message; public InterfaceAwareActionAttribute(string message) { _Message = message; } public void BeforeTestCase(object fixture, MethodInfo method) { IHaveAnAction obj = fixture as IHaveAnAction; if(obj != null) obj.Message = _Message; } public void AfterTestCase(object fixture, MethodInfo method) { } } [InterfaceAwareAction("Hello")] public interface IHaveAnAction { string Message { get; set; } } [TestFixture] public class ActionAttributeSampleTests : IHaveAnAction { [Test] public void SimpleTest() { Console.WriteLine("{0}, World!", Message); } public string Message { get; set; } }
Console Output:
Hello, World!
Remarks
Here we see a new action attribute, [InterfaceAwareAction]. This attribute uses the fixture argument passed into BeforeTestCase and casts it to an interface, IHaveAnAction. If the fixture implements the IHaveAnAction interface, the attribute sets the Message property to the string passed into the constructor of the attribute. Since the attribute is applied to the interface, any class that implements this interface gets it's Message property set to the string provided to the constructor of the attribute. This is useful when the action attribute provides some data or service to the tests.Assembly Attached Action
Example 1:
[assembly: ConsoleAction("Hello")] [TestFixture] public class ActionAttributeSampleTests { [Test] public void SimpleTest() { Console.WriteLine("Test run."); } }
Console Output:
Before Suite: Hello, from {no fixture}.{no method}. Before Case: Hello, from ActionAttributeSampleTests.SimpleTest. Test run. After Case: Hello, from ActionAttributeSampleTests.SimpleTest. After Suite: Hello, from {no fixture}.{no method}.