Unit Testing Isn't About the Tests
So, here's the story... A colleague of mine commented on a code review, I submitted. "Should we add a story to our backlog to add some tests for this file?" This seems to be a common thing where I work... Maybe it's not as uncommon as I hope it is.
Now, this was a little more than a refactor, which usually warrants a test (or at least, a change in unit tests, if they exist). However, in this particular case, the change in functionality was actually negative... I was removing functionality that we no longer needed. I'm not one to write tests to ensure things aren't there. Had there been tests to check that they were, I might have reversed them to make them fail before removing the functionality (then removed the tests afterwards...). In the end, I replied that I didn't think it would be a good use of our time... Which brings me to my point.
Unit tests are most valuable when you're writing the code. Adding them after the fact, has limited benefit, and is usually an exercise in humility. I strongly believe that way lies madness...
Clear, Concise Code
The primary benefit of TDD is this... This is pretty obvious, but I don't think people really understand how TDD helps to accomplish this, or to what extent. When considering the functionality I'm planning on adding, I usually have a thought about how I'll be implementing it. Sometimes it's a little too complex, sometimes not quite complex enough.
When I write my test first, it forces me to think of what, exactly, I aim to accomplish with the unit of code I'm about to write.
- Write a sentence that describes it. This is usually the hardest part. If you can't write a concise sentence to describe it, you're probably biting off too much.
- Consider your inputs. Create some dummy data, or go get some sample data. It doesn't have to be good... it doesn't even really need to be representative, yet... mostly just the write type of data.
- What do you expect in return. This is usually, the easiest... It's what's left after your input has been processed. Where do you want to end up?
After thinking through all of this, I usually have a really clear idea of what I need to do... and while that's huge, it's not what this benefit is about.
After writing your test... you write just enough code to make it pass. Then, when it passes, you refactor. Refactor your code, try to remove redundancy. Add variables to make it clearer what is being done. Rename old variables if there is a better name.
Be relentless! Don't allow a single line of code that doesn't serve a purpose... Don't allow two lines for what can be done as clearly with one.
When your done refactoring your code, refactor your test... If your inputs aren't very representative, replace them. If you need a larger sample of inputs, add an array and a loop... Be sure to run your test with every little change, so you know everything is still going to plan. If you have repetition across a set of tests, extract that repetition out into a beforeEach (or setup or whatever your testing suite uses).
After you've written a sizable chunk of functionality this way, you'll look at your source and think... that's all... It's so small... And I've been at it for hours... But, it's beautiful! It's clear, it's meaningful, and it does exactly what you expect it to do... and nothing else. That is the biggest thing. There's no extra code to get in the way when you need to make a change later. Everything there is there for a purpose.
The Other Benefits
Yea, you get unit tests which can run on every build and tell you if somebody changed something in a way that the test can detect problems. That has some value, however, I've found most bugs don't actually fail the tests... They're usually adding functionality that you didn't test for.
You get a specification list, which describes what you're trying to accomplish with your code... meh. If you were ruthless enough while writing the code, it's already pretty clear without the specs.
While these could be beneficial, especially for code that wasn't written in TDD fashion, I don't really feel like they're enough to go through the hassle of writing the tests. Sure, if you're dealing with a really complex bit of code, it would probably be beneficial to write some pinning tests to try to nail down the current functionality, before touching it. However, chances are pretty good that you'll miss things (probably the same things you'd miss while just refactoring the code in the first place).
In the end, I feel that if you're not going to practice TDD, you may as well, not write the tests, at all... You're missing out on the greatest benefit of TDD, by putting off writing the tests to the end. On that note, this benefit is huge!!! Try it for a couple of weeks... at least on new development. If you're doing it right and you have a good workflow, you'll never go back to writing huge chunks of code at a time.