Testing can be one of those hot topics for developers. Some love it, some hate it, and some just don’t care. But the reality is, testing is a crucial part of software engineering. It’s not optional, and it’s not something you can skip if you plan on shipping a quality product.
Ask most established engineering teams if they test their code or should test their code, and the answer will be a resounding yes. But ask them if they have a good testing strategy, and the answer might not be as clear.
And that’s not without good reason. Testing can be a complex topic - TDD? BDD? Unit tests? Integration tests? End-to-end tests? The list goes on, and that’s just the methodologies! What about the tools? Code Coverage? Automated testing, and CI?
It’s easy to see why testing can be overwhelming. But it doesn’t have to be. And it shouldn’t put you off implementing a good testing strategy, because at the end of the day, yes, you need testing, and no, it’s not optional.
Common Complaints
One of the biggest reasons developers don’t test their code is because they don’t know where to start. There are so many different types of tests, methodologies, and tools that it can be overwhelming. There’s also the fear of writing bad tests, or tests that don’t cover the right things.
Tests are only as good as the person writing them. Sometimes tests only test the test writer’s understanding of the code, not the code itself.
But this shouldn’t be a reason to avoid testing. Testing is a skill, and like any skill, it takes time to learn. You’re not going to be an expert overnight, but that doesn’t mean you shouldn’t start. We wouldn’t be engineers if we skipped over every hard problem that came our way.

