How writing Unit Tests forces you to write Good Code: And 7 bad arguments why you shouldn't
Unit Testing is about writing good, well-designed, decoupled code that, as a result, is automatically testable.
Unit Testing is a well-established practice in our industry. Why then, are so many developers not writing unit tests? Why do some consider Unit Testing to be a luxury. Something you do when you have time or budget left? This bothers me. I strongly believe that these developers are missing the point of Unit Testing and fail to see why it’s not a luxury, but pretty much a necessity. In this post, I would like to give my perspective on why I consider Unit Testing to be necessary, which I already touched on shortly in a previous post (about Agile Software Development). I will also discuss (and reject) some of the arguments developers use for not writing Unit Tests. In this post, I borrow from insights from other people, such as Robert C. Martin, Martin Fowler, the Gang of Four, Kent Beck, etc.
Disclaimer: I write Unit Tests for the majority of my code, especially all my business logic. For classes with complicated designs or algorithms, I usually write tests in parallel or when I feel the design has solidified sufficiently. I’m a big fan of TDD, but I also believe that following the ‘write tests, then code’ rule too rigidly can hamper the Agility of evolving and experimenting with code. Also, although I write Unit Tests often, I pretty much run into tricky test scenarios on a daily basis. I consider this a good thing, because it improves my code as a result.
What is Good Code?
To make my point more clearly, it is useful to start with (my) definition of Good Code. There are many academic definitions, but let’s keep it simple:
Good Code is code that works well and is easy to maintain, extend, comprehend and understand (in the present and in the future)
To write Good Code, the following (well-established) practices are important:
- Reduce coupling: Classes should be loosely coupled. Classes that are tightly coupled are dependant on each other. This makes it difficult to change one class without changing the other, and greatly increases rigidity and viscosity. Tightly coupled code will result in cascades of changes in other classes when you want to change one class. See my post on Ninject on practical ways to deal with this in C#;
- Maximize encapsulation: Classes should encapsulate parts of business logic and encapsulate the implementation within an intuitive interface, so that other developers understand how to use the class;
- Single responsibilities: Classes should be responsible for one process, business rule or procedure, and should do that very well. If your clas has many different responsibilities, it will become progressively larger, harder to maintain and (much) harder to understand for yourself and others;
- DRY: Classes should not repeat logic that was already implemented elsewhere. Do not Repeat Yourself;
- Open/closed: The design of your code should be such that the functionality of existing classes can be easily extended without changing the class (very often);
- Documentation: Classes should be documented in such a manner that other developers know how to use them. Preferably, documentation is implicit, which means that the names of classes, methods, their parameters and the code are self-explanatory and separate documentation is not even necessary;
- Use design patterns: The architecture of your code should follow design patterns wherever possible. This makes it easier to communicate your intentions to other developers and improves your design;
In my mind, Good Code consists of many small decoupled classes that have small (but distinct) responsibilities within the bigger system and cluster together in a logical and intuitive manner. This code facilitates frequent refactoring. Bad code is the opposite and consists of very large and tightly coupled classes that do many things and are hard to understand and refactor. Good Code is not perfect code. But the number of ‘WTF’ moments will be a lot smaller than with bad code. The picture below from Robert C. Martin’s book ‘Clean Code’ illustrates this well:
Good Code can be written without writing Unit Tests, but writing Unit Tests will flush out most design flaws, bugs, dependancies and other oversights. Thus, writing Unit Tests is more likely to result in Good Code that is — as a result — also automatically testeable. But you do need Good Unit Tests for this. So, what makes a Good Unit Test?
What is a Good Unit Test?
It’s also useful to clarify what I consider to be good Unit Tests, so that we’re talking about the same things. A good Unit Test:
- Tests only your class, not it’s depedancies: A good unit test tests only your class, and specifically some small unit of the functionality of your class. A unit test should not implicitly test code in classes outside of your class in any way because a Unit Test should be able to control all the variables. If you are implicitly testing other classes, you are suffering from dependancies in your code;
- Input/output checks: A good unit test manipulates the dependancies and the input of a class and verifies the output. The output can be a return variable or a verifiable call to another class (through mocking);
- Hypothesis-based: A unit test should be treated as a hypothesis test. In science, a hypothesis is a single expectation that is being tested in isolation. A good unit test tests one thing, and tests it well (sometimes in different ways);
- Concept-based: A unit test tests a single concept. There’s no point in writing 10 very small unit tests that test all sorts of input validation in very similar ways. In that case, write a test that tests input validation as a concept (includes the other tests). If this makes the test a bit bigger, but reduces repeating the same test, I think it’s worth the extra lines;
- Small: a unit test should be small, between 5 and 25 lines of code. This is a pretty arbitrary cutoff, but you should be able to understand what the Unit Test is doing. If the test is very long, this is a strong indication that the class you are testing is responsible for too many things or has too many dependancies. Refactoring a class into smaller classes is a good strategy;
- Intuitive: a unit test should be intuitive enough so that other developers can quickly understand the gist of the test;
- Uses business/domain terminology: Unit tests that test business logic (most Unit Tests will do this) should stick as close to the domain terminology as possible. They should be named such that a customer can also understand what is being tested (e.g. ‘When a customer orders too few articles, he can’t complete his order’ instead of ‘When no items in record, throw exception’. This allows the Unit Tests to be used a form of documentation for your class and can be checked when a customer wants to know how the system behaves;
No that we’ve defined Good Code and Good Unit Tests, let’s move on to the arguments that are often used when developers consider Unit Testing unnecessary.
Why not #1: ‘Unit testing takes too much time or is too difficult’
Let me rephrase this to ‘Writing good, well-designed, decoupled code — that is easily testeable as a result — is difficult’. Developers experience Unit Testing as difficult when they run into these kinds of problems:
- Classes are tightly coupled to other classes, which makes it hard to test because you need to control those other classes as well when you are writing your tests. This is very, very difficult and very error prone. It also makes your tests brittle, because you have to change your tests if the implementation of the coupled classes change. This is indicative of Bad Bode because it increases rigidity and viscosity;
- Classes are difficult to understand (because they’re large or too clever), which makes it hard to formulate clear tests that are straightforward to execute and verify. This is indicative of Bad Code because it increases needless complexity;
- Classes that do many things (because they have many responsibilities), which requires many, many groups of partially correlated Unit Tests before you can consider the class tested. This is indicative of Bad Code because it increases needless completely and reduces transparancy;
- Classes that are not intuitive or have a bad interface, which makes testing them very arduous. This is indicative of Bad Code because it decreases transparancy;
When you first start writing Unit Tests, you will quickly run into your limits as a developer. For me, this was actually quite humbling. You will quickly experience the pain of tightly coupled code, classes that have too many responsibilities and code that is hard to understand. This may seem like a bad thing, but it’s actually a _very good_thing. After all, it will force you to write your code in such a manner that it becomes testeable. So, you’re actually learning and becoming a better developer.
Why not #2: ‘Unit testing is a luxury (we don’t have)’
Developers sometimes consider Unit Testing to be a luxury. In their perception, you write Unit Tests when you have time left. I can’t blame them. A big part of the problem lies in the common (mis)conception Unit Tests is primarily about ‘testing your code’ and catching bugs. In other words, they allow you to verify if your classes behave according to specifications. You write your code, then you check if it’s working by running your automated unit tests (red/green development). Although this objective measure of progress and quality is a major advantage of Unit Testing, it can easily lead to the position that writing ‘Unit Tests is a luxury’ and can be skipped because there’s no time.
Although it’s very useful to verify the behavior of your classes in an objective manner, the primary benefit of Unit Testing is that they force you to write Good Code. The reasoning of this argument is basically that writing Bad Code is acceptable ‘because there’s no time’ to write Good Code. Although there are certainly moments when you have no choice, there’s something fundamentally wrong when there’s never any time or this argument is always used. In that case, your codebase will quickly rot and the total cost of changing code will increase dramatically as time goes by.
Why not #3: ‘Unit testing is not necessary, because I already write Good Code’
Great! Then you shouldn’t have any problem writing Unit Tests and prove that you’ve written Good Code. In addition, the Unit Tests will also make the code automatically testeable, which is very useful when you’re going to refactor your code in the future or for other developers to understand what your code is doing. If this is actually used as an argument, it’s more indicative of the arrogance of the developer :) If you are that good, you should see why Unit Testing is necessary.
Why not #4: ‘Unit testing is not possible with this legacy code’
Writing Unit Tests for legacy code is difficult because legacy code is often also Bad Code. There are a lot of dependancies, classes are very large and do many different things. Unit Testing will be hard in this case. But the alternative should not be to just leave everything as it is, and continue with a continuously rotting codebase. There are many different strategies to refactor legacy code into testeable code, see for example Martin Fowler’s classic ‘Refactoring’. A few key strategies:
- When you are writing new code, make sure to put it into new classes. Follow the Good Code guidelines for all new code that you write and write Unit Tests for them;
- Your new classes will probably require legacy classes to do work for them. In order to keep your class testeable and avoid tight coupling, it is adviseable to apply Adapter of Facade design patterns to isolate the legacy functionality you need. Put an interface over it to allow you to mock it’s behavior when you’re writing Unit Tests;
- Write Unit Tests for legacy classes. Although it will be hard, the act of writing tests will greatly improve your understanding of the code. If it’s very hard to change the existing class to mock out dependancies in your Unit Tests, just write tests that accept these dependancies for the time being. This might mean that you have to actually insert stuff into your database in your test, then perform some logic and finally check something in the database. This is very dirty, but once you have a good battery of automatic test cases you can start refactoring out the dependancies by writing Good Code. By continuously checking if your tests still work (which will be refactored along with the class to allow mocking of dependancies), you can refactor the class into a testeable class;
As with all the other arguments, the key decision is whether or not you want to write Good Code. If that’s your goal, then write Unit Tests. Not for the tests, but for the process that makes your code testeable in the first place. If you don’t, then accept that you are probably writing Bad Code.
Why not #5: ‘I don’t have enough knowledge of Unit Testing’
I don’t see how this can actually be a valid argument. The writing of the Unit Tests themselves is not hard. There are many testing frameworks available that are quite intuitive. So that shouldn’t keep you. A lot of developers that use this argument are probably just having trouble writing Good Code that it is testeable. If that’s the case, there is some merit to the argument. But instead of not writing Unit Tests, it’s actually a reason to start writing them so you become better at writing Good Code!
Why not #6: ‘Unit testing are inferior to regression/integration/manual tests’
Regression/Integration tests allow you to verify the integrity of a system and as a check to see if your classes work well together. Developers using this argument would have a point if Unit Testing is only meant to test the behavior of the system. After all, a good suite of regression tests will probably achieve the same goal. However, Unit Tests should not be ignored for two reasons. The first one is that they allow you to test isolated behavior of a single class instead of the whole system. When you are testing the whole system (through regression/integration) the number of potential test scenarios will grow exponentially as the system’s complexity increases. Unit Tests will remain straightforward because they test one class and one class alone (actually, if you don’t decouple your classes from other classes, you will run into the same problem). More importantly, Unit Testing is about forcing you to write Good Code that is testeable as a result. Writing regression and integration tests will not force you to write Good Code.
In other words; the purpose of regression, integration and manual tests is primarily to verify the system’s behavior. Although this is also a purpose of Unit Testing, a more important purpose of Unit Testing is to write Good Code that is testeable as a result.
Why not #7: ‘Someone else (like a tester) can write tests for my code’
This argument is valid when you would only write Unit Tests to check if your code is actually working. However, even in that case it would be adviseable to write the Unit Tests yourself. But writing Unit Tests is not only about testing your code, it is far more about learning how to write Good Code that is testeable. If someone else writes tests for your code, there is nothing holding you back from writing Bad Code. Sure, a tester might ask you to chance your code to make it more testeable. But why don’t you write tests yourself and refactor your code to Good Code instead of waiting for someone else to fish out your design flaws.
Are there valid arguments for not Unit Testing?
Yes. As always, you have to be pragmatic. But you should be for the right reasons. It’s never good to write Bad Code, because it will cost you in the end. But there are a few valid reasons why writing Unit Tests would take a lower priority for me:
- Very urgent time constraints: If a lot of functionality has to be delivered in a very short time, writing Unit Tests is not always feasible. It does take some extra time. But this also means that you don’t have automated tests and — more importantly — no way to force you to write Good Code. In this case, you clearly sacrifice code quality and maintainability. I don’t think that’s a very bad thing if it happens only occassionally. But make sure to clean up the code later and do write Unit Tests when you can. Your company should understand the value of a good (not rotting) codebase too;
- Technical framework doesn’t allow it: Some frameworks may not allow Unit Testing. I come from a .NET background, and all the .NET languages have many, many testing frameworks. If there’s not testing framework, you could even invent your own. The basic concept of unit testing is not that hard. It’s just a class with methods that check input and outputs. At least write Good Code;
If you know or have heard of other valid reasons, I would like to hear about it. Drop a note in the comments.
Conclusion: So what’s it going to be? Good Code or Bad Code?
Unit Testing is about writing Good Code that is testeable as a result. So, it pretty much comes down to the decision of writing Good or Bad Code. Good Code can be written without any Unit Testing, certainly, but it’s far easier to write Bad Code. Although Unit Testing gives you a wonderful objective measure of quality and progress (red/green development), it primarily acts as a powerful guide that forces you to write Good Code.
So, do yourself and your team a favor and start writing Unit Tests for all the code that you produce from now on :)