Practical .NET

Test-Driven Development, Generate from Usage and Continuous Testing

Use Test-Driven Development, generate from usage and a free Visual Studio add-in to let you spend more time doing what programmers want to do: writing code that works.

If my experience with my clients (and with the participants in the courses I teach) is any guide, you're probably not using Test-Driven Development (TDD). One reason developers don't use TDD is because it's presented so badly. For instance, even though most developers spend most of their time modifying existing code, descriptions of TDD seem to assume you're doing new development. To make matters worse, most descriptions of TDD don't describe how it integrates with the rest of the features in Visual Studio (including generate from usage) to increase the amount of time you can spend writing code.

I'm going to walk through what it's actually like to work with TDD using something similar to a real-world problem. The problem is an enhancement to an application that, among other things, calculates shipping costs. Up until now, the application has used only one shipping method; but the company has decided to support an express shipping method that requires a different calculation. It's probably reasonable to suppose that, once this particular gate is opened, the company will start adding other shipping options.

When I check out of source control the Visual Studio solution that contains the application, I find that it contains two projects: the UI (a Windows Form or an ASP.NET project) and a class library containing the classes the UI requires. The key issue for using TDD is whether the application has classes separate from the UI -- it doesn't matter whether those classes are in a separate library (as in this case).

In the class library, I find a class called Product that has a method called CalculateShippingCosts. This method, when passed a destination, returns the cost to ship the product to the destination. Typical code to call this method in the UI might look like this:

Dim prod As Product
prod = New Product("A123")
Dim ShipCost As Decimal
ShipCost = prod.CalculateShippingCost("Winnipeg")
Checking for the Right Answer

My first step is to create some tests so after I've made my changes, I can check that my new code produces the same answers as the old code. I begin by going to the File menu and selecting Add | New Project. In the New Project dialog, under the Test category, I select Test Project and give this new test project the name Product-ShippingCostOrig (the "Orig" is for "Original"). When the project is added to Solution Explorer, rather than modify the default UnitTest1 class, I delete it. I then right-click on the test project and select Add | Unit Test.

This brings up the Create Unit Test dialog, shown in Figure 1, which lists all of the other projects in the solution. While I might be delighted to create a complete set of tests for every class in every project, my client probably doesn't want to pay me for that kind of effort. All I really need are tests around the method I'm modifying.


[Click on image for larger view.]
Figure 1. The unit test dialog will write the test skeletons for the members you will be altering on any existing classes.

So, in the dialog, I drill down to the CalculateShippingCost method on the Product object and select it. In the dialog, I also click on the Settings button and uncheck the default setting that marks all tests as inconclusive (that generates a message saying that I must verify the results of the test -- I expect TDD to execute my tests and verify my results without my intervention).

After clicking the OK button, Visual Studio adds a new class, called ProductTest, to my test project (the name of the class is generated based on the class I selected in the dialog). That's too broad a name for the tests I'm going to write, so I rename the class StandardShipping. I delete all the code in the class except for the method that's been written for me, which is called CalculateShippingCostTest. This method contains the suggested code for a test of the method.

I modify the generated code so that it creates product "A123" and passes the destination "Winnipeg" to the CalculateShippingCost method. I know the right answer for shipping A123 to Winnipeg is $235, so I make that my expected answer. I also add a message in my Assert statement to document what the test is supposed to prove. My resulting version of the generated test code looks like this:

<TestMethod()> _
Public Sub CalculateShippingCostTest()
  Dim ProductId As String = "A123"
  Dim target As Product = New Product(ProductId)
  Dim Destination As String = "Winnipeg"
  Dim expected As [Decimal] = New [Decimal](235)
  Dim actual As [Decimal]
  actual = target.CalculateShippingCost(Destination)
  Assert.AreEqual(expected, actual,
    "Wrong result on simple calculation")
End Sub

As you can see, whoever wrote the code generator for the test code seems to have been paid by the keystroke. I don't like wordy tests because the more lines of code I have in a test, the more likely it is I'll have a bug in my tests. I prefer to write lots of terse tests that are easy to check.

