Where to put Unit Tests in .NET

When you write a .NET application that has (for sake of argument) several DLLs, you have various options about where you compile your NUnit Unit Test Fixtures to. They are:

1. Create one ‘testing and production’ DLL that contains all the classes in your application (production and test fixture), and run NUnit against just that.

2. Put the Test Fixtures in the same (production) DLL as the classes they are testing, then run NUnit across all the production DLLs.

3. Create one ‘testing’ DLL for your application and put all your Fixtures and test classes in it. Run NUnit against this, which itself calls the production DLLs.

4. Create one ‘testing’ DLL for each production DLL. Run NUnit against these, which themselves call the production DLLs.

I’ve seen most of these used. At my client right now, we are currently using option (1) as it offers the easiest and quickest way of compiling and running all the Unit Tests in your application. The problems with it though are:

– it breaks the encapsulation between your projects

– if you miss something out in compiling your special DLL you end up not actually testing what goes into production.

Option (2) is what we are using in CruiseControl.NET. There are some benefits to this choice:

– You only need to compile production DLLs

– Your tests are available in production for debugging if necessary

This second benefit is something I was specifically discussing with a colleague the other day, as I think it maybe a drawback of this method. To me, I have a bad feeling from a security and efficiency point-of-view about putting test-classes into production. That said, if you are developing a bespoke server application deployed locally (which is the situation which would benefit most for such debugging opportunities), and you have tight security, maybe this is worthwhile.

A drawback of options (1) and (2) is that for development’s sake, you’d probably create sub-namespaces for testing. e.g. If I’m testing the Sheep class in the Farmyard.Animals namespace, I’d probably create a SheepTest class in the Farmyard.Animals.Test Namespace. This means that in my SheepTest class I need to add a using statement for the actual Namespace I’m testing.

Options (3) and (4) are similar to each other in that both allow you to test the real production DLLs, while at the same time allowing you to only deploy production classes to production. They also both enable you to write your test classes in a separate project and DLL, but at the same time use the same Namespace as the target class (the real DLL doesn’t depend on the test DLL, so ‘Intellisense’ still works nicely.) If you choose not to deploy these DLLs to production, you can always save the binaries, or recompile later, should you want to run the tests in a production environment.

The drawback of these options is you end up compiling more DLLs, and having more Visual Studio projects, than you have production DLLs.

Weighing-up between options (3) and (4), we see the following comparisons:

– Option 3 allows you to run all the tests for your application by running NUnit against just one DLL, which itself is useful for development speed.

– Option 4 more closely models the componentization of your application. This means you have a natural componentization for your test DLLs, you can easily run just the test fixtures for one component, and if you move components between applications you can more easily move the unit tests. That said, if you change the structure of your components, you also need to change the structure of your test DLLs.

So, which of these to use? Well, more and more I’ve been using the <solution> task in NAnt to compile my .NET projects. It naturally fits, and works well, with the Visual Studio .NET model of an application. VS has its drawbacks, but more and more I see the benefit of a build tool that works closely with it, and it also does model intra-application / inter-project dependencies quite nicely. When using a combination of <solution> and Visual Studio, having extra projects really is little overhead. Therefore, the ‘extra-baggage’ drawback of options (3) and (4) are negated to the extent that I think they feel the ‘right’ thing to do. Right now, I prefer option (4) due to its better fit of the application, but it really is a close call to whether it is better than option (3).