In a previous blog entry I talked about unit testing and how I’ve learnt from my (many) mistakes when writing unit tests and practices that I’ve seen that wind me up.
Today I’d like to talk about how I’ve been writing unit tests recently, employing the ideas of TDD (test driven development), and some of the pros and cons of using this approach.
When I first learnt of TDD and was strongly encouraged to use it, I thought it was about writing tests then code. This is kind of true but it also a gross simplification and one that others that I’ve spoken with also have. At the time I really didn’t like it and rejected the idea but having learnt more, I think it is actually kind of swell.
TDD is more iterative and helps you design the code.
- Write a “single” unit test describing an aspect of the program
- Run the test, which should fail because the program lacks that feature
- Write “just enough” code, the simplest possible, to make the test pass
- “Refactor” the code until it conforms to the simplicity criteria
- Repeat, “accumulating” unit tests over time
Here’s a basic example of TDD for a method to take two strings and adds them:
- Start with the most basic case:
- Assert.Eq(myThing.Add(“1”, “2”), 3)
- Write code to make that pass.
- Tidy up the code you’ve written
- Repeat the process as you build up functionality
- What’s next? Error handling with string parsing:
- Assert.Null(myThing.Add(“cat”, “2”))
- After writing the test, see the result and fix if necessary (seems likely at this point).
- Okay, time to do the tricky bit. Again, write a new test, see the result and iterate:
- Assert.Eq(myThing.Add(“one”, “2”), 3)
- Some edge cases:
- Assert.Eq(myThing.Add(“-3”, “four”)
- or: Assert.Eq(myThing.Add(input1, input2), expectedOutput)
- What’s next? Error handling:
- StrToInt.returns(null) / StrToInt.Throws(ex)
- And so on…
One thing I quite liked was when I’m testing my interface for the new class within other classes. It had me thinking “how do I want to handle these situations?”. Previously I would have written a wad of code, handling errors as I see potential to bump into them etc then knowing what I intended the code to do, I’d write the test to ensure it passed. TDD got me more focused on desirable behaviour.
The other benefit that I found was that if I found adding an extra bit of functionality required touching other unit tests that weren’t interested in that change, I knew that my code design was wrong. I was building much more independent tests and therefore, I hope, more maintainable code. If we decide to change how to handle one bit of a method, I won’t be having to update every sodding test like we’d done in the past.
Of course the benefit of better and more maintainable code could just be because I’m more experienced (even if I barely write code since returning to a test role). However I remember feeling especially chuffed with the code.
I’ve heard that TDD can help reduce manual testing required. Personally I’m not sure if that is the case for me given that historically I’ve had very good coverage – even they were written in an overly complicated manner. Anyway, I’d be very apprehensive about reducing the functional testing on the basis of code being unit testing. However I was at least happier than I wouldn’t need to repeat manual dev testing.
There are of course drawbacks. I would have a torrid time if I tried doing this in an area that has really badly written code and tests. It was definitely easier to embrace when I was adding new features.
Also thinking back to some of my previous projects, I may have started work on a changeset with a less defined idea of what I wanted to do. We all know (hopefully) of exploratory testing but I’ve often embraced “exploratory coding”, where I’m exploring ideas of how to put together a class or how an API works through the code.
You can probably still use TDD with this early doors by using behaviour driven tests with little thought on implementation. However my problem here is that if I’m not confident on how something will work, I can find myself adding/removing parameters and changing my design of the code quite a bit until I get a “feel” for it.
I’ve found that if it isn’t a clear area that I’m working on, I might do my exploration of the code, see how it works, understand what I want to be doing and importantly, know that my code is like my exploration notes and not get attached. Then when I have an understanding, I’ll switch to TDD and write it “for real”. However I’ve only limited experience of doing this so I’m not sure how practical it is.
Finally in my experience so far I’ve found that it was definitely slower than some of my similar sized user stories in the past. In the short term it may negatively impact velocity and leave a bad impression but if you’re writing tests that are easier to maintain then this should benefit you in the long run.
Yes, it took me longer to write each changeset, but I wasn’t re-writing unit tests every time my next changeset built upon my previous code. The next time I work on this feature I expect to be quicker than I would have in the past.
In the long run, TDD seems like it will not only help me write better code and tests but whoever picks up working on that area will hopefully thank me for the effort. I’d certainly be grateful if the previous developer in an area has written maintainable and testable code.