Decorate Your Tests

Standard

In my last post I showed you how to write a minimal NUnit addin. It didn’t do anything useful, so let’s see what we can do with test decorators.

What Is a Test Decorator?

The Decorator Pattern describes a design for attaching additional responsibilities to an object dynamically. It is also known as Wrapper. Test Decorators wrap a single test case or a test fixture and therefore allow you to run some code before and after it.

Typical uses for test decorators are measuring performance or setting up and cleaning up a database.

Copy a File Before Running a Test

Assume that a test needs a certain file each time you run a test. If those tests are in the same test fixture and there are no other tests in this fixture, you will use a setup method. Otherwise test decorators may help you. Furthermore test decorators provide a way to share test extensions.

So, let’s start by writing an NUnit addin:

    [NUnitAddin(Name="Copy File Decorator Addin",
                Description="Copies a file before running the test.")]
    public class CopyFileAddin : IAddin, ITestDecorator
    {
      public bool Install(IExtensionHost host)
      {
        IExtensionPoint decorators = host.GetExtensionPoint("TestDecorators");
        if (decorators == null)
          return false;
                
        decorators.Install(this);
        return true;
      }
    }

As CopyFileAddin implements both the IAddin and the ITestDecorator interfaces, it installs itself at the extension point “TestDecorators”. I described the IAddin interface in my last post and ITestDecorator declares the method Decorate and CopyFileAddin implements it as follows:

    public Test Decorate(Test test, MemberInfo member)
    {
      // Decorate only single test cases
      if (test is TestCase)
      {
        Attribute attribute = Reflect.GetAttribute(
            member, "CopyFileTest.Framework.CopyFileAttribute", false);
                    
        if (attribute != null)
        {
          string sourcePath = (string)Reflect.GetPropertyValue(attribute, 
              "SourcePath", BindingFlags.Public | BindingFlags.Instance);
          string destinationPath = (string)Reflect.GetPropertyValue(attribute, 
              "DestinationPath", BindingFlags.Public | BindingFlags.Instance);
                        
          test = new CopyFileTestCase((TestCase) test,
              sourcePath, destinationPath);
        }
      }
    
      return test;
    }

In NUnit 2.4 you mark tests to be wrapped by using specific .NET attributes. The test decorator extension identifies those tests and wrap them with a special decorator test case class, e.g. the following test case will be wrapped with a CopyFileTestCase if the CopyFileAddin is properly installed in your NUnit – in my last post I described how to do this.

The attribute CopyFile marks the test case to be wrapped and stores the source and destination path of the file to be copied. The test may be run in another thread, so the paths must be absolute.

The decorator class looks like this:

    public class CopyFileTestCase : TestCase
    {
      private TestCase _testCase;
      private string _sourceFilePath;
      private string _destinationPath;
            
      public CopyFileTestCase(
          TestCase testCase,
          string sourceFilePath,
          string destinationPath)
          : base((TestName)testCase.TestName.Clone())
      {
        _testCase = testCase;
        _sourceFilePath = sourceFilePath;
        _destinationPath = destinationPath;
      }
    
      public override void Run(TestCaseResult result)
      {
        if (File.Exists(_sourceFilePath))
          File.Copy (_sourceFilePath, _destinationPath);
            
        _testCase.Run(result);
                
        if (File.Exists(_destinationPath))
          File.Delete(_destinationPath);
      }
    }

Test Cases And Test Suites

Maybe you are wondering, what the classes Test and TestCase are about. NUnit organizes tests in test cases and test suites and both are tests. They form another design pattern known as Composite.

Test, TestCase and TestSuite

If your decorator supports single test cases only it will wrap a TestCase, like CopyFileTestCase. You may decorate test suites or both test cases and test suites by wrapping a TestSuite or Test object respectively.

RepeatedTestDecorator and MaxTimeDecorator

NUnit 2.4 already provides a test decorator: the RepeatedTestDecorator. You can find it in the assembly “nunit.framework.extensions.dll”.

    using System;
    using NUnit.Framework;
    using NUnit.Framework.Extensions;
    
    namespace RepeatedTest
    {
      [TestFixture]
      public class RepeatedTestFixture
      {
        private int _count;
            
        [TestFixtureSetUp]
        public void TestFixtureSetUp()
        {
          _count = 0;
        }
            
        [Test]
        [Repeat(5)]
        public void RepeatedTestCase()
        {
          _count++;
          Console.WriteLine(_count);
          if (_count == 5)
            Assert.Fail("The test has been repeated 5 times.");
        }
      }
    }

You will have to reference the assembly “nunit.framework.extensions.dll”. The test will fail with the
provided message. The NUnit package installs some sample projects and amongst them a solution with examples for the new Core Extensibility features. There you can find the sample decorator MaxTimeDecorator, which fails a test when its duration exceeds a provided limit.

Downsides

Currently there is no way to specify an Addin as requirement. Imagine, you’ve written a unit test using the MaxTimeDecorator. Your test will fail if it runs longer than the provided limit. You
commit your changes to your source repository. Your colleague gets the new unit test from the repository and works on the program. Her changes affect the performance of the code and the test lasts longer than required. If she didn’t install the MaxTimeDecorator Addin, she would not notice the defect.

There is currently no mechanism to control the order in which the decorators are applied. NUnit applies decorators in the order in which they are returned through reflection. Therefore pay attention to this issue if you use more than one decorator.

    [Test]
    [Repeat(5)]
    [MaxTime(1100)]
    public void RepeatedTestCase()
    {
      _count++;
      System.Threading.Thread.Sleep(1000);
      if (_count == 5)
        Assert.Fail("The test has been repeated 5 times.");
    }

In this example it is not predictable if the test will fail because of the counter or the exceeded time limit. If the RepeatDecorator wraps the MaxTimeDecorator, it will fail because of the counter, otherwise because of the time limit.

More Information

If you need more information about NUnit addins, have a look at the NUnit documentation.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>