Are unit tests necessary?
Allow me to assert my opinion
Of course they are! Aren’t they?!
I can already hear people shouting an answer to the top headline. I myself have changed which horse I’ve rode over the years when it comes to unit tests. In the early days of my professional career, I didn’t even know these were a thing. While I try to cover all of my code in unit tests, I do believe there are cases where it’s simply not worth the effort.
Let’s dig in.
Automated testing and their intent
In my view, there are four types of automated tests that every developer should be aware of:
Unit Tests
Integration Tests
Acceptance Tests
Load Tests
Unit tests are for testing the code you wrote. The key idea with unit tests is to isolate your code from external code (code you didn’t write). When your code uses external code, that code is known as a dependency. We “mock” external code to provide predictable responses given a set of input. These are the type of tests we’re going to explore in this post, the rest are for awareness/information.
Integration tests remove isolation from your code. Think of theses tests as “does my code play nice with external code?”.
Acceptance tests can have a few meanings, but I like to think of these as “end-to-end” testing. From UI to the persistence and back, does an entire feature work?
Load tests help simulate massive amounts of traffic to your application. Often times code works in small-scale but falls over or manifests issues at-scale.
Regression tests in my view can be some or all of the above. A regression test helps protect “yesterdays” feature from “todays” feature. Without tests covering existing functionality, you really have no idea if your new feature has inadvertently broken a past one.
Canary in the mine
Based on the descriptions alone, you can begin to see the value of testing your code. If nothing else, they act as a canary test. A canary test is a metaphor back to when coal miners exposed a bird to the air the miner breathed. If the canary died, it became an early warning.
Automated test failures are akin to the canary dying. Sometimes the canary dies but it’s a false-positive due to poorly written tests or tests that need updated due to business logic changes.
I really haven’t made a value proposition to you nor have I actually given you an answer to the title question. Let’s press on.
The role of unit tests in relationship to you, the developer
As a senior engineer, I often review other senior engineer code. It may surprise you that I skip the unit tests when scrolling through the pull request during review. The reason is because unit tests are primarily for you, the developer.
In my view, a unit test is a proof of work (with less computing power than Bitcoin) of your code. Does my code do what I think it does? The test confirms it.
A young Kevin would scoff at the notion that the code doesn’t do what I think it does and therefore unit testing is just redundant work. I would further confirm this assertion that it works — by running the code locally to make sure it does whatever the Jira card asked me to do.
It turns out though that despite my best intentions, I code bugs into features everyday. How do I know? Because when I confirm my code does what I think it does (via writing a unit test), I discover often that it doesn’t do what I think it does.
Common things I discover during unit testing:
I’ve misnamed something (a variable, a class, a file)
I’ve introduced un-testable code (more on this below)
Null-references
Edge-cases
REGEX issues
Validation issues
While some things still sneak through, my overall code quality has a much higher first impression to reviewers. I’d much rather find the above issues before my code goes to review, QA or worse — production. Humbly, not writing unit tests is risky and is a testament to how much risk you are willing to take on your code changes. If you value your PTO, nights and weekends; I’d highly suggest covering your code as best as you can.
So there it is, you have to write tests, right? Not quite.
Yeah, but…
There are several situations where it’s simply not worth writing tests in my humble opinion. However it’s mostly due to external business forces:
Agencies often underprice themselves to get the job. The first things to go out the window in a tight budget are automated tests and QA. You get what you pay for. While this isn’t “acceptable” in my view, it’s a distinct reality at a lot of agencies. The irony of this option is that you may end up spending more time fixing bugs after it is deployed.
You are working on a trivial project where quality is simply not a high requirement. You’ve coded it your best shot and if it doesn’t work right, making a change is trivial and has low consequences for failure.
The sky is falling and if you don’t push the code change right now, the cost of waiting is higher than the assurance of unit test coverage.
Your boss is annoyed because it can take twice the time to write tests than it did to not write tests. If you are in this situation, upgrade your job to another company.
Great Scott Marty!
It takes time to write and nobody wants to go back and cover old things. But I firmly believe in this old adage:
If You Don’t Have Time to Do It Right, When Will You Have Time to Do It Over?
If not for unit tests, QA or some sort of automation — how do you know when you’ve achieved the expected outcome? Because a few manual tests worked?
I highly suggest that you include unit test writing time in any scope/estimate. If not, you’ll likely be finding time later to fix what wasn’t caught before you deployed it.
Testing 1, 2, 3; is this thing on?
None of this matters if you don’t write code that is testable. Testable code is code that uses the principal of inversion of control (IoC). IoC is a means to inject dependencies rather that create them in your code. IoC is closely related to dependency injection which is for another day. Code that has “trapped” dependencies are not easily testable. Imagine you have a class that sends an email. If you can trade out the class that sends the email, you’ll have to make sure you have an email server ready to run every time a test runs (which now makes it an integration test).
Even if code is injected, without some sort of abstraction; you might not be able to easily test things without possibly wrapping it first in some other interface. One notorious class that can be tricky to mock/test is HttpClient.
Which means you (and your team) should embrace SOLID programming as that dovetails very nicely with unit testing. Which takes me to the topic of legacy code.
Your legacy awaits
Legacy code is code that we all loathe and dread modifying. We no longer know what it does exactly and changing it can unleash a chaos monkey amongst all-the-things.
One of the reasons that nobody wants to touch legacy code is due to lack of test coverage. If we make a change, we may have little to no idea if we broke anything. Add test coverage to legacy code is suspect at best. Often the reasoning for the code’s purpose has left your organization with one of the turnover cycles. There’s also a good chance it’s not-testable anyway because there was no initial requirement to make it so.
TDD/BDD/Meh
There are a few schools of thought that come with unit testing. Most it’s a matter of when you write the tests, before or after you’ve written the principal code. You’ve likely heard of TDD (Test-Driven Development) and BDD (Behavior-Driven Development). My short description on these is they are both not good. You can read all about them here but the main rub I have with TDD/BDD is that they want you to write tests before you write your code. For example, create a failing test; then make it pass by writing code. While it sounds like a logical thing to do, I think it’s just completely counter-intuitive to how I think.
Most of the time I have no idea what the exact implementation will be and therefore I can’t even begin to think of what to name the test files/methods. I usually have a good design grasp going into a feature; I just couldn’t tell you what I’m going to name the methods/classes/variables until I get coding. Heck, I change my mind on naming things many times because Naming Is Hard.
For me I have to make the feature work, then cover the code with tests.
I gotcha covered
So if code is written first, how do I know I’ve written tests against the new code? Simple, I setup a draft PR and can easily see what code I’ve added. I cover a class in a test fixture then move my way through the PR until I’ve covered it all.
Can I miss something using that process? Sure I can but that’s also why I used DotCover by JetBrains (not a sponsor). Their tool can literally give you a line-by-line highlight (in either Visual Studio or Rider) to indicate if zero or more tests cover a line of code.
I still have to write tests that are coherent and actually tests things. This post isn’t about “How to write unit tests”, rather it’s about “Should I write unit tests?”
I aim for about 90-95% code coverage (which DotCover can tell you that percentage). Getting 100% coverage is ideal but there is a diminishing return trying to get it all covered.
So do you have to write unit tests?
No, you don’t. That may be shocking after reading through all of that, however I strongly think you should. There is a lot of peace of mind developing today’s features knowing that yesterday’s features are somewhat guarded by previous test coverage. As a bonus, you might actually find a bug before your peers do.
Unit Tests won’t find load issues, cohesion issues or make you breakfast. They will help protect your organization and your reputation. I expect a veteran coder to cover their code in tests. Not doing so is risky, perhaps you’re willing to accept that risk.
Unit tests are not foolproof, they won’t guard against run-time issues with DI or connection string/credential/configuration issues (look back at the other automated test types).
Happy testing!


Great article! I had a similar evolution, working in agencies trained me to think testing wasn’t important but Uncle Bob was always a voice in my head. I agree with him that unit tests are the difference between professional engineers and everyone else. As for TDD though, I’ll say that it is counter-intuitive at first, but having disciplined myself on a few tasks using this I am a big fan of it now. I can say that the tasks I’ve completed test first have been less buggy and I’ve also been much more confident about shipping the code. I actually feel now that it’s harder to achieve high coverage writing tests retrospectively