Test-Driven Development from a Conventional Software Testing Perspective, Part 2
After my experience of test-driven development (TDD) immersion described in part 1 of this series, I was ready to take the next step in my learning. I had acquired some basics on how to do TDD from an expert practitioner, but realized that I still had much more to learn.
As my TDD teacher said, "Practice makes perfect." I needed to do more programming, but in a strict TDD way, so I dabbled here and there as I programmed doing test automation with Ruby. I grew comfortable with the Ruby Test::Unit automated unit test framework, and practiced writing a test and enough code to make that test pass. I was ready to take my TDD practice to the next level, so when I had an opportunity to do more test automation work, I jumped at the chance. After all, test automation is software development, so, as a software tester who does test automation work, this seemed like a great place to try applying TDD so I could learn more.
TDD and Conventional Testing Work
After working as a tester, I decided to use TDD on a test automation project myself. I had a task to program a testing library that other testers would use to make their test automation work easier and less susceptible to product changes.
I started off with a spike, writing experimental code I would use to build a proof of concept, and then throw away. Once I was comfortable with the environment and related libraries I needed to use, I set the spike aside and started afresh. The spike gave me the ability to think of a basic design to get started. I found that I couldn’t just start coding completely cold with a test, like some of my TDD friends do. The spike gave me the ideas I needed to be able to start writing the first tests. Once I had learned enough about the environment through the spike, I got rid of that code.
To start developing my custom library, I wrote a test, and with confidence came up with a method name for the yet-to-be-developed production code. I ran the test and got a red bar. The error message told me it couldn’t find that method, so I wrote the method, and added the necessary include statement so the automated test harness could find it. It failed again, but this time it failed on the assertion, not because it couldn’t find the method.
I was on a roll. I added more code to my method and presto! When I ran the test, it passed with a green bar. Remembering the "do an opposite assertion" trick I’d learned from my developer friend, I added an assertion that did the opposite. This was a simple method, and it returned a Boolean as a result, so my assertions were "assert this is true" and "assert this is false." Something happened, though: Both passed, when they shouldn’t have. I had a false positive on my hands, which was even more serious than a test failure.
Some investigation showed me a fatal flaw in my new method. It was returning something of the wrong type, but my test harness was interpreting it as a Boolean. I changed my tests so they would catch this problem more easily, changed my method, and the tests passed correctly. I then created some simple test data so my tests would run quickly and not use hard-coded values, and I reran the tests. I found a couple of failures because the test data exposed weaknesses in my code. In short order, I took care of these weaknesses and added some new tests for the trouble spots.
Continuing on this path, I had a handful of methods, but it didn’t make sense to leave them as a collection of methods. It was getting awkward to call them, and a couple of groupings had emerged within them. It made more sense to have these methods be part of objects, so I created two classes and put each group of methods in them. I added setup and teardown methods to my automated unit test suite, which would create new objects, and then I could call these methods in my unit tests.
Soon all my tests passed again, after a couple of failures revealed some errors. Running the automated unit tests gave me confidence, and I could change code like this fearlessly. I renamed a lot of my methods so other people would find them easier to use, and refactored regularly now that I had a better understanding of the emerging design.