But now that I have my first test written, I should see if it works. I right-click in my test routine and select Run Tests. The test runs and I get a green light in the Test outputs section. Success!

Ensuring Code Works
But there are cases where the shipping cost code is "tricky" -- where it took the original developer several tries to get the calculation right. It's likely I'll have bugs in those areas also. So I add some additional tests for those cases, which includes sending a Category J item to Panama City. My terser test code is just three lines long:

<TestMethod()> _
Public Sub CategoryJToPanamaCity()
  Dim prod As Product = New Product("J42")
  Dim result As Decimal = prod.CaculateShippingCost("Panama City")
    
  Assert.AreEqual(415.1D, result,
    "Wrong result on J category to Panama City")
End Sub

I also want to make sure that my new code raises the same errors the old code raises. For instance, if I pass the existing code an invalid destination, it throws an ArgumentException. I create a test that succeeds if that error is thrown and flags an error if the error isn't thrown by using the ExpectedException attribute:

<TestMethod()> _
<ExpectedException(GetType(ArgumentException))>
Public Sub BadDestination()
  Dim prod As Product = New Product("A123")
  prod.CalculateShippingCost("???")
  Assert.Fail("Should have blown up on bad destination")
End Sub

When I start writing my new code, I can now (with a single click) prove that my code works just like the old code.

The alternative is to stop writing code so that I can run the actual application to test my code. But to use the actual application, I'd have to manually enter my test data into the application's forms and check the result in whatever part of the application displays it (which might mean spelunking through database tables). And I'd have to do that right every time. Odds are that, to get the application to run, I'll have to enter much more data than needed to actually test my changes (and do that on every test).

Using TDD, I can write (and execute) just enough code to test my changes without any intervention on my part -- no data entry and no data checking. With TDD, I just alternate between writing code in my tests and in my application, occasionally stopping to build my solution and execute the tests.

Designing the Solution
I'm now ready to start generating new code. One solution for supporting the new shipping method is to add a new parameter to the CalculateShippingCost method that specifies the shipping method.

Because I expect this to be the first of several new shipping methods, I'm going with a different design. I'll create two ShippingCost objects: one to calculate the standard shipping cost and one to calculate the express shipping costs. This means I have to move the existing calculation for the standard shipping code out of the Product class and into a new ShippingCost object.

This kind of refactoring of existing code is inherently dangerous. While this new design better supports what the application now has to do, I could introduce a new error into existing, working code where none existed before. However, the tests I've created assure me that my new code does exactly the same thing that my old code did. On that basis, I'm willing to modify working code. In fact, one of the benefits of this new design is new ShippingCost objects can be added in the future without having to modify existing, working code.

There's another reason that I'm adopting this design: by putting my new code in a separate class, I can test it independently of the existing code. Looking ahead, my testing now breaks down into four independent stages:

  1. Prove the revised version of code works
  2. Prove the new express shipping code works
  3. Prove that I can swap between ShippingCost objects
  4. Prove that the application can use the new (fully tested) classes

To allow the application to switch between ShippingCost objects, they need to look alike. I could declare a base ShippingCost class and have the various ShippingCost classes inherit from it. However, a quick look at what's required for the new express shipping method shows that it doesn't share much processing with the existing, standard shipping method. Because there's no common code I could put in the base object, I define an interface which I'll call IShippingCostCalculator.

I start by creating a new test project for my new code. I could rewrite the old tests to work with my new design, but I'd prefer to keep the first set of tests around in case I ever have to demonstrate what the original Product code produced.

I call my new test project ProductShippingCostCurr. As before, I delete the default test class; but this time I just drag the StandardShipping cost test class from my Orig project into my Curr project. This creates a copy of the class that holds all the tests that my new version of the standard shipping code must pass. I have to add a reference to the class library containing the Product object, but once I've done that, I can rerun all my tests and prove that everything still works.

The problem is that once I start rewriting the Product object, the Orig tests will stop compiling. I right-click on the Orig project and select Unload Project. If I ever need the tests, I can get them back; but until then, I don't need them.

