How I learned TDD
I first heard about unit tests at an open source developer conference in 2006 and since then I've been trying to understand this concept.
The first tests I wrote were with PyUnit, I don't remember what the project was, I just remember that it was horribly implemented, I didn't have any notion of the SOLID principles, so it was very difficult to write the tests and eventually I gave it up.
A couple of years later I started to develop a large enterprise application in .NET which forced me to improve my software architecture skills, I started reading a lot of books on the subject:
- Patterns of Enterprise Application Architecture by Martin Fowler
- Agile Principles, Patterns, and Practices in C# by Robert C. Martin and Micah Martin
- Programming .NET Components, 2nd Edition by Juval Lowy
- Architecting Applications for the Enterprise by Dino Esposito and Andrea Saltarello
As I was reading the books I began to realize how little I understand about software craftsmanship and how much more there is to learn.
So I started writing tests again (still after the code itself), I could see the value of the tests, since it helped the stability of the application I was working on (to this day there aren't any new bugs).
But the tests still didn't feel right, they didn't cover all of the scenarios, they were annoying to write, I kept having to change the original after adding the tests.
Later on I wrote a todo.txt desktop editor in Python and QT, I looked for a testing framework and found doctest which allows you to write the tests inside the code (or in simple text files), like this:
def add(x, y): """ >>> add(0, 0) 0 >>> add(0, 1) 1 >>> add(1, 0) 1 >>> add(1, 1) 2 """ return x+y
As you can see the docstring includes 4 tests (0+0, 0+1, 1+0 and 1+1).
This was my first attempt at TDD, it wasn't perfect, some tests were written before and some were written after the implementation, but I could feel I was starting to get it (you can see the git code in bitbucket).
A few years ago I started getting into Ruby on Rails, and the smartest thing I did was subscribe to Gary Bernhardt's Destroy All Software screencasts, most of what I know now about TDD I learned from these screencasts (so thank you Gary!).
I discovered an amazing testing framework called RSpec where you write your test in a much more readable format:
describe Cat do describe "#talk" do it "returns 'Meow'" do cat = Cat.new cat.talk.should == 'Meow' end it "returns 'Prrr' after eating" do cat = Cat.new cat.eat cat.talk.should == 'Prrr' end end end
When you run the tests this is the output:
Cat #talk returns 'Meow' returns 'Prrr' after eating
The hierarchy and the plain text for examples and contexts make the output perfect as documentation for the code.
I learned that there are several kinds of tests:
- Unit tests: test methods or classes in isolation, all external dependencies are stubbed (if a test fails you will usually find the error in a very narrow area of code).
- Integration tests: test the integration between several methods or classes, some of the external dependencies can be stubbed (if a test fails its harder to find the error).
- Acceptance tests: test the system as a whole, without any stubs. For example, open the web application in a browser and check that certain elements exist, or execute a command line application and check that the output matches what was expected.
I developed various Rails applications, and with each application my technique and my understanding of TDD improved, I could feel I was making progress. Back then I only wrote unit and integration tests.
At first, I was progressing slowly, working with TDD was much slower than non-TDD because I kept getting stuck on tests I wasn't sure how to write. But after a few months watching lots of screencasts, reading tons of blog posts on TDD, and spending a lot of time trying to improve my code and my tests I got to a stage where I'm working faster with TDD than without.
Whenever I write code without writing the test first, it just feels wrong and sloppy, and I can't make excuses anymore, since it doesn't take more time.
About a month ago I read The RSpec Book and it filled all of the holes, finally I had a full understanding of an entire BDD process that felt right to me:
- Write the user stories
- Select the user stories for the current iteration
- Write a cucumber feature for each story (with all of the required scenarios, I find it best to write the scenarios with a QA person or a product owner).
- Now start making the scenarios pass using ordinary TDD (write the actual code with unit and integration tests first).
- When all of the scenarios of a feature are passing, it basically means the feature is complete (now you need to do some exploratory testing to see if it really works).
As I understand it there are two ways to use Cucumber:
- QA tool - writing super-specific tests (using the "evil" websteps, I click this, I enter that, I see this...), this way your feature files get huge and you can no longer use it for documentation because it's too long and no one will read it.
- BDD tool - write the acceptance tests from the stories in an abstract way, so the feature files are easier to scan and understand.
I know a lot of people don't like using Cucumber, and at first I thought so too, but after reading the RSpec book and trying it out I think that as long as you're using it as a BDD tool it can actually be more comfortable then rspec (for acceptance tests), I like the the Scenario Examples feature allows to you to tests multiple inputs in a pretty clean way:
Scenario Outline: Add two numbers Given I have entered <input_1> into the calculator And I have entered <input_2> into the calculator When I press <button> Then the result should be <output> on the screen Examples: | input_1 | input_2 | button | output | | 20 | 30 | add | 50 | | 2 | 5 | add | 7 | | 0 | 40 | add | 40 |
The cucumber tests filled the gap between the user stories and the integration/unit tests for me and they help me stay much focused and a have a better indication of "done".
To sum things up, the three things I like most about this process:
- My code is a lot more stable.
- Helps me focus.
- I'm discovering my code instead of guessing it into existence.
Hope you enjoyed this post, thanks for reading,