Special Reports

Write Unit Tests

Unless you test your application fully, you can't be sure changes you make won't affect another part of the system. Use unit tests to improve programming and application-release quality.

Technology Toolbox: VB.NET, C#

You might be familiar with the all-too-common experience of making a change to an application, testing it, and releasing it, only to discover that the change you made broke something elsewhere in the app. Unless you test your application fully, you can't be sure changes you make won't affect another part of the system. In some organizations, quality assurance (QA) departments are responsible for catching knock-on effects. However, the release-to-QA, fix, release-back-to-QA cycle can take a considerable amount of time. Besides, not everyone has a QA department. You need a methodology for making changes that minimizes the risk of unforeseen side effects. Enter unit testing.

The premise for unit testing is that you write discrete tests to test your application's functionality. You run your unit tests after you've made any changes to the system. If all tests pass, you can be confident the application still executes as you intended. You still have no guarantee it will, because the change might cause a problem a unit test doesn't cover. And unit tests can't replace system tests QA departments write and perform. But unit testing is a useful safeguard.

You write tests for each method belonging to a class that exhibits some type of measurable effect. The scope of method types you'll test is up to you. I usually write tests for each public/protected method and for any private methods I think play an important role in the class's functionality. You can group unit tests together to form test suites, which a unit-testing tool can then execute automatically.

My introduction to unit testing came when I began working on an Extreme Programming (XP) project (see the sidebar, "Take Programming to an Extreme"). I'd been writing tests for my code for several years, but most had been full component tests. Each test that I'd written had performed some complex processing and returned a measurable result. With unit tests, you write fine-grained tests for each piece of functionality. For example, if you have a component consisting of one public class and 10 private classes, you write unit tests for all 11 classes, not only the one public class.

