I'm not yet a true convert of test-driven development -- at least, not for UI classes or code requiring complex I/O (the vast majority of my work). But for simple, rock-bottom library classes (like the FloatComparer class I recently posted) I find unit testing to be indispensable. (Not a panacea, but extremely useful nevertheless.)
Still, it can be a bit cumbersome -- and the more popular tools of the trade (like NUnit) tend to make the experience even more so. NUnit makes my life difficult in the following ways:
- It complicates the build process. Nobody can just grab my code and click the 'build' button, without they have NUnit installed. Skipping the unit tests isn't the problem -- not finding the NUnit assembly reference is the problem.
- It complicates the redistribution. The testee assemblies contain a build-time reference to NUnit.Framework.dll, so, even though the test code is never intended to be run on an end-user's machine (and may even be #ifdef'd out), I'm still compelled to redist the NUnit assembly, or else lay awake at night worrying about fusion exceptions.
Granted, these same basic complaints apply to any third-party component. But NUnit is not a component! It's a developer tool. There's a difference between, say, a third-party grid control, which I may only reuse once in a blue moon (and then, usually for full-fledged development projects with unavoidably large and complex setups) and a tool or technology that's an intimate part of every little class I write. I just don't wanna deal with it! Indeed, it's the little standalone library classes like FloatComparer which lend themselves most readily to unit testing.
I see no reason for unit testing to complicate my build process or redist strategy. Do we really need a full-fledged assembly reference to NUnit.Framework, just to get at... what? A handful of attributes? No -- the NUnit EXEs could just as easily look up the attributes by name, rather than by strong-typing. I don't need strong-naming for my unit tests: names like NUnit.Framework.TestFixture aren't likely to collide with any other type names in my code!
Solution: I've taken to simply defining my own unit test attribute, wherever
I need it -- it's really
just a handful of code. I call my attribute [Jitsu.Testing.UnitTest] but your taste may
differ. I spent a few hours this past weekend and whipped up my own
TestRunner tool which simply reflects
for that attribute, by name, and then invokes any static methods which exhibit that
attribute. Crude, but effective!
#if TESTING namespace Jitsu.Testing { // Usage: tag unit test entry points with this attribute. // Methods' signature should be of the form 'static bool Foo()' [AttributeUsage(AttributeTargets.Method)] class UnitTestAttribute : Attribute { } } #endif
You can sneak this small block of code into any project -- it's short enough to type by
memory, or you can
link to a
common source file. The point is, there are no third-party assemblies to ship, and it allows you to keep all
your test goo neatly tucked away inside a #define block (out of your retail
builds!) if you wish.
To make matters even simpler, my TestRunner tool wraps a custom TextWriter over the stderr stream, allowing unit tests to log a failure by simply logging a message to Console.Error. So, a sample unit test method might look like this:
class Widget { // (Ordinary class innards, here...) #if TESTING [Jitsu.Testing.UnitTest] static bool TestWidget1() { Console.WriteLine("First test, s/b successful."); return true;//pass } [Jitsu.Testing.UnitTest] static bool TestWidget2() { Console.WriteLine("Second test, s/b failure."); Console.Error.WriteLine("Oops, something failed!"); Console.WriteLine("Continuing second test."); Console.Error.WriteLine("Yikes, something else failed!"); return false;//fail } #endif }
No specially defined exception types to worry about... if an exception is expected, you can just catch it and get on. If not, it will be caught and logged as a failure for that case. You needn't even bother with the boolean return code if you don't want to (if you return 'true', the case will still be failed if you write anything at all to Console.Error).
Here's hoping others out there may find this crude-but-effective approach to unit testing useful -- if NUnit has been causing you headaches, possibly preventing you from adopting TDD methodologies, then hopefully this alternative will cure the pain.
Happy Testing!
