Node.js is a cross-platform, open-source JavaScript run time environment that can run on Windows, Linux, Unix, macOS, and other operating systems. It runs on the V8 JavaScript engine, and executes JavaScript code outside a web browser.
On the other hand, ExpressJS is a web application backend framework for building restful apis with NodeJS.
TypeScript is a free and open-source high-level programming language developed by Microsoft that adds static typing with optional type annotations to JavaScript. In this guide, we will build an express api and add type checking using TypeScript and use it to learn unit testing while building restful apis with NodeJS.
This part is an intergral part of a 3 part series and a complete guide will involve environment setup, connecting an external database like MongoDB and writing database models and writing unit tests for crud operations(get, post, delete, put).
This first part will only discuss the environment setup and overall folder structure.
What Exactly is Unit Testing?
Unit testing is a software development process in which the smallest testable parts of an application, called units, are individually scrutinized for proper operation. In other wards, each part of the application is tested in isolation before merging to the main repository.
In the fast-paced realm of software development, ensuring the reliability of your code is paramount. Unit testing plays a crucial role in maintaining code quality, catching bugs early, and facilitating smoother collaboration within a team. In this multipart series, I'll guide you through the process of setting up a robust unit testing environment for your Node.js application built with Express and TypeScript using Jest and Supertest.
Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and other JavaScript technologies. We will also use Supertest for http assertions, forming the backbone of our testing suite. It is a NodeJS library, and enables developers and testers to write automated tests for routes and endpoints and can be used as a standalone library or with testing frameworks like Jest and provides a high-level abstraction to test HTTP requests.
Project Setup
Before diving into unit testing, it's essential to have a well-organized project structure. Whether you're starting a new project or integrating testing into an existing one, maintaining a clear separation of concerns will make your life easier in the long run. Consider structuring your project with directories for source code, tests, and configuration files. This foundational step ensures that your codebase is ready to embrace a testing mindset.
Steps:
Create a new folder in your desired location and name it unit_testing_api_guide.
With the folder open in your favourite code editor, open terminal and run the following command to initialise a NodeJS project:
npm init -y
After initialising the project, install the following dependencies with the following command:
npm install express cors helmet
express
is NodeJS framework we are using for routing, cors
helps us define a way for accessing our api on different domains whereas helmet
is collection of middleware functions that set various HTTP headers to enhance the security of your Node. I prefer starting with it in any NodeJS project because it starts by hiding vulnerable headers like 'X-Powered-By' header which tells about the technology youre using giving attackers a chance to concentrate on its vulnerabilities to penetrate your API.
All the dependencies for unit testing are only used in development so we will install them as dev dependencies(with a -D or --save-dev flag) along with other development dependencies like typescript after which I will explain.
On the same terminal, run the following command:
npm install -D typescript ts-node nodemon jest ts-jest @types/jest supertest @types/supertest @types/cors @types/express
Let me explain:
TypeScript
is meant to help us in type checking our code and and transpiles to JavaScript. ts-node
is a TypeScript execution engine and REPL for Node.js and will will help us transform TypeScript into JavaScript, enabling direct execution of TypeScript on Node.js without precompiling.
nodemon
is meant to help us restart the server in case of changes while in development. Also note that, in newer versions of NodeJS, nodemon is being phased out and you will find yourself using --watch
flag instead. Nevertheless, we will use it in this guide to restart our server automatically in case need arises.
jest
is the testing framework we are using and @types/jest
for adding TypeScript definitions for jest. On the other hand, ts-jest
is a Jest transformer with source map support and lets you use Jest to test projects written in TypeScript and supports all features of TypeScript including type-checking.
supertest
is for providing high level of abstraction to test http requests and @types/supertest
for providing TypeScript definitions for it.
@types/cors
and @types/express
are also for providing type definitions for cors and express respectively.
These are the dependencies we need for now and note that we may require other dependencies in the next parts of this guide with their type definitions for TypeScript but we will install them when need arises.
Project Folder Structure
Here is our overall project folder structure:
I find this level of separation of concerns appropriate in most of my NodeJS projects and we will be sticking to it in this guide.
Lets create our project step by step and remember our main emphasis is on testing directories and files. However, since we are going to learn how to write unit tests for each line of code from controllers, to routes and code in other directories, we need to understand how other folders are created, what they contain and why separate them.
First, create a new folder in the root directory and call it src. Inside src, create all folders as shown in our folder structure. Worry not, we will understand what each folder consists of step by step.
However, in this first part of the guide, we will write all our code inside the index file, index.ts as its all about environment setup and will go through most of the folders in the coming parts.
Also, create a file named index.ts inside src and place the following code:
import express from "express";
import cors from "cors";
import helmet from "helmet";
const app = express();
// USE HELMET AND CORS MIDDLEWARES
app.use(
cors({
origin: ["*"], // Comma separated list of your urls to access your api. * means allow everything
credentials: true, // Allow cookies to be sent with requests
})
);
app.use(helmet());
app.use(express.json());
app.get("/", async (req: express.Request, res: express.Response) => {
try {
res.send(
"Welcome to unit testing guide for nodejs, typescript and express!"
);
} catch (err) {
console.log(err);
}
});
// Start backend server
const PORT = process.env.PORT || 8800;
app.listen(PORT, () => {
console.log(`Backend server is running at port ${PORT}`);
});
export default app;
Before running our code to start writing unit tests, lets first make some configurations:
Create a file named tsconfig.json in the root directory and add the following configuration:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2020",
"baseUrl": "src",
"noImplicitAny": true,
"sourceMap": true,
"esModuleInterop": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}
Create another file named nodemon.json in the root directory and place the following code:
{
"watch": ["src"],
"ext": ".ts,.js",
"exec": "ts-node ./src/index"
}
Inside package.json, add the following scripts in the scripts section:
"scripts": {
"build": "tsc",
"start": "npm run build && node dist/src/index.js",
"dev": "nodemon",
"test": "jest --watchAll --detectOpenHandles"
},
The build command helps us transpile TypeScript to JavaScript in production where npm start
command is used to start or restart our server. The dist folder is the destination for our resultant JavaScript code as specified in tsconfig.json. In development, nodemon restarts our server automatically without a need for transpilation first.
Also, add the line:
"module": "module",
to your package.json to specify that we are using EsModules not NodeJS's default CommonJS pattern.
All set, now open the terminal and traverse to the location of your home directory and run:
npm run dev
to run the project in development mode.
Note that the command:nodemon
is executed and the result should be as shown below:
Jest Setup
Create a new file in the home directory and name it jest.config.js and place the following code:
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/**/*.test.ts"],
verbose: true,
forceExit: false,
};
This is a basic configuration for jest and we will be adding some additional config code when need arises in the next parts of this series.
Writing Sample Unit Tests
To confirm that our project is setup correctly for unit testing, lets write some tests.
Inside the tests folder, create a new file named app.test.ts and place the following code:
import app from "../index";
import request from "supertest";
describe("GET /", () => {
it('responds with "Welcome to unit testing guide for nodejs, typescript and express!', async () => {
const response = await request(app).get("/");
expect(response.status).toBe(200);
expect(response.text).toBe(
"Welcome to unit testing guide for nodejs, typescript and express!"
);
});
});
Let me explain:
We started by importing our Express app (app) and the supertest library, which is used for making HTTP requests in our tests.
We then have a simple Jest test case, It describes and tests the behavior of your Express app when a GET request is made to the root ("/") endpoint. It expects the HTTP response status to be 200 and the response text to match the expected message.
On your terminal, run the following command:
npm run test
or just
npm test
and you should see the following output:
Congulatulations, you just setup a unit testing environment for a NodeJS, Express app using TypeScript.
Interested in the source code? Checkout the repository for the article here
If you liked the article, consider following me on Twitter
Stay tuned for the next 2 parts in this series and furture articles.
Top comments (1)
Thanks for the article! Just a quick note: the test you showed is actually an integration test not a unit test. To do it you can abstract the route hanlder function and then test it as a unit of code.