There comes a point in every developer’s coding life where they think, “aw, snap, I wish I had some tests for this”. Tests are a fundamental necessity for maturing projects and by avoiding writing them, you’re actually not saving as much time as you think you are.
It works the way it is, I don’t need tests!
Maybe not now, but how about in 6 months time? Tests are an ideal way to:
- Ensure functionality doesn’t break. When you’re pushing forward, refactoring portions of the app, adding extra features, it’s relieving to know you have a bedrock of test cases that will flag up a problem if you break something. Otherwise, you could be shipping faulty code in a less-maintained part of the app.
- Good tests are self-documenting. They clearly list out what the app should do and its expected behaviour. They are valuable as functionality checklists, developer reference, and as a learning tool for developers new to the project.
- Protecting against regressions. Did something untested in your app go wrong in production? Add a test for it to ensure it never goes wrong again without you knowing.
When is the best time to focus more on tests?
The easy answer is: closer to v1.0 than v0.1
Usually, a project will start life being rapidly prototyped, moving at high velocity during the inaugural 80% of its development. There are some strategies such as TDD preaching that tests should come first and dictate development every step of the way. In our experience, this doesn’t work well with a “release early, release often” deployment pace, especially in a small team covering a wide surface area. You tend to get slowed down by constantly mirroring code changes with compatibility updates to your tests.
This doesn’t mean don’t think about tests though. I believe that if your application is designed well, as an interlinking lattice of clearly defined components and modules, constructing an effective test suite at a more appropriate point of project maturity is an efficient and rewarding process, and helps mature the app further as it reaches the big 1.0.
Focus on evolving the app and its functionality into what it needs to be. Then seal in and protect that functionality with tests.
What do I test?
Focus on core functionality. First think, what is it exactly your app does? Is it an API? Or an app with a frontend? Or a message queue consumer? The structure and purpose of your service influences the kind of tests you write.
That said, it goes without saying that your any app should be broken down into modules and components. Separation of concerns is paramount and keeps your application organised logically, with individually maintainable aspects.
We could go as far as to unit test each and every module to ensure its output is appropriate for a given input. In our experience this tends to be overkill. To begin with, we usually go for higher-level integration tests, which test the output of the endpoint and assumes everything underneath must be functioning to at least a certain level otherwise we wouldn’t get an expected result.
For example, if you’re testing an API, you might write a suite which sends requests to your API routes and validates the response. If there’s anything awry in the code serving that response, then you won’t get a 200 success code. This kind of low resolution testing leaves you confident there aren’t any bugs along the execution path of successful requests, and they are quick to develop.
There are plenty of downsides with this approach. For one, you have no idea whether something’s wrong in areas of code that execute in different scenarios to a standard request. Perhaps you might have some error handling around an external API call, which might ironically contain an error which crashes the app when the external API is having issues and this code is executed. Low resolution tests won’t pick that up. Additionally, there might be subtle changes in the environment, state, or input/output to your application that might not necessarily trigger crashes, but otherwise undesired behaviour.
These are disadvantages that could be solved by rigorous TDD development practises and near-100% unit test coverage. In reality, small companies rarely have time for that.
What counts is how you mitigate this. Strong conventions, careful application design, detailed monitoring, and keeping things modular and simple are the best ways to ensure everything pulls together reliably. If you get this right, then there’s no need to sink time into brain-dead granular tests that are veiled as a substitute to good application design.
What tools are out there?
There are some fantastic tools in the Node.JS community you can use to help structure and approach your tests. Our favourite test framework is mocha, but there are many others out there such as node’s native assert, tap, or you could write your own using stock JS.