Thanks to Mark Mishaev for inspiration.
Sometimes, I hear that someone is trying to start off with unit testing or TDD and they ask me for some good advice on where to start - some general guidelines that would help them along the way. I thought I'd summarize my usual answer here for everyone to benefit from.
So, if you're starting off with unit testing or TDD, here's my advice:
- Read at least these blogs to grasp some good practices:
- Sustainable Test Driven Development by Amir Kolsky and Scott Bain
- My own blog
- There are some other good ones, just look around, I recommend blogs by Steeve Freeman, Roy Osherove, Dan North, Liz Keogh, Robert C. Martin, James W. Grenning, Kent Beck, Mark Seemann, James Newkirk...
- Pick up some books on the subject. The topic of unit testing and TDD might seem straightforward (there are a lot of simplifications around, such as "unit test is just a small code that creates a class, invokes a method and performs assertions on its output" and "TDD means you write your test first, then write the code"), but it actually requires quite a bit of knowledge to get things right. Also, don't limit yourself to a single book - read at least three or four - many TDD gurus have more or less different styles of doing TDD and writing unit tests and your own technique will get more and more flexible as you manage to understand more of them.
- Try writing your tests first. For a list of benefits, take a look at:
- For new code, try to follow Need Driven Development
- To drive your code and design, try exposing yourself to “diagnostic pain”. This means putting some limitations onto yourself and when you’re tempted to break them, it means that the code or design is at fault and you should refactor. To give you a quick example, I do not use Setup and TearDown kinds of methods in unit tests (not mentioning Action Attributes from NUnit), which means all the objects used in the unit test are created in the body of the test method itself. I sometimes use helper methods when they improve readability and understandability of the test (but not to hide redundancy - that's the catch). At first it might look awkward, but I have created a lot of unit tests without Setup and Teardown and I'm very happy with the result. So it’s not like I don’t know how to use a unit testing framework - I deliberately hold back from using many of its features
- Ideally, a maintainable unit test has to fulfill three conditions, as pointed by Scott Bain:
- It fails reliably when behavior specified by it is broken
- It fails only when behavior specified by it is broken (so breaking another behavior should not break this test)
- No other unit tests will fail when behavior described by this test is broken
- Try using a technique called Constrained Non-Determinism. You can either use tools that help with this (like AutoFixture for C#) directly, or you can wrap it in your own class (if you're reading my blog, you probably know that I like to wrap creating anonymous values in a class called Any). Alternatively, just roll out your own mini library or a set of functions.
- Use continuous testing tools, (examples for .NET include Mighty Moose (free) or NCrunch (paid)) for running your unit tests. If no such solution exists for your programming language of choice, you can always write a script that performs continuous compilation and running unit tests and lets you know of the results. There are some tools like Watchr that can make writing such scripts easier (especially when integrated with your operating system's default user notification mechanism).
- Always specify class behaviors (not methods or classes) with unit tests – to help you with this, try using the following convention of naming your tests: ShouldXYZ() where XYZ is the description of the behavior the class should supply. The convention is taken from Dan North’s post and helps to avoid:
- Check-it-all unit tests – where one does a single setup and verifies multiple behaviors.
- Names that do not make sense. I saw one a unit test named: SendFrameMethodSendsFrame() which tells almost nothing about the behavior. The “Should” naming forces us to rethink the name and come up with something like: ShouldPassReceivedValidFrameThroughTheMessageQueue() which works better.
- Also, pick names that read well when converted from "ThisNamingConvention" (or "This_naming_convention" - whatever you choose to apply to your unit test methods) to "this naming convention" – it lets you use the unit test results report in Cruise Control or Jenkins as a living documentation of your code after you apply a little post-procesing to it.
- You can look at my blog post that discusses the repercussions of bad naming.
- When choosing a mocking framework for your project, put your money on ease of use and readability. For example, many mocking frameworks for C# use lambda expressions everywhere, but many programmers are still not used to thinking in terms of expressions and are a bit confused. When developers find using such framework awkward, they're more likely to reject the idea of unit testing or TDD. Thus, as a main mocking framework, I like to choose NSubstitute.
- Always keep handy a "fallback mock framework" - there are some features that your main mocking framework of choice does not support or that are difficult to achieve using this framework. For example, NSubstitute does not yet support partial mocks. That's why it's good to have a secondary mock framework of choice that can make up for the main one's weaknesses in some rare cases. For such a fallback mock framework, I like to use Moq, but that may as well be FakeItEasy or Rhino Mocks or something else - depending on what you need.
- Try to stay as close as possible to SOLID principles and adhere to the Law Of Demeter (which is a conclusion from those principles, specifically - from Open Closed principle) – all of this lets you write better, more focused and more maintainable unit tests. For a list of smells that show the problem is in the design rather than in tests, see:
- Watch out for unit tests execution times – unit tests that run for more than 2-3 seconds (to tell you the truth, many should even run in less than one second), almost always have a smell attached to them – either they touch external resources or are not unit tests. Staying away from external resources may be tricky. For one such example where this actually does get tricky and one may be tempted to break the isolation, see my example related to timers
- Avoid the "The code is so straightforward that there’s no need to test it" thinking. When the code is complex, the solution is refactoring, not unit-testing (of course, tests can be written, but they have to be used as coverings). Unit tests are about specification, not coverage, and short specifications are good.
- Perform a kata or two on your own. One such kata and its exemplary solution are posted on this blog
- There may be a temptation to write more coarse-grained tests/specifications that are tied to the code (they’re sometimes called white-box tests). This is possible, however, I recommend not to do it if it’s not very well thought. I’ve been in projects where coarse-grained, so called “white-box tests” led to situations where correcting and debugging the tests took twice as long as changing the code. Again, it’s possible to write good coarse-grained tests, it just that it’s hard – it requires reading some books and doing few dry runs to grasp many best practices that are different than in case of unit tests. On short discussion of how things may go wrong, be sure to look at my post: Perfect from the start.
- Also, there may be a temptation to "test private methods". This is a sign that things are going a little bit offroad.
- Attaining X% code coverage should not be a goal. High coverage is good when it’s a side effect.
I could go on writing, but I think these are the most important, non-obvious things I learned throughout my adventure with TDD. As always, feel free to add your advice in the comments.