Intro
Especially when working on larger projects, things can get quite complicated very soon. Some changes here, some minor cleanup there and BOOM - your code doesn't work (the way you want it to) anymore. As developers, we're used to testing newly implemented features, but we (usually) don't test code that was written weeks, months or even years in advance. Wouldn't it be great if we just had certain parts of our code tested automatically, in a controlled and clean environment? Entering Unit Tests.
This article will focus on the efficient testing of Node.js APIs developed in TypeScript. Therefore, prior knowledge in using TypeScript to develop Node.js APIs is assumed. If you're looking for a guide on how to migrate from JavaScript to TypeScript, head over to this article. It should get you up and running real quick.
Before we get our hands on all the technical stuff though, let's have a look at the different testing approaches for your API.
Oh, this post is accompanied by some hedgehog gifs โค I personally relate to the last one the most... my friends even labeled it live footage of me when they bring food near me ๐ค
How should I test?
Well, it all depends on the amount of work you want (and can afford) to put into testing. If you're really dedicated, you might want to write tests for even the smallest chunks of your code (you know, those functions named something like checkIsEmail
, for example). If you're still in your project's planning phase, Test-Drive-Development (TDD) might also be the right approach for you. However, if you're working on a larger project and implementing tests for an existing codebase, you shouldn't really (and probably don't afford to) waste too much time on testing every single crumb of logic there is to your API.
Instead, you might want to focus on testing just your endpoints, following the assumption that if your endpoints execute correctly, the logic behind does as well. Now, while that happens to be a risky assumption at times (especially when certain database interactions are considered), testing should be useful for the developer.
If it's pain, you're doing it wrong.
In a nutshell, there is no single "right" way to implement unit tests into your project. It all depends on your available resources and the role testing plays in your development process. Therefore, think carefully about the depth of your testing.
That being said, let's get our hands on some technical stuff ๐
Preparing your project
In order to implement our first tests, we need to install some packages first. We're using the testing framework Mocha in combination with the assertion library Chai. Those are very common tools and work perfectly with TypeScript. Additionally, we'll use ts-node to run Mocha. ts-node
is capable of executing TypeScript code and thereby allows us to write our tests without having to compile them first. If you're interested in how that's done, read more on REPL (Read-Eval-Print Loops) here.
Run the following command in your project's root directory to install the packages:
npm install chai mocha ts-node --save-dev
Install the packages' types:
npm install @types/chai @types/mocha --save-dev
Next, we're updating our package.json
file to configure Mocha and define our test command:
"scripts": {
"test": "mocha"
},
"mocha": {
"require": [
"ts-node/register"
],
"spec": "tests/**/*.spec.ts"
}
There are multiple configuration options available and an extensive list (although in
.yml
) can be found here.
It is important to require ts-node/register
. Thereby, we're configuring Mocha to use ts-node
for execution of our tests; as our tests are written in TypeScript, this is crucial. If you'd like to require any other files, just add their path to the array. One scenario in which you might want to add another file into the array could be the definition of environment variables for your tests. Just define them in a file and require it.
The spec
option defines where our tests are located. Setting this option is advisable as it prevents Mocha from searching your entire project folder for tests. In our case, Mocha is looking for .spec.ts
files inside the tests
folder.
And that's it! Configuration is done, it's time to write our first test.
The first test
At the location you specified for the spec
property of your Mocha configuration, create your first file sample.spec.ts
.
In there, just paste the following code:
import { expect } from "chai"; // or assert, whichever you prefer
import "mocha";
const addNumbers = (a: number, b: number) => a + b;
describe("Numbers added correctly", () => {
it("should equal 5", () => {
const result = addNumbers(3, 2);
expect(result).to.equal(5);
});
});
In above test, we've defined a function addNumbers
which adds two numbers (parameters a
and b
). Using the describe
function, we're basically "grouping" our tests. Inside the callback, we're then declaring our first test. In order for our test, named should equal 5
, to succeed, the result of addNumbers(3, 2)
must equal 5.
In case above code appears unclear to you, check out both Mocha's and Chai's docs.
Run tests
In order to run our first test, simply run the following command which calls the test
script we defined inside our package.json
:
npm run test
Next, you should see a confirmation that your test succeeded. That's it. You've learned the necessary basics to write your own tests.
Test preparation & cleanup
Another useful feature of Mocha is the possibility of doing preperation/cleanup before/after execution of all tests, a group of tests or every single test. Thereby, we can ensure that our tests run in a clean environment, facing the same conditions everytime they're executed.
This is possible as Mocha collects all tests prior to executing a single one of them. Thereby, we're enabled to define hooks that run before or after tests are executed.
Let's have a look at it.
Run code before any other test is run
Doing so requires you to include below code into the root level of any test file. Alternatively, you could create a new file helper.spec.ts
which takes care of global preparation/cleanup.
before((done) => {
// do some preperation work here
connectionManager.establishConnections(() => {
done(); // call inside a callback to indicate that async operations are complete
});
});
Above code is run before any tests are executed. This allows you to do preperation work that is needed for many (or all) of your tests. The done
callback can be used to indicate that any asynchronous operations are done.
Run code before a group of tests is run
If you would like to do some preperation work prior to executing a specific group of tests, simply put the before
function inside a describe block.
describe("Group of tests", () => {
before((done)=> { /*...*/ }); // executes before below tests
it("test 1", () => { /*...*/ });
it("test 2", () => { /*...*/ });
it("test 3", () => { /*...*/ });
});
Preperation before every single test
Using Mocha's beforeEach
hook, we're enabled to run code before every test that is executed. Just like the before
hook, you can either declare it globally or inside a describe
block.
Cleanup
In order to do cleanup, simply use after
or afterEach
the same way before
or beforeEach
are used.
Outro
Congrats, you now know how to make use of unit tests for your TypeScript Node.js API ๐ I sincerely hope you had a nice read and learned something new. As always, if you have any questions, just comment them and I'll try my best to help you out ๐
I would love to hear your feedback! Let me know what you liked or what could've been better with this tutorial - I always welcome constructive criticism! If you have any questions, just comment them and I'll try my best to help you out :)
In love with what you just read? Follow me for more content like this ๐
Sources
ยฉ GIFs from Giphy
๐ Hooks from Mocha's official documentation
๐ Chai documentation
๐ ts-node
package documentation