Test-Driven Development

 < Day Day Up > 



Unit testing has been around for a long, long time. It was recognized as an essential part of developing software decades ago. However, in the last few years a new testing technique, test-driven development (TDD), has gained increasing prominence. TDD is one of the practices recommended by advocates of Extreme Programming (usually abbreviated as XP). One excellent introduction to TDD is Kent Beck’s Test-Driven Development: By Example (Addison-Wesley, 2003). Beck boils down TDD to two simple rules:

  • Write a failing automated test before you write any code.

  • Remove duplication.

That’s right—in TDD, you write the test before you write the code. Here’s the general plan of action in a bit more depth:

  1. Quickly add a test.

  2. Run all tests and see the new one fail.

  3. Make a little change.

  4. Run all tests and see them all succeed.

  5. Refactor to remove duplication.

TDD in Action

An example will make the TDD process more clear. Right now, there’s a bit of undone code in the middle of the Engine object:

 public void GetDownload(Download d)  {      string filename = string.Empty;      string foldername = string.Empty;      WebClient wc = new WebClient();      try      {          // ... some code omitted ...          // Use WebClient to do the download          wc.DownloadFile(d.SourceUrl, Path.Combine(foldername, filename));          // UNDONE: set file size in the Download object      }      catch (Exception e)      {          // Bubble any exception up to caller, with custom info          throw new DownloadException("Unable to download" ,d, e);      }      finally      {          wc.Dispose();      }  } 

The intent is clear: When the engine downloads a file, it should fill in the property of the Download object that records the size of that file. To get this working using TDD, I start by writing a new test in the DownloadEngineTests project:

 /// <summary>  ///    Make sure the engine properly sets  ///    the size of the downloaded file  /// </summary>  [Test]  public void TestDownloadSize()  {      Engine eng = new Engine();      eng.DefaultDownloadFolder = @"e:\Temp";      Download dl = new Download();      dl.SourceUrl = @"http://localhost/DownloadEngine/DLTest.txt";      // delete any existing file      try      {          File.Delete(@"e:\Temp\DLTest.txt");      }      catch (Exception e)      {          // no existing file to delete          // not a problem      }      // perform the download      eng.GetDownload(dl);      // verify the file's size in the Download structure      Assert.AreEqual(57, dl.Size);  } 

Not surprisingly, the test fails, as shown in Figure 5.4. Note that NUnit assumes that the first argument to the AreEqual method is the expected value and the second is the actual value.

click to expand
Figure 5.4: A failing test for new functionality

RULE

When using TDD, keep code changes as small as possible.

The next step is to make a small change—in fact, the smallest change that will make the test work. It’s important to not get ahead of yourself by writing a bunch of code at once. The goal of TDD is to make certain that all of your code gets tested by writing the tests before the code. The revised GetDownload method with code to fill in the file size:

 // GetDownload uses the information in a Download object  // to get the specified file from the Internet to a local  // hard drive. Any download errors are wrapped in a  // custom exception and returned to the caller, along with  // the failing Download object.  public void GetDownload(Download d)  {      string filename = string.Empty;      string foldername = string.Empty;      WebClient wc = new WebClient();      try      {          // This method should never be called without          // a Source URL          Debug.Assert(((d.SourceUrl != string.Empty) &&              (d.SourceUrl != null)),              "Empty SourceUrl",              "Can't download a file unless a Source URL is supplied");          // If no filename specified, derive from SourceUrl          if((d.FileName == string.Empty) || (d.FileName == null))          {              filename = Path.GetFileName(d.SourceUrl);          }          else          {              filename = d.FileName;          }          // If no download folder specified, use default          if((d.DownloadFolder == string.Empty) ||              (d.DownloadFolder == null))          {              foldername = DefaultDownloadFolder;          }          else          {              foldername = d.DownloadFolder;          }          // Ensure that we got a folder name somehow          Debug.Assert(foldername != string.Empty,              "Empty foldername",              "Download folder not set and no default supplied");          // Use WebClient to do the download          wc.DownloadFile(d.SourceUrl, Path.Combine(foldername, filename));          // Set file size in the Download object          FileInfo fi = new FileInfo(Path.Combine(foldername, filename));          d.Size = fi.Length;      }      catch (Exception e)      {          // Bubble any exception up to caller, with custom info          throw new DownloadException("Unable to download" ,d, e);      }      finally      {          wc.Dispose();      }  } 

After making this change, I compile everything, run the tests again—and they all pass. That shows me that not only did I implement the file size code correctly but that I didn’t break anything else in the process. Now I can move on to other tasks.

Effects of TDD

It’s pretty typical to start writing more code when you commit to TDD. I find that my test harnesses run up to twice as large as the code that they’re testing. Of course, much of the test code is pretty routine; it consists of creating objects, invoking methods, and checking the return values. When I’m heavily into the TDD mindset, I might write a dozen tests for a new method. When I can’t think of anything else to test, then the method is done, and it’s time to go on to something else.

The key that makes TDD work (at least for me) is the discipline of writing the test before writing the code. That’s the only way that I know of to make sure that I really write the tests. Otherwise, they tend to be left until, well, later—and later seldom (if ever) arrives. Writing the tests first means the tests get written. It also means that I think about what I’m building and how it might fail. The delay of writing the tests gives me a little more time to plan and results in better code.

Writing better code is a major benefit of TDD, but it’s not the only one. I find that the most important plus to test-driven development is the sense of confidence that it gives me in my code. It’s difficult to describe this feeling unless you’ve experienced it. By writing many fine-grained tests, and knowing that the code passes those tests, I’m sure that it meets the requirements as embodied in the tests. This is especially critical when new requirements come up that I didn’t think of when I was starting out. Surely you’ve been in the situation where adding a new property required tinkering with code all over the place. Scary, wasn’t it? Well, with TDD, you can banish that fear forever.

The major problem in tinkering with your code is that you might break something unexpectedly. But if you’ve been doing TDD, you will have tiny tests that cover every bit of code you’ve written. In that case, you can make your changes and run your tests. Either they’ll all pass (great!), a few will fail and you’ll figure out how to fix them, or things will be horribly broken— in which case you can toss out your changes and start over (another reason to use source code control!). What you can avoid is the horrible uncertainty of not knowing whether things are broken. That confidence translates into a direct productivity boost.



 < Day Day Up > 



Coder to Developer. Tools and Strategies for Delivering Your Software
Coder to Developer: Tools and Strategies for Delivering Your Software
ISBN: 078214327X
EAN: 2147483647
Year: 2003
Pages: 118

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net