Generating Code from Tests
I start by rewriting my tests, not my code. By writing my tests first, I concentrate on how I want to use my classes, rather than getting caught up on the inner workings of my logic. For my first test, I need to declare a variable using my new interface that will point at any ShippingCost object, so I add this line to my new test:

Dim sc As IShippingCost

Of course, because I haven't written the interface, I get an error message that "IShippingCost is not defined." From the error's smart tag I select the option to "Generate Interface IShippingCost" and let Visual Studio write the code for the interface. But, unfortunately, Visual Studio generates the interface in my test project. To fix that, in Solution Explorer I drag the interface to the class library I want it in, then delete the original version from my test project. I also have to convert the generated code from its default scope of Friend to Public. That gives me this code:

Public Interface IShippingCost

End Interface

Back in my test method, I declare a StandardShippingCalculator object and call the CalculateShippingCost method I want the class to have, passing a Product object and a destination:

Dim sc As IShippingCost
sc = New StandardShippingCalculator
Dim prod As Product = New Product("A123")
Dim result As Decimal
result = sc.CalculateShippingCost(prod, "Winnipeg")

Assert.AreEqual(235D, result)

After I write the code in my test, I go to the SmartTags for the various errors to generate my code. However, to avoid having to drag classes around when I'm done, I select the Generate New Type option. This brings up a dialog box (Figure 2) that lets me specify the scope of my class and what project to put the class into.


[Click on image for larger view.]
Figure 2. The Generate New Type dialog lets you tell Visual Studio what code to add to your projects.

Visual Studio is smart enough to add my CalculateShippingCost method to my IShippingCost interface. Unfortunately, it's not smart enough to have my class implement my interface. However, all I have to do is have the class implement the IShippingCost interface and, again, Visual Studio will generate the method for me -- I'll do that when I write the code for my new method.

But I don't write that code yet. Instead, I run my new test. Of course, because I haven't put any code in my method, my test fails. This is actually critical: A good test fails at least some of the time. Early in my TDD career, I wrote a test with a bug that ensured that any code would pass the test. I then wrote some buggy code that, of course, my "never-fail" test didn't flag -- it was several hours before I tracked down the problem. Running my tests before I write any code demonstrates that, at least under some conditions, the test reports a failure.

Now that I've worked out how I want to call my new code and have proven I have a good test, I copy the original code from the Product class and paste it into my new method. I then rewrite that code to work in its new environment. Once I've gotten the code to build, I run my test and … it fails, not surprisingly. I admit it: It's an unusual situation for me when code works the first time I run it (your mileage may vary).

I use the Test menu's Debug | Tests in Current Context to run my code in debug mode to step through this first test and figure out my problems. When I finally pass the test, I rewrite the next of my copied tests to use my new class and run it. If I fail, it's back into the method to find my problem.

Once I get each test to pass, I click in my test class outside of any test methods and select Run Tests -- this runs all the tests in the class. Every once in a while, I find a test I passed earlier is now failing, indicating that a recent change has introduced a new bug. However, because I've probably made that change within the last 15 minutes, it never takes me long to track down the problem. Figuring out a solution sometimes takes longer, of course.

Continuously Testing New Objects
Having rewritten my standard calculation, I can start on the express method, by writing the tests I need. I create a new Unit Test (called ExpressShippingTest) and add an Imports statement for the class library holding the objects I'm testing. My first step is to write a test that proves the object can be created:

Dim sc As IShippingCost
sc = New ExpressShippingCalculator

Again, I use the error's SmartTag "Generate New Type" dialog to generate the class, press F12 (Go To Definition) to go to the class and add the Implements statement so Visual Studio will generate the method.

My second test doesn't go much further than my first test -- it just proves that I can call the method (I don't bother checking the result):

Dim sc As IShippingCost
sc = New ExpressShippingCalculator
Dim prod As Product = New Product("A123")
sc.CalculateShippingCost(prod, "Winnipeg")

My third test actually does something useful -- it proves that I can calculate shipping costs. I don't yet know what the right answer is for the express shipping method, but I do know it shouldn't be zero, so that's what I test for. Of course, because I haven't written any code, the first time I run the test, it fails. But that assures me that this is a valid test:

