Fiery debates over TDD approaches and unit testing strategies loom over many technical interviews, pull request reviews and around the water cooler in countless open-plan offices across the industry. We’re passionate about this and we want everybody else to know it. So it’s no wonder that we constantly ponder over what good unit testing really is, and if there’s anything we can do to make our slice of unit testing goodness even better.
What is a unit test anwyay?
A unit test is a tool used to verify the smallest piece of testable code against its purpose, in isolation from other parts of the application.
In itself, a unit test is of course nothing a piece of code. But it’s written with the sole purpose of testing another, requirement-driven, piece of code (unit).
There are three simple steps to every unit test verification, and they form the notorious AAA.
Arrange: A small part of the application which we want tested is initialized.
Act: A stimulus is applied, usually by calling a method on the corresponding part of the code.
Assert: The resulting behaviour is observed. If it matches our expectation, the unit test has passed. If not, it has failed.
What makes a good unit test?
A good unit test exists.
I know, I know, thanks for that, Captain Obvious!
But what I’m trying to get at here is something that I’ve been facing time and time again:
Most developers don’t write unit tests.
There are, of course, many reasons why:
- Writing tests is costly, and so it’s often seen as a luxury. Tests take time and money, costs which your company, manager or client might not think justified.
- Tests increase the size of the codebase, which could potentially make codebase maintenance a costlier process too.
- Writing unit tests is scary. A legacy codebase with no to little test coverage can be a particularly frightening prospect. Where to even begin? And what would be the point anymore?
While these arguments each have their degree of validity, a pragmatic approach to unit testing is perhaps always the best idea.
Extreme time constraints and occasionally even technical restrictions, may in fact make the writing of unit tests unfeasable.
But, and we’ll come back to this further down, what unit tests force you do to, in fact what good* unit tests force you to do, is to write good code.
So not writing unit tests, good unit tests, really means sacrificing on code quality and maintainability.
Occasionally that can’t be helped. But going back to the codebase once the dust has settled, and addressing the compromise in quality by writing some good unit tests, will make everybody’s life much easier in the long run.
* In the lines above, I’ve more than once highlighted the fact that what we’re aiming for here is not just an ever growing, tangled collection of unit tests.
Unit tests written for the sake of writing unit tests rarely bring significant value, and are in fact often a burden to the team during the development process.
What we should be aiming for is a collection of stable, nimble, good unit tests, written for the right reasons and with the right outcomes in mind. But more on that below.
A good unit test comes first. Maybe.
Many a blog post has been written about the test-first/test-after debate. I’m not going to get myself and this particular post dragged down the slippery slope of that argument, as my attitude on the subject is more along the lines of: “Can we stop fighting about this and just write some good unit tests already, one way or another?”
I will nonetheless mention a fairly well known Microsoft Research study from 2008, in which they collected data from teams working on Visual Studio, Windows, MSN, as well as from an IBM project, and compared the efficacy of test-first vs test-after approahes. The researchers found that the TDD teams produced code that was 60% to 90% better in terms of defect density than non-TDD teams, but also that the TDD teams took 15% to 35% longer to complete their projects. Food for thought.
A good unit test is not about finding bugs.
A good unit test looks at one unit of the code separately. A number of good unit tests passing really only proves that their corresponding units of code work as expected independently. It does not prove that they work correctly together, that they are configured correctly and that no external moving parts interfere with them. It does not prove that the application is bug free. There are infinitely more effective ways to ensure that, and they involve running the entire application, as in production, similarly to how it happens during manual testing. If the steps of that process are automated, it becomes integration testing, and that’s a whole different kettle of fish.
So while a combination of manual and integration testing is the way to go in terms of finding new and regression bugs, good unit testing’s purpose should be to help in designing robust components.
The one caveat to this is refactoring, by which we understand the process of making changes to a unit’s code without the intention of changing its behaviours. In this case, unit tests may identify unintended changes in the code’s behaviour, i.e. bugs.
A good unit test tests one unit of work, independently.
We’ve touched on this a few times already, but it’s such a vital characteristic of good unit testing, that it surely needs a dedicated section.
A unit test tests the smallest testable piece of functionality, in isolation. Changing other parts of the code should not affect a unit test written with a specific code unit in mind.
A good unit test doesn’t depend on other unit tests. The order of test execution should make no difference in terms of the passing or failing of your unit tests, and each unit test should ensure a return to clean state, by appropriately setting up and tearing down the data it needs and generates.
A good unit test doesn’t depend on the environment. The aim is to be able to run unit tests as frequently and easily as needed. Which means that one should make sure that they don’t need a specific environment, and don’t rely on various machine settings.
A good unit test doesn’t depend on external data. Just as unit tests should not depent on the environment, they should not depend on any form of external data. They should not atempt to access databases or external files, and should therefore be entirely portable.
A good unit test doesn’t have side effects. On the other side of the spectrum, unit tests should not update database records, or send emails to all your customers. Side effects are sometimes dramatic and irreversable, and always, not testable.
A good unit test is easy.
Good unit tests are small and intuitive, therefore the amount of effort spent in writing and understanding them should be minimal.
The intention of unit tests should be easily apparent, and they should serve as a valuable form of documentation. They should be concised and consistent, and follow predefined, clear naming and coding conventions.
A good unit test is good code.
During the lifetime of a project, there will be just as much test code as production code, if not more. It will be read, time and time again, by yourself and many other developers. And so it should be good code, treated as if it were production code. It should follow good design standards, consistent and thorough naming, so that it’s as flexible and easy to work with as possible.
A good unit test is automated, repeatable, and fast.
Invokind and checking the results of a unit test should be automated, fast, and run repeatedly as part of your build, producing the same results every time.
A good unit test adds value.
And we’re coming full circle to what we said at the beginning of this post. One shouldn’t write unit tests just for the sake of writing unit tests. Good unit tests and good coding go hand in hand. They offer clarity, inspire confidence, save time, make changes less daunting, and document the codebase. They give us a new lens through which to look at code design, leading to more flexibility and mantainability.