Sunday, August 10, 2014

How to use MsTest and NUnit Initialisation Methods

How to use the available initialisation methods in both MSTest and NUnit can be a little confusing when learning how to write effective tests. Effective tests are designed, just like production code, shared code should be in one place (DRY) and initialisation should be separate so to make tests less brittle.

Imagine if every test constructs an object like so:

var subject = new Foo();

Also imagine that the Foo class has decent number of methods to test.  One day a developer adds a parameter to the Foo constructor.  Now many tests are broken and require the same change. If the initialisation code is in one place this would have been a fast fix:- these tests are less brittle.

It would be handy to run some code before each test, some only once per class, and sometimes you need to initialise something before you run any tests.

First up its important to note some semantic behavour of all xUnit frameworks.
Every test will run in its own instance of the test class.  No two tests share the same instance.  When a test is finished running the instance is available for garbage collection. This means that any instance fields or properties are not shared between the tests in a class.  This is by design and cannot be configured. This is mostly to isolate one test from another, but another reason for this is to support running tests in parallel on multiple threads, although MsTest and NUnit do not run tests in parallel by default, although they both can.

MsTest

In MsTest here's the low-down on the available initialisation methods:

  • TestInitialize
  • ClassInitalize
  • AssemblyInitialize
  • TestCleanUp
These attributes are only recognised inside a class decorated with [TestClass].

TestIntialize

[TestInitialize]
public void TestInitialise() 
{
    // Your initialisation code here
}

This method will run before every test. This is normally the best place to set up test data, mocks, and instantiate the subject under test.  Sometimes, if possible, execution of the subject method can be done here as well so that each test only contains an assert. This has very similar to class's constructor, and both can be used, but its considered best practice to use the TestInitialise and not use constructors on Test Classes.

ClassInitialize


[ClassInitialize]
public static void ClassInitialise(TestContext context)
{
    // Your initialisation code here
}


This method will run once per class only, its very similar to a Type-Constructor. This can be used to initialise any static fields and properties in the test class. The TestContext argument primarily allows output to the MsTest console log.

AssemblyInitialize


[TestClass]
public class Global
{
    [AssemblyInitialize]
    public static void AssemblyInitialise(TestContext context)
    {
        // Your initialisation code here
    }
}

This method will run only once per Unit Test Assembly per test run.  Only one AssemblyInitialize method can exist per unit test assembly (or an exception is thrown when a test run starts). It is considered best practice to put this method into an easy to find Global class with no other tests. The TestContext argument primarily allows output to the MsTest console log.  I personally find this handy for registering AutoMapper configuration before running any tests involving mappers.

TestCleanUp

[TestCleanup]
public void TestCleanUp()
{
    // Your clean up code here
}

This is useful when you need to dispose any instance fields or properties, or reset anything back to a known state. Use with caution though, use if this method could be a "smell" that your tests are not isolated from dependencies like database and third-party services.
Along with TestCleanUp you can also use the ClassCleanUp attribute.  I'm not a fan of this, I've never had a good reason to use it.  To me, it indicates use of a static dependency and that doesn't sit well with my style.

Here's how the Sequence unfolds

Lets use a test to show the sequence of method calls.


    [TestClass]
    public class Global
    {
        [AssemblyInitialize]
        public static void AssemblyInitialise(TestContext context)
        {
            Debug.WriteLine("Assembly Initialise");
        }
    }

    [TestClass]
    public class SemanticTest
    {
        private int counter;
        private Guid id;

        public SemanticTest()
        {
            this.id = Guid.NewGuid();
        }

        [ClassInitialize]
        public static void ClassInitialise(TestContext context)
        {
            Debug.WriteLine("Class Initialise");
        }

        [TestMethod]
        public void Test1()
        {
            Debug.Write("Test1 ");
            this.counter++;
            Debug.WriteLine("Counter: {0} Id:{1}", this.counter, this.id);
        }

        [TestMethod]
        public void Test2()
        {
            Debug.Write("Test2 ");
            this.counter++;
            Debug.WriteLine("Counter: {0} Id:{1}", this.counter, this.id);
        }

        [TestMethod]
        public void Test3()
        {
            Debug.Write("Test3 ");
            this.counter++;
            Debug.WriteLine("Counter: {0} Id:{1}", this.counter, this.id);
        }

        [TestMethod]
        public void Test4()
        {
            Debug.Write("Test4 ");
            this.counter++;
            Debug.WriteLine("Counter: {0} Id:{1}", this.counter, this.id);
        }

        [TestMethod]
        public void Test5()
        {
            Debug.Write("Test5 ");
            this.counter++;
            Debug.WriteLine("Counter: {0} Id:{1}", this.counter, this.id);
        }

        [TestCleanup]
        public void TestCleanUp()
        {
            Debug.WriteLine("TestCleanUp " + this.counter);
        }

        [TestInitialize]
        public void TestIninitalise()
        {
            Debug.WriteLine("Test Initialise");
        }
    }

The output is:
Assembly Initialise
Class Initialise
Test Initialise
Test1 Counter: 1 Id:fb05338c-b64e-4f30-bdbb-22f4c35749b1
TestCleanUp 1
Test Initialise
Test2 Counter: 1 Id:c207ef4b-83c1-4909-828b-10a7a43ef653
TestCleanUp 1
Test Initialise
Test3 Counter: 1 Id:07e5ddd0-6f3d-4e32-bd2d-2a9544e8bd51
TestCleanUp 1
Test Initialise
Test4 Counter: 1 Id:73e703a9-9c6b-4b02-8cb5-433c8c64167e
TestCleanUp 1
Test Initialise
Test5 Counter: 1 Id:37124412-e593-4ba2-8d28-6421d32117c8
TestCleanUp 1

Here its plain to see all tests ran in their own instance of the SemanticTest class.

NUnit

All the above behaves identically in NUnit. Here are the equivalent attributes in NUnit as compared to MsTest.
  • SetUp == TestIntialize
  • TestFixtureSetUp == ClassInitialize
  • SetUpFixture == AssemblyInitialize
    Although in NUnit, SetUpFixture is scoped to a namespace not an entire assembly, which allows multiple SetUpFixture per assembly, but only one per namespace.
  • TearDown == TestCleanUp



See Also

No comments:

Post a Comment