14 January 2009

Debugging C# NUnit unit tests

I'm finally getting around to adding unit tests to a C# project I'm working on and got a chance to play around with NUnit. Yes, I know, I should've started adding these tests long ago (like when I started the project), but better late than never.

Using NUnit is quite easy:
  • Download the MSI from http://nunit.org (Current version 2.4.8)
  • Install
  • Add a Reference to "nunit.framework" in your project
Now for each class that needs a unittest, you add
 using NUnit.Framework;
at the top of the file and add a new class that you mark with a [TestFixture] attribute. This attribute is used by the NUnit GUI app to identify the classes that contain your unit tests.

Within your new [TestFixture] class, add tests by including methods marked with a [Test] attribute, and you can optionally have SetUp/TearDown methods to make your tests easier to manage.

Here is a stub [TestFixture] class with a single [Test]:
 [TestFixture]
public class Utils_Test
{
[Test]
public void Test_XXX()
{
// The unit test.
Assert.IsTrue(true);
}
}
If you want to add the SetUp/TearDown methods, they are marked with their own attributes as follows:
 [TestFixture]
public class Utils_Test
{
[TestFixtureSetUp]
public void FixtureInit()
{
// Initialization for the entire TestFixture.
  // Called once at beginning before any tests.
}

[TestFixtureTearDown]
public void FixtureCleanup()
{
// Cleanup for the entire TestFixture.
// Called once at end of all tests (even if they throw exceptions).
}

[SetUp]
public void TestInit()
{
// Init for each test.
}

[TearDown]
public void TestCleanup()
{
// Cleanup after running each test.
// Called even if the test throws an exceptions.
}

[Test]
public void Test_XXX()
{
// The unit test.
Assert.IsTrue(true);
}
}
But again, all you really need is the [Test] method.

Once you've done this and built your project, you can launch the NUnit GUI exe, point it at your assembly and it will find and run all (or some, if you choose) of your tests.

One problem is that when you encounter a failing test, you can't just jump in and debug your code. The free Visual Studio C# Express Edition doesn't allow you to attach to a running process like the non-free Professional versions of Visual Studio.

To get around this, I added the following TestSuite class to seek out (using reflection) and call all of the unit tests (just like what the NUnit GUI does).
 using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;

namespace MyApplication
{
public class TestSuite
{
public static void RunTests()
{
int nTests = 0;
int nFailedTests = 0;

foreach (Type t in Assembly.GetExecutingAssembly().GetTypes())
{
if (t.GetCustomAttributes(typeof(TestFixtureAttribute), false).Length != 0)
{
// Gather test info.
MethodInfo mFixtureSetup = null;
MethodInfo mFixtureTearDown = null;
MethodInfo mSetup = null;
MethodInfo mTearDown = null;
List<methodinfo> mTests = new List();
foreach (MethodInfo m in t.GetMethods())
{
if (m.GetCustomAttributes(typeof(TestFixtureSetUpAttribute), false).Length != 0)
mFixtureSetup = m;

if (m.GetCustomAttributes(typeof(TestFixtureTearDownAttribute), false).Length != 0)
mFixtureTearDown = m;

if (m.GetCustomAttributes(typeof(SetUpAttribute), false).Length != 0)
mSetup = m;

if (m.GetCustomAttributes(typeof(TearDownAttribute), false).Length != 0)
mTearDown = m;

if (m.GetCustomAttributes(typeof(TestAttribute), false).Length != 0)
mTests.Add(m);
}

// Run tests
object obj = Activator.CreateInstance(t);

if (mFixtureSetup != null)
mFixtureSetup.Invoke(obj, null);

foreach (MethodInfo m in mTests)
{
nTests++;
if (mSetup != null)
mSetup.Invoke(obj, null);

try
{
m.Invoke(obj, null);
} catch (Exception) {
nFailedTests++;
Console.WriteLine(String.Format("Exception thrown in {0} during {1} test. ", t.Name, m.Name));
}

if (mTearDown != null)
mTearDown.Invoke(obj, null);
}

if (mFixtureTearDown != null)
mFixtureTearDown.Invoke(obj, null);
}
}

System.Windows.Forms.MessageBox.Show(String.Format("{0} tests. {1} failed", nTests, nFailedTests),
"Unit test results");
}
}
}
And then I add a call to
 TestSuite.RunTests();
somewhere in my application so that I can run the unittests from within the app.

So now when I encounter a unittest failure in the NUnit GUI, I can set a breakpoint at an appropriate place in the code and then run the unittests in the Visual Studio debugger.

It's not perfect, but it works.

[Note: older versions of NUnit had a [Suite] attribute that could be used to set up test suites, but this doesn't seem to be present in the most recent releases. In addition, you apparently needed to add each unittest to the test suite manually, which is just asking for problems with missing tests.]

3 comments:

Anonymous said...

Great info, and workaround.
Thanks!

thannls from OZ said...

WOW ! Code posted on the internet that just about ran first time - thank you !!!

As I'm a total beginner, a few things took me a bit of time ..

1 You need to add System.Windows.Forms into project References (if it wasn't already there)

2 I had to change one line of code (maybe a typo, or the framework version I'm using??):
the line defining the mTests variable I made :

List mTests = new List();

thannls from OZ said...

oops, the correction I just posted has angle brackets and didn't work - I meant this:


List<System.Reflection.MethodInfo> mTests = new List<System.Reflection.MethodInfo>();