There is a lot being written about Test Driven Development in the .NET world. So much so that it can seem overwhelming to absorb, and you always feel like you’re entering in the middle of the conversation. Yep, me too, even four years ago when I first dipped my toe into the TDD pool.
After a couple years in SSRS/BI-land, it’s back to regular development, and back into the TDD pool. The pool has matured–there are new tools, stronger opinions, and much more conversation. Essentially, I’m starting over, but with a little experience. In this post, I’ll show you what I’m setting up, and how to get started with a simple project.
What to use?
There are a number of applications available, each with its own adherents and detractors. Some are free, some you buy. If you have Visual Studio 2008 Professional or better, you have a testing tool built in.
I tried a couple tools getting started, including NUnit and MbUnit. I preferred MbUnit, so I downloaded the latest version, part of the Gallio Automation platform. Gallio/MbUnit is one of the more popular TDD applications. It’s well supported and well documented, which helps greatly.
So what is Gallio and MbUnit? Simply understated, MbUnit v3 is a set of classes for unit testing, and Gallio is what actually runs the tests and displays the results. Gallio can run more tests than just MbUnit, and integrates with build tools.
Installation was a snap–I downloaded the installer and let it run. Gallio integrated with VS 2005 and VS 2008 automatically, but had an issue loading a language pack when I started VS 2008.
Planning what you’re coding
Before you start hacking away on the keyboard, you need to figure out what you’re writing. You can use whatever project management methodology you want. Here’s a user story for something I encountered recently:
For our space utilization analysis, I need to be able to easily take an order date, and figure out the date the week ends on, so I can accumulate inventory changes to the same base date. I need to be able to specify the end date, since calendar and fiscal weeks may have different end days. Dates should be calculated back from the given date. If the the weekday of the given date, and the week end day are the same, then the given date should be used.
Test first!
One of the tenants of TDD is writing your tests first, called the “red, green, refactor” cycle of development. Red refers to a test which fails, green refers to a test which passes, and refactor refers to reworking the code you wrote to make the test pass to make it leaner. This takes some realignment of your thought process, but gets easier as you do it.
Open Visual Studio, and create a new project. I’m calling mine DateLibrary, since I’m actually creating a library of date functions useful to me. Delete the automatically created Class1.
We need to separate the tests from the code we’ll use in our applications, so our tests don’t end up as part of the application. Add a second class to the solution, called DateLibraryTests to contain the tests. Again, delete Class1, and add one named WeekEndTest. Add references to your class library project, Gallio and MbUnit, and then add using statements, as shown:
C#:
using System; using DateLibrary; using MbUnit.Framework;
VB.NET:
Imports System Imports DateLibrary Imports MbUnit.Framework
Now, let’s write our first test. To reiterate, one of the tenants of TDD is writing your tests first. Realistically, you might need to write a little application code first. I prefer to have a little Intellisense guide my test writing, so I just stub out a class and methods, just enough so my app will compile, but no actual code. In our example, we need to return something, so we just return a blank date and time. Here’s the code stub:
C#:
using System; namespace DateLibrary { public class WeekEnd { public static DateTime BackDate(DayOfWeek EndDay, DateTime FromDate) { DateTime _backdate = new DateTime(); return _backdate; } } }
VB.NET:
Imports System Public Class DateLibrary Public Function BackDate(ByVal EndDay As DayOfWeek, ByVal FromDate As Date) As Date Dim _backdate As New Date Return _backdate End Function End Class
The first parameter of our method is the day to figure back to, and the second is the date to figure from. We use a DayOfWeek for the first parameter to limit input values.
One of the criticisms of TDD is that everything you want to test must be public, since tests are placed in a separate class. So if you plan on having private methods and classes, you’re either out of luck, or you can change the modifier after your tests are run, have public accessors to your private methods, or include test code in your application. What you do is up to you, based on the local coding standards and application design. For this sample, public methods are just fine.
Now we write our test. In TDD, Assert basically means “I am expecting these values…”. So, our test case could be thought of as “I am expecting these values are equal”. The values to be compared are the one we’ll calculate from our code above, using inputs which we already know the answer to, and the answer we already know. Our test looks like this:
C#:
using System; using DateLibrary; using MbUnit.Framework; namespace DateLibrary { public class WeekEndTest { [Test] public void WeekEndsSunday() { Assert.AreEqual(new DateTime(2009, 06, 21), WeekEnd.BackDate(DayOfWeek.Sunday, new DateTime(2009, 06, 25))); } } }
VB.NET:
Imports System Imports DateLibrary Imports MbUnit.Framework Public Class WeekEndTest <Test()> _ Public Sub WeekEndsSunday() Assert.AreEqual(CType(<span class="str">"6/21/2009", Date), WeekEnd.BackDate(DayOfWeek.Sunday, <span class="str">"6/25/2009")) End Sub End Class
The [Test] attribute is used by the test runner to find the tests from the regular methods.
Make sure the solution compiles without errors. If it does, it’s time to run our tests. Gallio/MbUnit includes the Icarus test runner, which provides easy to read graphical feedback. To start Icarus, navigate Start >> All Programs >> Gallio >> Icarus GUI Test Runner. Once Icarus has started, we need to load in our tests. Go to Project >> Add Assemblies, navigate to the bin folder of your test project, select the test DLL and click Open.
The tests will be parsed, and listed in a treeview. You can add as many test DLLs as you need, we just have the one for this example.
Click the Start button, and our tests will be run. The Execution Log shows the results, in this case, red, just like we expected.
Further down, you can see the expected and returned values. Our test failed (as expected) because a blank date was returned, and did not match the expected value.
Great. Like we said above, “red, green, refactor”. We now have red taken care of. Before we do anything else, let’s do a quick sanity check to make sure all systems are correct. In our test, we’re expecting the returned value to be 6/21/2009. As a test of our tests, let’s change our procedure slightly to make sure it returns the expected date, and run our test again. The results should be green.
C#:
using System; namespace DateLibrary { public class WeekEnd { public static DateTime BackDate(DayOfWeek EndDay, DateTime FromDate) { DateTime _backdate = new DateTime(2009, 06, 21); return _backdate; } } }
VB.NET:
Imports System Public Class WeekEnd Public Shared Function BackDate(ByVal EndDay As DayOfWeek, ByVal FromDate As Date) As Date Dim _backdate As New Date _backdate = "6/21/2009" Return _backdate End Function End Class
Sure enough, our results are green. This does not satisfy the green portion of “red, green, refactor”, this is merely to confirm our systems are working correctly.
Now it’s time to write some actual application code, and have our test be green for real. From the user story above, we arrive at the following code:
C#:
using System; namespace DateLibrary { public class WeekEnd { public static DateTime BackDate(DayOfWeek EndDay, DateTime FromDate) { DateTime _backdate = new DateTime(); int _dayOfWeek = new int(); _dayOfWeek = (int)EndDay; if (FromDate.DayOfWeek == EndDay) { _backdate = FromDate; } else if ((int)FromDate.DayOfWeek > (int)EndDay) { _backdate = FromDate.AddDays(-(int)FromDate.DayOfWeek - (int)EndDay); } else { _backdate = FromDate.AddDays(-(int)EndDay - (int)FromDate.DayOfWeek); } return _backdate; } } }
VB.NET:
Imports System Public Class WeekEnd Public Shared Function BackDate(ByVal EndDay As DayOfWeek, ByVal FromDate As Date) As Date Dim _backdate As New Date() Dim _dayOfWeek As New Integer() _dayOfWeek = CInt(EndDay) If FromDate.DayOfWeek = EndDay Then _backdate = FromDate ElseIf CInt(FromDate.DayOfWeek) > CInt(EndDay) Then _backdate = FromDate.AddDays(-CInt(FromDate.DayOfWeek) - CInt(EndDay)) Else _backdate = FromDate.AddDays(-CInt(EndDay) - CInt(FromDate.DayOfWeek)) End If Return _backdate End Function End Class
Now, we run our test again, to make sure our code does what we think it will do:
Success! This time, we’re green for real. The final part of the TDD is to refactor. Refactoring is a process of editing your code to make it more concise, more maintainable, and more reusable. It’s not an entirely simple process, requiring a good deal of thought. The best part about TDD is that as you refactor, you can easily tell if you’ve broken your code by running your tests again. It’s a process we won’t cover here.
For more information
To learn about and download MbUnit and Gallio, go to http://www.gallio.org/.
For a deeper look into TDD, the book recognized as starting it all is Test Driven Development By Example, by Kent Beck. The examples are in Java and JUnit, but this is the book recognized as the authoritative work on TDD.
Test Driven Development: By Example (Addison-Wesley Signature Series): Kent Beck: Books
ISBN: 0321146530 |
If you’d prefer to have examples in .NET, you might like Test Driven Development in Microsoft .NET. James Newkirk is a leader in Agile development, and is one of the founders of NUnit and xUnit.
Test-Driven Development in Microsoft .NET (Microsoft Professional): James W. Newkirk, Alexei A. Vorontsov: Books
ISBN: 0735619484 |
Excellent article. Thank you! 🙂