Another common complaint is that testing takes too long.
“I don’t have time to write tests, I need to ship this feature!” The return on investment for testing is not immediate, we’re a small team, we don’t have the time to invest in testing when we could be shipping features and manual testing is good enough.
It’s true that tests, like any code, requires some maintenance. It can also be true that writing tests can take longer than writing the code itself sometimes. But to say that you don’t have time to write tests is a false economy.
This takes a short-term view that ignores the tech-debt you’re going to accumulate by not actively writing tests for your code. It ignores the time spent debugging, after the context has been lost, and the time spent fixing bugs and running through a new release process to get the fix deployed. It ignores the cost to your support team, and most importantly, the cost to your users.
This view also ignores the strides the testing community is making to make testing easier and more accessible. Playwright now has a test generator that can generate tests for you. It opens a browser, let’s you navigate around as you would do in a manual test, and then generates the test code for you. It’s not perfect, but it’s a great start to getting that included in a test suite.
AI is here, and it isn’t going anywhere either. It’s been used to generate whole test suites. It’s not perfect either, but it’s not about being perfect if it saves you time getting started.
Our codebase isn’t big enough to warrant it, we can get by with manual testing.
The size of your codebase and team is mostly irrelevant. Okay, granted, there are cases where you can forgo some testing. This blog for instance, has little use for any sort of integration tests or e2e tests. It’s statically generated html after all. But it’s also not going to be delivered to paying customers either. That doesn’t mean I’m not using TypeScript/linters to catch errors though.
Testing is about getting feedback on your code, and the earlier you can get that feedback, the better. Manual testing is not something that should be skipped either, but it’s not a replacement for automated tests, and vice versa. Automated tests can catch bugs that manual testing might miss, human error is a thing after all. Computers are also brilliant at repetitive tasks, running the same tests over and over again, without getting bored or tired.
Show me a human that can run through the same test suite 100 times without missing a beat, and I’ll show you AI in disguise.
Testing Strategy
Let’s back up a bit and talk about testing strategy. When folks begin to talk about testing, they can sometimes forget that a good testing strategy is more than just automated tests (or arguments against automated tests).
A good testing strategy is one that is tailored to your team, your product, and your goals. It’s not a one-size-fits-all solution, and it’s not something you can always copy-paste from someone else. Most importantly, and this is often overlooked, a good testing strategy is one that includes multiple layers of feedback.
Yes, for most projects you should write automated tests. You should if you value your time anyway. Much better to catch a bug locally from the tests than getting a call at 2:00 in the morning and fix it then. Often I find myself saving time when I put time in to write tests. It may or may not take longer to implement what I’m building, but I (and others) will almost definitely save time maintaining it.
Kent C. Dodds has a great article on writing tests where he takes the “testing pyramid” concept, and uses it to create the “testing trophy”.
"The Testing Trophy" 🏆
— Kent C. Dodds 🌌 (@kentcdodds) February 6, 2018
A general guide for the **return on investment** 🤑 of the different forms of testing with regards to testing JavaScript applications.
- End to end w/ @Cypress_io ⚫️
- Integration & Unit w/ @fbjest 🃏
- Static w/ @flowtype 𝙁 and @geteslint ⬣ pic.twitter.com/kPBC6yVxSA
Testing is more than just writing unit tests or integration tests. It’s about getting feedback on your code at every level. This includes static analysis, it includes unit and component tests, it includes integration tests, and it includes end-to-end tests. But it also includes manual testing, which could happen in peer reviews or QA before a release.
In order to have a good testing strategy, you need to have a mix of all these different types of tests. Ignoring one or more of these types of tests can lead to a false sense of security, and can lead to bugs slipping through the cracks. This ultimately leads to a worse experience for your users, and more time spent fixing bugs.
Specifically, when designing a testing strategy, you should understand why and what you’re testing. There is some debate around code coverage and whether it’s a good metric to measure your tests by. Ultimately I agree with Kent’s take on it:
I’ve heard managers and teams mandating 100% code coverage for applications. That’s a really bad idea. The problem is that you get diminishing returns on your tests as the coverage increases much beyond 70% (I made that number up… no science there). Why is that? Well, when you strive for 100% all the time, you find yourself spending time testing things that really don’t need to be tested. Things that really have no logic in them at all (so any bugs could be caught by ESLint and Flow). Maintaining tests like this actually really slow you and your team down.
Being so focused on one aspect of testing (code coverage) isn’t likely to breed good tests. Why are you testing that piece of logic or that component? What is the overall benefit to the test? Remember, we’re testing so that we can get feedback on our code, and so that we can catch bugs before they reach our users. A user doesn’t necessarily care if you called one function over another, so long as the end result is the same.
This is where a great article from Artem Zakharchenko comes in - the true purpose of testing.
You write tests to validate the intention behind the system.
This is what we should be mostly testing for. Does the product fulfill the contract we’ve made with our users. Does it do what it’s supposed to? Not how it does it.
If I have an application that is supposed to return a list of users in a team for our customer, the customer doesn’t care if it’s using a for
loop or a map
function. The customer cares they can see their list of users from their team.
Test with intention.
I’m sold! What now?
So, what should you do? If you’re not already testing your code, or if you’re not happy with your current testing strategy, here are some things to consider:
- Start small: You don’t have to write tests for your entire codebase all at once. Start with a small feature or component, and write tests for that. Once you’re comfortable with that, move on to the next feature or component.
- Keep going: Once you’ve started writing tests, don’t stop. Make testing a part of your regular development process, and write tests for every new feature or consider it for every bug fix. Repeat after me, “testing is not optional”.
- Automate: If you’re not already automating your tests, start now. Automated tests can save you time in the long run, and can catch bugs that manual testing might miss. Invest some time to set up a CI pipeline if folks are finding it hard to get on bored running tests locally. Make it a requirement before merging code.
- Get all the feedback: Make sure you’re getting feedback on your code at every level. This includes static analysis, unit tests, integration tests, end-to-end tests, and manual testing. Consider peer reviews, QA, and user feedback as part of your testing strategy. None of these are a replacement for the other, none should be skipped without a very good reason.
- Don’t be afraid to refactor: If you find that your code is hard to test, don’t be afraid to refactor it. Testing is a great way to find areas of your code that could be improved, and refactoring can make your code easier to test. Ironically, once you have tests in place, refactoring becomes a lot less scary and easier to do!
Conclusion
Even with these arguments, you might be fighting an uphill battle to get testing implemented in your team. It’s not easy, and it’s not always fun. But it’s necessary.
Try to encourage your team to see the value in testing, and to see it as an investment in the quality of your product. And try to encourage folks to investigate what testing tools are available today, rather than relying on past experiences.
And repeat after me, “testing is not optional”.
Thanks for reading! 💜