Unit Testing Attributes Behavior By Example
Overview
Unit Testing frameworks contains a cluster of unit testing attributes that can be applied at the class or method level. An in-depth understanding of the behaviors of these attributes will significantly improve our day to day interactions with unit testing code.
In designing unit test objects, we commonly separate them using multiple classes each representing specific testing scenarios. Some people like the idea of nesting classes based on area of functionality or other testing criteria. After the separation, we analyze those individual classes and tend to group them together based on certain criteria. In .NET, this grouping can be represented by namespaces. Yes you are correct that there are other ways to do this but for the purpose of this document, we will not cover them here.
Problem
With all those test classes and unit testing scenarios laid out, we regularly need a common routine that we want to execute before or after performing a test or groups of tests. This common routine can be required at the class, namespace or assembly level. In this article, we will explore the behavior of some unit testing attributes depending on the context of their execution. For the purpose of demonstration, we will use a project that contains three test classes each representing a functional area to illustrate the behavior and the target class to be tested.
Framework Attributes
We will be using an attribute here that is specific to a certain unit testing framework but the same principle can be applied to other testing frameworks as well. For the following examples, we will use NUnit. To specify the common method that gets executed before any test method or groups of test methods, we shall use the “SetUp” attribute. To create a routine that gets executed after any test method or groups of test methods we will use the “TearDown” attribute.
Class Level Behavior
namespace NUnitFixtures
{
public class Tests1
{
[SetUp]
public void ClassLevelSetup()
{
Console.WriteLine("Class level setup for Tests1");
}
[Test]
public void Test1()
{
ClassToBeTested a = new ClassToBeTested();
Assert.IsTrue(a.Add(1, 2).Equals(3));
Console.WriteLine("Test1");
}
[Test]
public void Test2()
{
ClassToBeTested a = new ClassToBeTested();
Assert.IsTrue(a.Add(3, 4).Equals(7));
Console.WriteLine("Test2");
}
[TearDown]
public void InternalTearDown()
{
Console.WriteLine("Class level teardown for Tests1");
}
}
}
In the above example, we have two test methods Test1 and Test2. The ClassLevelSetup is decorated by [SetUp] attribute which means it will be executed before each test method is run. The InternalTearDown method is decorated by the [TearDown] attribute which will get executed after each test method. This illustrates the “Class Level” setup and teardown behavior. Executing the test above displays the following result:
Class level setup for Tests1
Test1
Class level teardown for Tests1
Class level setup for Tests1
Test2
Class level teardown for Tests1
Namespace Level Behavior
We have just demonstrated a way to create a common routine that can be executed before or after each of the test methods within the unit test class. There are occasions where you need a common code that gets executed before and/or after a collection of test methods across different unit test classes. Consider the code below that contains two test methods without any [SetUp] or [TearDown] methods:
namespace NUnitFixtures
{
public class Tests2
{
[Test]
public void Test2_1()
{
ClassToBeTested a = new ClassToBeTested();
Assert.IsTrue(a.Add(1, 2).Equals(3));
Console.WriteLine("Test2_1");
}
[Test]
public void Test2_2()
{
ClassToBeTested a = new ClassToBeTested();
Assert.IsTrue(a.Add(3, 4).Equals(7));
Console.WriteLine("Test2_2");
}
}
}
You will also notice that it shares the same namespace as our first test class. In order to show the behavior of the namespace level setup and teardown, we will keep the internal setup and teardown methods in Tests1 class and create a new class that will have the namespace level common setup. The new class is structured as illustrated below:
namespace NUnitFixtures
{
[SetUpFixture]
public class TestFixtureClass
{
[SetUp]
public void Setup()
{
Console.WriteLine("Namespace level setup executed..");
}
[TearDown]
public void Teardown()
{
Console.WriteLine("Namespace level teardown executed..");
}
}
}
In the class above, you will notice that it also shares the same namespace as the other two test classes. The class is decorated with the [SetUpFixture] attribute which means that it will contain a one-time [SetUp] and [TearDown] for all test methods within a specified namespace. Executing both unit test classes will yield the following result:
Namespace level setup executed.
Class level setup for Tests1
Test1
Class level teardown for Tests1
Class level setup for Tests1
Test2
Class level teardown for Tests1
Test2_1
Test2_2
Namespace level teardown executed.
This clearly illustrates that the namespace level setup and teardown methods were only executed once for all the test methods within the namespace.
Assembly Level Behavior
Now that we have done both the class and assembly level behaviors, it’s about time that we dive deeply into how we can have a common setup and teardown method that spans across all namespaces within the current assembly. You will be surprised later when you find out how easy it is to do that. First, let’s take a look at the third unit test class that is under a different namespace.
namespace ADifferentNamespace
{
public class Tests3
{
[SetUp]
public void InternalSetup()
{
Console.WriteLine("Internal setup inside ADifferentNamespace");
}
[Test]
public void Test3_1()
{
ClassToBeTested a = new ClassToBeTested();
Assert.IsTrue(a.Add(1, 2).Equals(3));
Console.WriteLine("Test3_1");
}
[Test]
public void Test3_2()
{
ClassToBeTested a = new ClassToBeTested();
Assert.IsTrue(a.Add(3, 4).Equals(7));
Console.WriteLine("Test3_2");
}
[TearDown]
public void InternalTearDown()
{
Console.WriteLine("Internal teardown inside ADifferentNamespace");
}
}
}
The third unit test class also contains a class level [SetUp]and [TearDown]methods which will get executed before and after each test method within the class. Also note that even if this is in a different namespace, we are still testing the same class that was tested by the previous test classes. In the real scenario this would be different class but what we are trying to show here is the concept of assembly level behavior of the setup and teardown methods so let’s not delve into that area.
Now for the most awaited part, we will be adding another setup fixture class and keep the existing ones to demonstrate how all of them can work together. Try to spot the difference between the namespace level setup fixture example above and the assembly level shown below:
[SetUpFixture]
public class AssemblyTestFixtureClass
{
[SetUp]
public void Setup()
{
Console.WriteLine("Assembly level setup executed.");
}
[TearDown]
public void Teardown()
{
Console.WriteLine("Assembly level teardown executed.");
}
}
Executing the tests above will yield the following result:
Assembly level setup executed.
Internal setup inside ADifferentNamespace
Test3_1
Internal teardown inside ADifferentNamespace
Internal setup inside ADifferentNamespace
Test3_2
Internal teardown inside ADifferentNamespace
Namespace level setup executed.
Class level setup for Tests1
Test1
Class level teardown for Tests1
Class level setup for Tests1
Test2
Class level teardown for Tests1
Test2_1
Test2_2
Namespace level teardown executed.
Assembly level teardown executed.
The trick is to remove the namespace where the[SetUpFixture] attribute is applied. By definition, [SetUpFixture] attribute applies to all test methods within the namespace, but if we remove the namespace of the class where it is applied, the fixture gets applied at the assembly level. Easy as ABC right?
To implement this using Microsoft UnitTesting Framework see this article.
I hope this article has been helpful to you in understanding the behavior of the [SetUp], [TearDown]
and [SetUpFixture] attributes. Applying the knowledge acquired here, you should be able to implement re-usable codes across all of your test methods within the class, namespace or assembly level.
Happy Unit Testing!
Mabuhay!