Thursday, April 29, 2010

Catching the Disease - Test Driven Development with MXUnit

I had the pleasure of attending CF.Objective() 2010, my first, and it was a great experience. One of the sessions, led by Marc Esher, was about integrating Hudson, MXUnit and Ant. I later attended a BoF facilitated by Marc on Test Driven Development (TDD). In that session there was discussion of becoming "Test Infected"; that is, having unit testing be so ingrained in the development process that one wouldn't think of writing a line of code without it.

I wouldn't say I'm infected yet, but I'm starting to feel some symptoms.

A couple of years ago, I read about this TDD thing and it sounded like a good idea so I downloaded a test harness and started playing around with it. I even began writing my tests before writing my objects just like the process described. It was a miserable failure.

I wrote the tests, got little green "pass" bars and created my objects. Woohoo! But what did all those extra lines of code get me? At the time it seemed like I spent way too much time trying to figure out what to test and how to test it and not enough time actually building functioning code. In short, I just didn't get it - I must have been immune to the infection.

Catching the Disease
A few years passed and I began reading about this testing thing again. I had need to test some objects against a third party integration that was being upgraded and decided to try out MXUnit to write the tests; I figured it would be a better way to go than just manually executing the functions and dumping the results. I was right - it was better. Way better.

Not only did I get little green bars (woohoo!) I also had a test that I could execute every time there was an upgrade or I wanted to change a bit of code for this object. I was getting warm - could that be a slight fever coming on?

Tests are NOT TDD
I was beginning to see the value in scripted testing but writing tests isn't the same as Test Driven Development. The process of TDD means writing your tests before your objects. The idea is that you know what you want your object to do or return, so write a test for it. These tests will fail because...the object doesn't exist yet. Our job as developers is to make the tests pass and in doing so writing an object that does what it's supposed to do.

Recently I needed to create an object to return a list of courses from an online learning service. The object would return both a list of courses and an individual course by ID. It was a web service, so it needed to create the web service object which I would use to return my results. I knew what I wanted the object to do and return, so now I could write the tests.

I started with a basic test - testCourseService.cfc. I wrote a test for the first function which I called "testGetCourses". I expected the function to return an array of courses so I needed to a) invoke the web service and b) execute the web services function to return results. I realized at this point, I would need to test that the web service returned the expected object. I continued to write tests and add functionality in tandem until I got the tests to pass. In the process, I got more than a functional object - much more.

Order Now and We'll Throw in the Documentation...For Free!
Aside from the comfort of knowing my object functions in the desired manner, I also get a host of other benefits:
  • Tests document the desired behavior for the object
    By recording the expected behavior of each function, I can now look at the tests and see the intent. For the CourseService mentioned above I wrote a test for a getCourse method that included a test for a bad ID, no ID and a valid ID being passed into the function. In each case the expected behavior was indicated in the test.

  • Tests Illuminate Architectural Decisions
    In my head, I had assumed I would need a DAO for my CourseService to abstract the data access functions. I was thinking that the service would be the public interface and I would hide the internals in another object. The tests led me to drop that idea and keep everything in one object. This was a very simple component - in more a complex situation, the tests would help me understand how to design the package and dependencies before actually getting too far into the code an realizing the architecture needed a refactor.

  • Speaking of Refactoring...Tests Make Refactoring Easier
    It's inevitable. You'll want or need to refactor your code eventually. Either there's a better algorithm, redundant code or new technology you want to take advantage of. Regardless of the changes you make, the tests should still pass. If they don't, then that tells you something about your changes. Of course, you may need to write additional tests to accommodate those changes, but when you get those green bars, you're good to go.

  • Serendipitous Decoupling
    Not only would it make a great band name, it's also an additional benefit of unit testing. Objects need to be tested as units, meaning each individual unit is tested apart from any of its dependencies or objects dependent on it. Because of the need to test our objects in isolation, we end up with a more flexible design that tends to follow proven design patterns.

  • What Am I Missing?
    I'm sure my list is incomplete - and maybe inaccurate. What am I missing? What did I get wrong? I'd love to hear your comments!
Much Still to Learn
So would I say I'm "test infected"? Yes, I think I've got the disease. I understand the principles behind TDD, I find testing valuable and have started to make it a regular part of my coding, but there is still much to learn. I think writing tests is as much an art as a science. Trying to figure out what to test and how to test it is a challenge and, let's be honest, it takes time to write tests.

Luckily, there are a lot of great resources out there to help us out, some of which are listed below, and like any new skill it takes practice - a lot of practice. I'm planning on making this an ongoing series to help document my trials and tribulations in the world of unit testing and TDD. If you have any suggestions or hints or think I've just gotten it wrong - let me know! I'm new to this TDD and I know others out there are too.

Further Reading
If you have other favorite resources, let me know. Happy testing!

No comments:

Post a Comment