7. Corner Bugs
Even when we've written solid tests, once in a while someone using our code (a paying customer, a persnickety cubemate) discovers a bug. Don't let that stop you from continuing to write tests that catch the majority of bugs. Instead, use it as an opportunity to improve your testing skills. In the meantime, a bug has been reported and it needs to be fixed. Thankfully, you're able to quickly identify the suspect lines of code because you happen to have vast knowledge of the code base. So you fire up your favorite editor with fingers poised on the keyboard, ready to make the necessary repairs. But before you do that, don't let a golden opportunity to forever corner that bug pass you by.
How will you know when your code changes have squashed the bug? After all, if you're moments away from making a change, then you must have expectations about how the code will work after you've made the change. Writing code is a means to an end. Now is the time to turn your expectations into an automated test that will signify the end. The bug has been fixed when the test passes. Moreover, once the test passes, you have an automated way to keep the bug cornered for life.
8. Expand Your Toolbox
Often, we'd like to test something, but we just don't have the right tool for the job. We're short on time as it is, and spending precious time crafting a test harness is yet another reason not to test. Thanks to the work of others, there's no excuse for skimping on testing for lack of sufficient tools. The open source world is currently teeming with handy testing tools. It pays to be creatively lazy by looking around before reinventing yet another test harness.
Say, for example, you're writing a servlet that provides a shopping cart service. The intent of the servlet is to add the item and quantity specified in the request parameters to the shopping cart. You'd like to test that the servlet works, but the method you want to test requires an HttpServletRequest instance. You can't create one of those very easily. And if you have to crank up a J2EE server to put the servlet in a known state every time you want to run the test, you won't run the test very often. It's time to expand your toolbox to include the Mock Objects framework. The following JUnit test uses the Mock Objects framework to test the servlet outside of a J2EE server:
import junit.framework.TestCase;
import com.mockobjects.servlet.*;
public class ShoppingServletTest extends TestCase {
public void testAddRequestedItem() throws Exception {
ShoppingServlet servlet = new ShoppingServlet();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setupAddParameter("item", "Snowboard");
request.setupAddParameter("quantity", "1");
ShoppingCart cart = new ShoppingCart();
servlet.addRequestedItem(request, cart);
assertEquals(1, cart.itemCount());
}
}
Notice that the test presets the request parameters on a MockHttpServletRequest instance. That instance is then passed in to the servlet's addRequestedItem() method. When you run the test, your servlet is fooled into thinking that it's running in a servlet container. Later on, your integration tests will cast a wider net by validating that the servlet works in its native environment. But when you're writing the servlet code, using mock objects makes running the tests quick and painless.
So, before attempting to write a test harness from scratch or giving up on testing altogether, survey the tools others have crafted in their times of need. JUnit is a framework, not an application. By all means, if the standard JUnit assertion methods aren't enough, then write custom assertion methods. It's also relatively easy to write applications that build upon JUnit. JUnit.org maintains a list of existing JUnit applications and extensions. Don't stop with JUnit and its Java ilk. Many xUnit testing framework implementations for other languages and technologies are already there for the taking (visit XProgramming.com). If you can't seem to find what you're looking for, let Google be your guide. And if you do end up building a test harness, please share it so that others can expand their toolbox.
9. Make It Part of Your Build Process
A test is a valuable radiator of information. It documents -- in an executable format -- how code works. You don't have to trust that the documentation is correct; just run the test for yourself. If it fails, the output tells you straight up that the code doesn't work as the test promises. So once you've written a passing test, treat it with the respect it deserves by checking it in to your version control system. Then capitalize on the investment by running the test as part of your team's build process.
While you're grooving in the test-code rhythm, it's convenient to use the JUnit test runner integrated into your favorite IDE. But you also need to externalize the build process so that anybody on your team, regardless of their IDE loyalties, can build and test the code on their machine. In the Java world, Ant is the king of the hill when it comes to making your build and test process portable. The following snippet of an Ant build.xml file uses the built-in <junit> and <batchtest> tasks to run all JUnit tests conforming to the *Test naming convention:
<path id="build.classpath">
<pathelement location="${classes.dir}" />
<pathelement location="${lib.dir}/junit.jar" />
</path>
<target name="test" depends="compile" description="Runs all the *Test tests">
<junit haltonfailure="true" printsummary="true">
<batchtest>
<fileset dir="${classes.dir}" includes="**/*Test.class" />
</batchtest>
<formatter type="brief" usefile="false" />
<classpath refid="build.classpath" />
</junit>
</target>
First notice the use of the <path> element to explicitly declare a classpath for the build rather than relying on the CLASSPATH environment variable being set correctly. This makes the classpath portable across machines. Second, notice that the test target is dependent on the compile target. So, to compile and test the code in one fell swoop, anybody on your team can check out the project from the version control system and type:
ant test
Finally, notice that the <junit> task is configured with haltonfailure="true". This means that the build process will fail if any test fails. After all, the build contains tainted goods if all the tests don't pass.
Why stop there? Now that you have an Ant target that compiles and tests the project, schedule the test target to be automatically run by a computer at a periodic interval. For example, using
CruiseControl or Anthill (both free) you can put an idle machine to good use running any Ant target as often as you'd like. Using a separate build-and-test machine implies that everything needed to build and test your project is under version control. You are using version control, aren't you? You'll be surprised how often a separate machine flushes out build problems. And if the build fails, those schedulers will even send you an email so that you can take appropriate action to get back on solid ground.
So, no matter how many tests you have, realize their value to your team early and often by making testing part of your process. Add each passing test you write to your version control system and run all the tests continuously to radiate confidence.
10. Buddy Up
When learning anything new, I've found it helpful to buddy up with another newbie. Besides being a lot more fun than trudging up the learning curve alone, together, you and your buddy can cover more ground. You can also keep each other accountable to the goals you share and challenge each other to become better. As you practice the techniques described in this article, openly discuss with your buddy your triumphs and struggles. Critique each other's tests and share design insights gained from code driven by tests. And when you feel pressure to slip back into old coding habits, a good buddy will bring you back from the brink.
So how do you find a buddy? It's been my experience that many folks secretly want to try test-driven development, but they don't want to be the only person on the team doing it. So start by expressing your desire to learn and practice test-driven development. By making this proclamation, you'll invite social support that can be a powerful motivator to help you follow through. Moreover, once you step into the spotlight you'll likely draw others out of the shadows.
11. Travel With a Guide
Sometimes buddying up just isn't enough. If you and your buddy are learning at the same time, you may both stumble into the same pitfalls. Traveling with an experienced guide will help you avoid getting bogged down. Don't feel that seeking outside help is a way of copping out. You'll be more productive if you don't have to blaze your own trails.
Consider arranging for training in unit testing or test-driven development to quickly put these techniques into practice. For this kind of training to be truly effective, it needs to be customized for you. For example, students I've taught have found short and focused sessions -- tailored and applied to the software they're building and the technologies they're using -- to be most beneficial. So look for training that covers the basic trails, but then lets you chose advanced paths of interest.
As you continue to practice test-driven development, you'll undoubtedly hit a few snags. Don't spend too much time fighting through them. A few minutes of one-on-one discussion with a mentor who's been there and done that will keep you on pace.
12. Practice, Practice, Practice
Writing tests first is a programming technique that takes practice, and lots of it. Accept the fact that you won't see miraculous results overnight. Experts say it takes a minimum of 21 days to build a positive habit and six months for it to become part of your personality. So when you feel yourself backsliding, don't despair. Just keep pressing on and pay careful attention to mental assertions you're making that could be codified in tests. Your brain will love you for it!
As with anything new, the more you practice, the better you get. Start simple by promising yourself to write just one good automated test a day. If you write more, it's bonus points. Tomorrow morning, you'll at least have one passing test. In a week, you'll have at least five. Run all your tests every time you change code, even if you don't think your change could possibly break anything. This will get you in the habit of running the tests often and build your confidence in the tests. Before long, you'll have a suite full of tests and you won't be able to confidently touch the code without running the suite afterward. Green bars are your reward for progress.
Summary
Getting started writing tests doesn't have to be difficult or time-consuming. Just wade in gradually by spotting practical opportunities to let your computer automatically check what you're already checking manually. Before writing new code, assert your expectations about what it should do. Along the way, listen to the tests for design insights. Make testing an integral part of your development process by writing tests first and making it easy for anyone on your team to run all the tests at any time. Finally, don't go it alone.
I hope these techniques help you get the testing bug this year. It's a resolution that's sure to improve your design and testing skills. Don't forget to relax by reminding yourself that every day you're just getting started. You'll quickly find that indeed you do have time to test, and then some.
Resources
JUnit Download Page
JUnit FAQ
Answers to the world's most frequently asked JUnit questions.
Pragmatic Unit Testing
Andy Hunt and Dave Thomas, The Pragmatic Programmers, LLC, 2003.
A must-have, easy-to-read book that will help you quickly start writing good JUnit tests.
"Lucene Intro"
by Erik Hatcher
The first of a two-part series of great Lucene articles. I buddied up with Erik to write the Lucene learning tests.