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
}
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
No comments:
Post a Comment