If I were just a bit of a cynic, I would say that open source is a nice spin on freeware. Freeware is free. Generally, freeware comes with source code, but calling free code open source seems to have a legitimizing quality about it. And, in fact, sometimes freeware carried a connotation that the code was hacky. I am not sure if it was Eric Raymond’s great book The Cathedral and the Bazaar that elevated the status of freeware or great products such as NUnit, but whatever combination of events occurred, we programmers are benefiting from open source code.
In this article, I want to talk about a synergy that includes the excellent open source NUnit (www.nunit.org) testing tool, instrumenting your code for tracing, and TraceListeners. Collectively, these three aspects of .NET programming can make debugging and testing a ton of fun and help you come off as a real pro when you deliver bulletproof code.
Rather than assume you have had time to familiarize yourself with any of these things—TraceListeners, NUnit, and the notion of instrumenting your code—I will briefly introduce all three things and show you a nice way to tie them together. If you want more information about NUnit, for example, check earlier articles on www.codeguru.com (such as “Testing Visual Basic .NET with NUnit”).
Instrumenting Your Code
Instrumenting is a term that is gaining in popularity. We programmers use the term to mean adding code that helps us diagnose and keep track of how our code is behaving. Years ago, we’d add some print statements and send some text to the console; this text might tell us that our code entered a specific method or dump some state information. With Visual Basic, we began using the Debug.Print method to send text to the Immediate window. In Visual Basic .NET, we can use Debug.WriteLine or Trace.WriteLine. Adding these statements to your code is part of what we mean by instrumenting your code.
To add Trace.WriteLine statements to your code, add an Imports statement for the System.Diagnostics namespace, and add some strategically placed Trace.WriteLine statements. (If you are not sure, a namespace is a thing that contains classes, structures, and enumerations.)
Suppose, for example, that we are writing an application that employs differential equations to calculate missile trajectories. Before we start blowing things up, we will clearly want to test our code. We will definitely want to watch the behaviors unfold as the code is running to prevent things such as firing before aiming. Simplifying here, we could write methods to aim and fire, adding Trace statement to each method.
Listing 1: Instrumenting for Tracing.
Option Explicit On Imports System.Diagnostics Public Class FiringSystem Public Sub Aim(ByVal Elevation As Double, _ ByVal Direction As Double, ByVal Distance As Double) Const Message As String = _ "Aiming with Elevation={0}, Direction={1}, Distance={2}" Trace.WriteLine(String.Format(Message, Elevation, Direction, _ Distance)) 'Aim here End Sub Private Function WasAimed() As Boolean Throw New NotImplementedException() End Function Public Sub Fire() Debug.Assert(WasAimed()) Trace.WriteLine("Fire in the hole") ' Blow up bad guys End Sub End Class
In the example, our stubbed out FiringSystem class provides an Aim and Fire method. The Aim method will send a string containing the Elevation, Direction, and Distance to a TraceListener. (More on TraceListeners in a moment.) The Fire method asserts that we did in fact aim and emits a trace statement that indicates that Fire was called.
When we run our FiringSystem, the Trace.WriteLine statements tells us what is going on.
Implementing a TraceListener
A TraceListener is a class that inherits from the abstract class TraceListener. To listen for Trace statements, TraceListeners are added to the Trace.Listener collection. Listener is a shared collection on the Trace class. By default, a debug TraceListeners has already been added to the Listeners collection and output is written to the Output window in Visual Studio .NET. However, if we aren’t running VS.NET, we won’t see these messages. Suppose we want to trace after we deploy the software or in another environment? Then, we can implement our own TraceListener and have it send trace messages to a log file or database.
To implement your own TraceListener, create a new class that inherits from System.Diagnostics.TraceListener, implement a Write and WriteLine method, and add an instance of your class to the Trace.Listeners collection. Listing 2 demonstrates a simple TraceListener and Listing 3 demonstrates how to start listening.
Listing 2: Implementing a stripped down TraceListener.
Imports System.IO Public Class MyListener Inherits System.Diagnostics.TraceListener Public Overloads Overrides Sub Write(ByVal Message As String) Console.Write(Message) End Sub Public Overloads Overrides Sub WriteLine(ByVal Message As String) Console.WriteLine(Message) End Sub End Class
Listing 3: Listening to our FiringSystem class.
Public Class FiringSystem
Private Shared Listener As MyListener = New MyListener()
Shared Sub New()
If (Not Trace.Listeners.Contains(Listener)) Then
Trace.Listeners.Add(Listener)
End If
End Sub
'... Elided for clarity
MyListener provides a stripped-down TraceListener. Our listener will hear Trace.Write and Trace.WriteLine statements that are called with string arguments and send these to the Console. To begin listening, we add a shared constructor to the FiringSystem class. The shared constructor is called before any other code is run; the shared constructor stuffs exactly one instance of MyListener into the Trace.Listeners collection.
For more on TraceListeners, read the December 2002 codeguru.com article “Implementing a Custom TraceListener.”
Testing with NUnit
Assume that our code now is sufficiently implemented and we want to begin testing. NUnit is built with .NET and employs the great .NET framework to make testing easy. (Refer to “Testing Visual Basic .NET with NUnit” on codeguru.com, February, 2003.)
At this point, we can write a couple of tests: We can test that Aim works and we can test to ensure that Fire throws the NotImplementedException. (We want to delay actually firing until we have really bullet-proofed this code. So, in the example, we are testing that we won’t accidentally fire yet.)
To implement an NUnit test, you will need to download and install NUnit from www.nunit.org , create a class library, and add a TestFixture class and a couple of test methods. Listing 4 demonstrates.
Listing 4: Implementing a NUnit test for the FiringSystem class.
Imports NUnit.Framework <TestFixture()> _ Public Class Test <SetUp()> Public Sub Init() End Sub <TearDown()> Public Sub Deinit() End Sub <Test()> Sub AimTest() Dim FS As CanYouHearme.FiringSystem = _ New CanYouHearme.FiringSystem() FS.Aim(1000, 90, 10000) End Sub <Test(), ExpectedException(GetType(NotImplementedException))> _ Sub FireTest() Dim FS As CanYouHearme.FiringSystem = _ New CanYouHearme.FiringSystem() FS.Fire() End Sub End Class
In the example, we stubbed out some setup and teardown code and two tests. The first test—AimTest—creates an instance of the FiringSystem class and calls Aim. The second test is tagged with the TestAttribute and ExpectedExceptionAttribute. The first attribute indicates that FireTest is an NUnit test, and the second attribute indicates that we should get a NotImplementedException from the FiringSystem.
After we compile the test library, we can load it in NUnit and run the tests. All greens in the GUI version of NUnit means that all of our tests passed (see Figure 1).
Figure 1: Green means a test passed; all green is all good.
Eavesdropping in NUnit with a TraceListener
We have one last thing to do. Remember all of that great Trace code we added to the FiringSystem? Why should we let it go to waste? We can add a TraceListener to our NUnit test library and watch what is going on in the background as our code is running in NUnit.
To add a TraceListener, we can copy and paste the MyTraceListener class into the module containing our NUnit tests and wire the TraceListener into NUnit. Listing 5 demonstrates the complete addition.
Listing 5: Adding a custom TraceListener to NUnit.
Imports System.Diagnostics Imports NUnit.Framework <TestFixture()> _ Public Class Test Public Class MyListener Inherits System.Diagnostics.TraceListener Public Overloads Overrides Sub Write(ByVal Message As String) Console.Write(Message) End Sub Public Overloads Overrides Sub WriteLine(ByVal Message _ As String) Console.WriteLine(Message) End Sub End Class Private Shared Listener As MyListener = New MyListener() <SetUp()> Public Sub Init() If (Not Trace.Listeners.Contains(Listener)) Then Trace.Listeners.Add(Listener) End If End Sub <TearDown()> Public Sub Deinit() Trace.Listeners.Remove(Listener) End Sub <Test()> Sub AimTest() Dim FS As CanYouHearme.FiringSystem = _ New CanYouHearme.FiringSystem() FS.Aim(1000, 90, 10000) End Sub <Test(), ExpectedException(GetType(NotImplementedException))> _ Sub FireTest() Dim FS As CanYouHearme.FiringSystem = _ New CanYouHearme.FiringSystem() FS.Fire() End Sub End Class
In the revision, we added an internal, nested class to our NUnit TestFixture. This nested class is a custom TraceListener. When a test is started, the subroutine tagged with the SetUpAttribute is run first; in our example, this puts the shared instance of MyListener into the Trace.Listeners collection. Now, when our tests are run, NUnit can eavesdrop, receiving Trace statements from our code and displaying them in the Standard Out tab of the NUnit GUI (see Figure 2).
Figure 2: The Trace statements are now sent to the Standard Out tab of the NUnit GUI.
Why did we get two Aim trace statements? The answer is that our original listener was implemented to write to the standard output device, the console. NUnit redirects the standard output device to the Standard Out tab of NUnit. Hence, if we implement a custom TraceListener that writes to the Console, we don’t need to put one in the test library itself. However, if you send Trace statements to the EventLog as an alternative, you can implement a custom listener, as shown in Listing 5, that writes to the Console object.
Summary
This article exemplifies what a framework can really do for us. The .NET framework permits us to tie everything together to create a synergistic and unified whole. The framework provides a lot of good classes, such as attributes, Console, Trace, and TraceListener, allowing the NUnit developers to extend the framework to build a great product, and we can tap into the framework from NUnit, yielding a cohesive suite of tests that uses existing instrumentation.
Writing unit tests with NUnit and instrumenting your code can yield excellent results, yet all of this code was relatively easy to write because it is supported by a coherent and cohesive framework. Achieving this same effect in VB6, for example, would be very difficult and time consuming, really placing VB6 developers at distinct a disadvantage to VB.NET developers.
About the Author
Paul Kimmel has written several books on Visual Basic, including the recently released Visual Basic .NET Power Coding from Addison Wesley. Pick up your copy at www.amazon.com. Paul is also available for short- and long-term consulting, public speaking, and .NET training. You may contact him at pkimmel@softconcepts.com.
# # #