So you write code, but you don’t want to write tests? This, then, is the list for you; six reasons to proffer to your colleagues, customers, and interviewers as to why you have this view. Be warned, though: the result may be a change in relationship with all three, and none for the better.
You don’t have time.
It takes time to write tests. For the sake of the argument, let’s focus on unit tests or tests that focus on one, isolated, unit of code. These should not cross system boundaries, by calling actual down-stream systems or databases that are not part of the system under test.
Writing unit tests in the format “Arrange, Act, Assert” (AAA) enables us to mock out the dependencies to create the desired scenario (Arrange), then execute the method which we want to validate (Act), and then validate that the outcome is what is expected (Assert).
If we consider that we should be attempting to cover all the scenarios that we can think of, and certainly those that meet the acceptance criteria specified in our requirements, it stands to reason that we will be writing a lot of test code. And that will take time. However, the time taken is recovered through the impact unit testing has on other activities:
- Debugging – trawling through your code-base, desperately placing breakpoints in places referenced by your stacktrace, is time-consuming and exhausting. If you test successfully at a unit level, you should catch and address bugs before you even deploy into a runtime.
- Fixing bugs in Preprod or Prod – the further along the line a bug is discovered, the more expensive it is in terms of people, time, and, as a consequence of the two, money. There may also be reputational impact. When a tester uncovers a bug, there has to be a process for logging, communicating, and tracking the issue. When a customer uncovers one, there are even more hand-offs and interactions required.
- Regression testing – through building up a suite of unit tests that cover an appropriate percentage of the code, we are able to execute full regression testing at the virtual touch of a button. This gives confidence that what has changed does not break another, unexpected, piece of code. Further to this, that suite can be executed as part of the automated pipeline, which gives additional confidence after merging various branches of code.
Further to the above, if we adopt Test Driven Development as a technique in developing software, we are also using the testing phase to assist in designing and refactoring the code, while it is being written. This leads to cleaner, more robust code. The discipline of writing a test that fails, writing just enough code to make it pass, then refactoring that code to make it better, can result in faster cycles to achieving working software.
While there is undeniably an overhead in the initial adoption of TDD, once it becomes embedded in your coding muscle memory, it leads to a flowing, concentrated output of high-quality code that does only what it is meant to do. This contributes to a decrease in code that you imagine may be useful in the future, but which ends up hanging around like an unwelcome guest that still needs to be entertained. TDD assists in maintaining the “You Ain’t Gonna Need It”, or YAGNI, principle.
In rebuttal of the not-having-enough-time argument, then, we can see that time is, in fact, saved in the overall lifecycle of your code, and that using TDD can save time in the code-writing process itself.
You hate your colleagues.
Seriously, if you don’t like your fellow-programmers, both known and unknown, don’t write tests. After all, a comprehensive, well-written test suite also documents the behaviour of the code itself. For this to be true, your tests need to be:
- Well named – the test method tells you exactly what it is verifying. Long, wordy method names are encouraged, as they enable the reader to understand the precise intent.
- Repeatable – the full suite should be executable numerous times, and in any order, so that no one test depends on the outcome of another.
- Reliable – the tests should assert what they say they will, and must fail if the output of an execution is not what was expected.
When your colleagues, current and future, are given this kind of test suite, they can immediately understand the intent of any significant code, and can even see how to invoke the public methods that make up the externally facing aspect of the system. What is more, they can confidently and happily make changes, with the knowledge that if they do break something, they will be alerted to it. (Of course, they should also be contributing or changing tests to validate the new behaviour.)
If you have a pathological dislike for developers, this would excuse you from writing tests. Of course, it excuses you from being a developer, as well.
You hate yourself.
If you can marginally tolerate other developers, but have a deep-seated loathing for yourself, you could also argue against writing tests.
As the code base to which you are contributing grows and time passes, even if you are the sole contributor, it is very likely you will not recognise code when you need to revisit it. Without well-written tests, you may not be able to discern the expected behaviour either.
And if you insist on still not writing tests, you will be doomed to a vortex of trial-and-error, punctuated with the desperate debugging forays described above. Eventually, you will get to the point where you are too scared to make any changes, as you don’t know what the impact will be.
As mentioned above, TDD leads to cleaner, better code, as writing testable code leads to the following:
- Single responsibility – the code you are testing should do only what is required to make the test pass. This means that is more likely that the code would only have one reason to change, which is the definition promulgated by Uncle Bob.
- Design – you need to think about the problem you are trying to solve, as well as the permutations you need to address. This is an up-front process, rather than an after-thought: your test describes the behaviour you anticipate, not what you accidentally discovered.
- Decoupling – focussing only on the class, or even method, under test means that the dependencies need to be mockable. This encourages, for example, dependency injection as a means of inversion of control, as interfaces can be passed into the class, and the expected behaviour can be emulated with little effort using a framework like Mockito.
If you neglect to create tests as a matter of course while developing, you will inevitably end up with an unmaintainable, organic mess, and you will be too scared to make changes.
You are an adrenaline junkie.
If, in fact, you enjoy being scared, and you are a developer, not writing tests is a very good way of getting that fix. If you don’t have the confidence that any change you make would ring alarm bells if it broke something else, you are risking fallout every time you promote your code into Production. You might as well ask someone to hold your beer while you do a deployment.
Testing in Production is not just irresponsible, it is disrespectful to your customer and your end-users.
If you are prepared to face the consequences of disappointing your customers, and steadily eroding their confidence, go ahead and don’t write tests.
Of course, even if you do write tests, and even if you have rounds of automated and manual acceptance testing, there is still a chance that a bug will slip through into Production. When this happens, the very first action you take is: write a test.
Make sure you can recreate the exact outcome demonstrated by the bug, and make sure the test fails. When it passes, you should be confident that you have addressed the problem. And when the whole suite passes, you know you did not break anything else.
It’s too hard.
If it is too hard for you to write tests, it is most likely because your code is bad. Fix your code.
As Albert Einstein pointed out, “[g]enius is making complex ideas simple, not making simple ideas complex.” We should continuously strive towards making our code as simple as possible (but not simpler. Also Albert.) TDD again raises its hand here; writing just enough code to make your (simple) test pass should innately result in a simple implementation.
Focus on testing the public methods of your class. This is sometimes referred to as black-box testing. This means you are verifying the outcome, or result, of an interaction with the class, not the inner workings. This may force you to craft these public methods in a way that enables testing, such as passing in a parameter rather than relying on an internal, magical field value.
Remember, too, to test only the class you are interested in, and mock its friends. This approach is very much not recommended in a genuine social environment, but it is paramount in unit testing.
Other good coding practices, such as composition over inheritance, also lend themselves to easier testing.
Testing should not be hard. Apart from anything else, it will make it more likely that it will be neglected or gamed. And if someone else takes over or contributes to your code-base, it should be easy for them to do the same with the testing components.
You are not professional.
Software programming is a relatively young profession, when compared to medicine, law, plumbing, and masonry, as examples. However, we have indisputably moved beyond that tender age when lackadaisical, carefree behaviour is indulgently tolerated.
We have responsibilities and obligations. And one of those obligations is to produce reliable, robust software that inspires confidence (and, of course, delight) in our customers and our peers. Without testing, we cannot be confident, and neither can our stakeholders.
When a customer asks you to cut out testing to save time or money, he or she is asking you to be unprofessional. You have the right, and the ethical duty, to refuse that. As Uncle Bob points out in Clean Code if a patient told a surgeon to do away with the silly hand-washing ritual prior to surgery, should the surgeon comply? The patient is the boss, after all.
If you don’t care about your chosen profession sufficiently to want to do it the right way, don’t write tests. But also: don’t write code. Rather find a profession that does suit you, and does make you happy, and which will make you proud to announce yourself a member thereof.
Testing should be an implicit factor in the definition of done when evaluating the completion of a given piece of work and should be one of the primary considerations in a code review. Unit tests should be considered first-class citizens in our code repositories and should be subjected to the same rigour as production code, to ensure maintainability and readability.
Writing unit tests is not just part of what we do. It is part of how we do what we do.