You need to use an automated unit-testing tool to write effective unit tests. A huge range of tools isn't available for .NET at the moment. For the purpose of this article, you'll use dotUnit—an open source port of JUnit, a popular Java unit-testing tool you can download (see Resources). (If you search for "unit test" at www.opensource.net, you'll see that unit-testing tools are available for most languages, including ASP.NET). This article's example code is in C# (you can download it and a VB.NET version here).

Many applications need to remember a set number of recently accessed items—often referred to as a Most Recently Used list (MRU). Microsoft Word's File menu is a perfect example; as a user opens documents, Word adds the filenames to the File menu. When the MRU's capacity is exceeded, the file the user accessed the longest time ago is removed from the list. For this article's example, you'll write an MRU class that supports this type of functionality in a generic manner.

Before you start developing the class, though, you need to decide when you'll write your unit tests. You can choose among three approaches: Write the full class and then create the unit tests; write the class a method at a time, creating unit tests as you go; or write each unit test first, one method at a time, and then write the required class methods.

Write Your Tests First
You'll use the third approach for this article. Writing tests before the implementation class itself might seem like madness. I did think it was crazy the first time I used this development style—but writing unit tests before you code is a major aspect of XP, and now I believe it's the best way to tackle development. It forces you to think exactly about how a client needs to use your class (the sequence of calls, input parameters, and return parameters). This helps you avoid writing extra functionality for classes that's never used.

You know intuitively you need to be able to set the MRU's capacity, add items, remove items, and clear the entire MRU. These requirements seem to mimic a specialist array, so you might assume, logically, you can create a specialized ArrayList subclass. You must create a unit test C# project to start. Open Visual Studio .NET and create an empty Class Library project called MRUUnitTests. Add a reference to dotUnit.dll. (You might need to browse to locate it—by default, it should be in C:\Program Files\dotUnit\bin.) Rename the default class MRUTestClass and make it a subclass of dotUnit.Framework.TestCase. This class will contain all the unit tests for your MRU class. The dotUnit.Framework.TestCase class, which dotUnit supplies, provides the necessary testing capabilities.

Add a constructor that accepts a single string parameter. This is required for dotUnit to use this class. This constructor calls the matching constructor on the superclass:

      public MRUTestClass(string name) : base(name)
      {
      }

The first test you write will ensure you can add items to your MRU:

      public void TestAdd()
      {
         MRU mru = new MRU();
         mru.Add(new Object());
         AssertEquals("Check Add method", mru.Count, 1);
      }

dotUnit's name must begin with Test in order for it to invoke your test method. The AssertEquals method is inherited from the TestCase superclass and checks that your MRU contains one item following a single call to Add. Assume the MRU's default capacity is one. Now you'll write your MRU class to satisfy this test. Add a new Class Library project to the current solution and call it MRUProject. (You use a different project for the classes you're testing because you don't want to include your test classes in your final distributable.) Rename the default class MRU and make it a subclass of System.Collections.ArrayList. Now write your Add method:

      public override int Add(object obj)
      {
         if (Contains(obj)) 
            Remove(obj);
         else if (Count == Capacity)
            RemoveAt(Count - 1);
          
         Insert(0,obj);
         return  0;
      }

.aspx" target="_blank">
Figure 1. View Testing Results.

The preceding code checks to see if the item you wish to add exists already in the MRU. If it does, you need to move that element to the beginning of the MRU. You achieve this by removing it from its old position and inserting it at the head of the array. If the MRU is full already, you must remove the last element. Notice that you have to specify to override the Add method of the base ArrayList class so you can implement the required behavior. The ArrayList class provides the Contains, Remove, RemoveAt, and Insert methods. The final two steps you need to take are adding a reference to the MRUProject to the MRUUnitTests project, and adding a "using MRUProject" statement to the MRUTestClass so it can see the new class.

Run Your Test
To run your test, build the solution and then launch dotUnit. Click on the Assembly button, select the MRUUnitTests.dll from the MRUUnitTests project's bin\debug directory, then click on Go. dotUnit executes your test and returns a 100-percent pass rate. Now you can write and run all the unit tests for your MRU class (see Listings 1 and 2 and Figure 1). Notice how the test class also includes tests that test the functionality of the base ArrayList class. This is a good idea because classes you use in your designs might not function exactly as you imagine.

Your MRU class is nice and compact, but it allows a client object to interact directly with the parent ArrayList; for example, a client object could call the AddRange method. You could override each method you don't want to expose and throw an exception, but that technique is cumbersome. It might be better to write the MRU class not as a subclass of ArrayList, but rather as a simple class that contains an ArrayList object. This way, you can control all the access a client object has (see Listing 3). In this implementation, you use a private items object to contain the MRU objects. This is better design because you can control completely how the class is used.

Now rerun your unit tests on the reworked class. The unit tests still all pass. Even though you've made a significant structural alteration to the class, you can be sure its external behavior hasn't changed. Such is the power of unit tests.

Sometimes you'll discover that changes you make to your code will break your unit tests. Whenever one of your tests fails, it's important to discover whether the application or the test is at fault. It might be necessary to rewrite your tests in such situations. (Rewriting is called refactoring in XP.) As tiresome as rework can be, it's important for maintaining your tests' validity. Having to rewrite tests has often forced me to think about whether the change I made was a good idea. Another good practice is to be alert when bugs occur in situations your unit tests don't test. When that happens, you should write a unit test for the situation so the bug doesn't crop up again in a later release.

Unfortunately, you can't write unit tests for every aspect of your apps. Tests for timely processes (such as separate threads) are extremely difficult to write, as are tests for application GUIs. Although tests for GUIs are possible, I tend to rely on the system tests our QA department has developed to catch GUI-related defects.

The ability to run unit tests automatically allows you to implement system additions and changes without testing the entire application manually. Unit tests can do more than prove your application still works after a system change. You can run your tests throughout the day to ensure you won't encounter any nasty surprises after you complete the current unit of work. If a simple change to your code breaks an alarming number of unit tests, it might be time to reconsider your approach and attempt an alternate solution. The developers in my organization run their unit tests twice a day, thereby ensuring the current build is valid. We don't make a release available to QA unless it passes all the unit tests. If a change results in a broken unit test, we either fix the tested code or modify the unit test. Only when we're back up to a 100-percent pass rate do we implement the next change.

comments powered by Disqus
Most   Popular