Dim sc As IShippingCost
sc = New ExpressShippingCalculator
Dim prod As Product = New Product("A123")
Dim result As Decimal = sc.CalculateShippingCost(prod, "Winnipeg")
Assert.AreNotEqual(0D, result)

I can now start to add to my new class the code to calculate express shipping. I start converting the specifications for the express shipping method into tests (for instance: what do the requirements say shipping product A123 to Winnipeg should cost?). As soon as I write one test (and have it fail), I add the code to pass that test. Because each test is relatively simple, the code I write to pass the test is relatively straightforward … most of the time (some requirements are just difficult).

At this point, I'm getting tired of having to constantly run my tests, so I download and install the AutoText.NET Visual Studio add-in shown in Figure 3. AutoTest compiles and runs my tests in the background every time I save my code. When there's an error, either in your build or your tests, it's displayed in the AutoTest.NET window in Visual Studio. With two keystrokes I'm sitting in the offending code; with one keystroke I'm in debugging with a breakpoint set on the offending test. So now I really am just writing code, and occasionally stopping to debug when I've done something wrong.


[Click on image for larger view.]
Figure 3. AutoTest.NET is a free Visual Studio add-in that will build your project and run your tests while you write your code.

Unfortunately, when AutoTest.NET builds my solution, it also builds any unloaded projects, so I have to remove my original tests from the solution after checking them into source control. AutoTest.NET isn't perfect yet (I wish it structured its output messages better, for instance), but I like it enough to keep using it.

Eventually, I've converted all of the express shipping requirements into tests and they've all run successfully. It's time to move on.

Integrating with the UI
In the application, shipping costs are calculated in several different places in the application. Rather than have the code that picks ShippingCost objects scattered throughout the application, I'll create a ShippingCostFactory. That class will have a GetCalculator method; when passed the data to select a shipping method, it will return the right object. For this article I'll assume that data is an enumerated value (the real method must be passed three or four variables). I write my tests first, eventually creating tests that prove, when passed the enumerated value, the right calculator is returned:

<TestMethod()>
Public Sub GetStandardCalcuator()
  Dim sc As IShippingCost
  sc = ShippingCostFactory.GetCalculator(ShippingType.Standard)
  Assert.IsInstanceOfType(sc, GetType(StandardShippingCalculator), 
    "Wrong type (not standard) returned from factory")
End Sub

Using the Generate New Type dialog, if I select the errors in this code in exactly the right order (ShippingCostFactor class, ShippingType enumeration, Standard enumeration value, GetCalculator method), the generated code won't require any "fix ups" when I finally start writing my code (I don't usually manage that).

After writing the test that proves I can return a StandardShippingCalculator, I add the code to the GetCalculator method to pass the test. I then write a test to prove that I can return an ExpressShipping-Calculator, and a final test to prove that when I pass Nothing, I get Nothing back. In between writing tests, I write some code to pass the tests. In the future, as new ShippingCost objects are added, this factory is the only existing, working code that will need to be modified.

The final step is to delete the original CalculateShippingCost method from the Product class and rebuild the application to find all the places I have to replace existing code with something like this:

ShipCost = ShippingCostFactory.GetCalculator(stype).
  CalculateShippingCost(prod, dest)

Because I can't separate that code from the UI, I can't use TDD to prove this code. Given the only thing I have to do is make sure the right variables are passed to GetCalculator, my opportunity for error is very small.

The process for integrating TDD into application maintenance is simple:

  • Write the tests that will prove your new code gets the same results as any code you're replacing.
  • Isolate any new code into separate methods or classes that you can test independently of other code.
  • Break down the requirements into simple tests you know are valid.
  • As you pass each test, check to see that you haven't introduced a new bug.

While you're doing this, let Visual Studio generate the utility code for you and find a continuous testing tool to run your tests (AutoTest.NET is just one). Then spend your time doing what you like to do: writing code.

comments powered by Disqus

Featured

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

  • What's New for Python, Java in Visual Studio Code

    Microsoft announced March 2024 updates to its Python and Java extensions for Visual Studio Code, the open source-based, cross-platform code editor that has repeatedly been named the No. 1 tool in major development surveys.

Subscribe on YouTube