Writing Your First Tests For Node.js Apps

ifedayo

Ifedayo Adesiyan

Posted on February 24, 2023

Writing Your First Tests For Node.js Apps

Software testing is a critical process in the software development cycle. It is the process of assessing the performance and features of a software application to discover and diagnose any flaws or bugs within it. This may entail running the software with different inputs to verify that the output meets predetermined criteria. The primary objective of software testing is to guarantee that the software is functioning as intended and satisfies the user's needs.
It is impossible to exaggerate the importance of software testing. It contributes to ensuring the usability, functionality, and dependability of the software. Also, it aids in lowering the price of rectifying errors, which can become significantly more expensive when found later in the development cycle.
There are different types of software testing, including functional testing, performance testing, and security testing. Each type of testing serves a specific purpose and requires a unique set of tools and techniques.
To ensure the effectiveness of software testing, it is crucial to have a comprehensive testing plan that includes the identification of testing goals, the selection of appropriate testing tools, and the creation of test cases.
This article will examine the best techniques for writing tests in TypeScript and provide code examples to illustrate them.

Prerequisites

Popular programming language TypeScript enhances JavaScript with sophisticated features like optional static typing. By using TypeScript to create tests, you can assure the caliber of your code and identify mistakes as they arise early in the development cycle. 
The prerequisites for following this guide are npm (Node Package Manager) and typescript.
Let's check if we have npm installed on our computer

npm -v
# 8.3.1
Enter fullscreen mode Exit fullscreen mode

This command will return the version number of npm if it is installed, or an error message if it is not. If you receive an error message, you will need to install npm on your computer.
To install npm, you can download and install Node.js from the official website. npm is included with Node.js, so when you install Node.js, npm will also be installed on your system.
To check the version of TypeScript installed on your system, you can use the following command in your terminal or command prompt:

tsc -v
# Version 4.7.4
Enter fullscreen mode Exit fullscreen mode

This will display the version number of TypeScript installed on your system. If TypeScript is not installed, you will need to install it using a package manager like npm.
To install TypeScript using npm, you can use the following command:

npm install -g typescript
Enter fullscreen mode Exit fullscreen mode

This will install the latest version of TypeScript globally on your system.

Assumptions

In this tutorial, I assume you already have a node.js project you want to write tests for. We will be writing tests for a CMS (content management system) with TypeScript and Mocha, a popular testing framework for Node.js.
We have the user and post model, with endpoints for authentication and CRUD operations for the post model.

Hands-on

To get started, we need to install some dependencies into our project. We can use NPM to install these dependencies by running the command below in our terminal or command prompt:

npm install --save-dev ts-node mocha chai supertest @types/mocha @types/chai @types/supertest
Enter fullscreen mode Exit fullscreen mode

This will install Mocha, Chai, Supertest, and the TypeScript types for Mocha, Chai, and Supertest.
In your src folder, you create a new folder called tests, and inside the tests folder, you create two files: user.test.ts and post.test.ts. user.test.ts will contain all tests for authentication and user endpoints and post.test.ts will contain all tests for the post endpoints.
Add the following code to user.test.ts

import { expect } from 'chai';
import request from 'supertest';
import app from './../app';



describe('USER AUTH', () => {
    let testUserId: string;
    let authToken: string;

    it('should not create a new user with invalid data', async () => {
        const res = await request(app)
          .post('/auth/signup')
          .send({
            username: 'testuser',
            password: 'testpass',
          });
        expect(res.status).to.equal(400);
        expect(res.body.message).to.equal('Email is required');
    });


    it('should create a new user with valid data', async () => {
      const res = await request(app)
        .post('/auth/signup')
        .send({
          username: 'username',
          password: 'password',
          email: 'email@example.com',
        });
      expect(res.status).to.equal(200);
      expect(res.body.data.token).to.be.a('string');
      expect(res.body.data.user).to.be.an('object');
      expect(res.body.data.user.username).to.equal('username');
      expect(res.body.data.user.email).to.equal('email@example.com');
      expect(res.body.data.user.password).to.not.exist;
      testUserId = res.body.data.user.id;
    });


    it('should not login with invalid credentials', async () => {
        const res = await request(app)
          .post('/auth/login')
          .send({
            username: 'testuser',
            password: 'wrongpass',
          });
        expect(res.status).to.equal(401);
        expect(res.body.message).to.equal('Invalid username or password');
    });

    it('should login with valid credentials and return token', async () => {
        const res = await request(app)
            .post('/auth/login')
            .send({
            username: 'username',
            password: 'password',
            });
        expect(res.status).to.equal(200);
        expect(res.body.data.token).to.be.a('string');
        expect(res.body.data.user.id).to.equal(testUserId);
        expect(res.body.data.user.username).to.equal('username');
        expect(res.body.data.user.email).to.equal('email@example.com');
        expect(res.body.data.user.password).to.not.exist;
        authToken = res.body.data.token
    });

    it('should get the current user', async () => {
        const res = await request(app)
            .get(`/users/${testUserId}`)
            .set('Authorization', `Bearer ${authToken}`);
        expect(res.status).to.equal(200);
        expect(res.body.data.id).to.equal(testUserId);
        expect(res.body.data.username).to.equal('username');
        expect(res.body.data.email).to.equal('email@example.com');
        expect(res.body.data.password).to.not.exist;
    });

    it('should update the current user', async () => {
        const res = await request(app)
            .put(`/users/${testUserId}`)
            .set('Authorization', `Bearer ${authToken}`)
            .send({
            username: 'updatedusername',
            email: 'updatedemail@example.com',
            });
        expect(res.status).to.equal(200);
        expect(res.body.data.id).to.equal(testUserId);
        expect(res.body.data.username).to.equal('updatedusername');
        expect(res.body.data.email).to.equal('updatedemail@example.com');
        expect(res.body.data.password).to.not.exist;
    });

    it('should not update the current user with invalid data', async () => {
        const res = await request(app)
            .put(`/users/${testUserId}`)
            .set('Authorization', `Bearer ${authToken}`)
            .send({
            email: 'invalidemail',
            });
        expect(res.status).to.equal(400);
        expect(res.body.message).to.equal('Invalid email');
    });

    it('should delete the current user', async () => {
        const res = await request(app)
            .delete(`/users/${testUserId}`)
            .set('Authorization', `Bearer ${authToken}`);
        expect(res.status).to.equal(204);

        // Make sure the user was deleted
        const deletedRes = await request(app)
            .get(`/users/${testUserId}`)
            .set('Authorization', `Bearer ${authToken}`);
        expect(deletedRes.status).to.equal(404);
        expect(deletedRes.body.message).to.equal('User not found');
    });
});
Enter fullscreen mode Exit fullscreen mode

These tests cover the authentication flow of creating a new user, logging in, and getting the current user's information. Additionally, it tests the user module's functionality by checking that the current user can be fetched, updated, and deleted.
It's important to note that these tests only cover basic functionality and that there may be additional edge cases and error scenarios that should be tested depending on the specific requirements of your API.
In post.test.ts , we will write tests for the posts endpoints

import { expect } from 'chai';
import request from 'supertest';
import app from './../app';


describe('POST', () => {
    let testUserId: string;
    let authToken: string;
    let testPostId: string;



    it('should create a new user with valid data', async () => {
        const res = await request(app)
        .post('/auth/signup')
        .send({
            username: 'username',
            password: 'password',
            email: 'email@example.com',
        });
        expect(res.status).to.equal(200);
        expect(res.body.data.token).to.be.a('string');
        expect(res.body.data.user).to.be.an('object');
        expect(res.body.data.user.username).to.equal('username');
        expect(res.body.data.user.email).to.equal('email@example.com');
        expect(res.body.data.user.password).to.not.exist;
        testUserId = res.body.data.user.id;
    });


    it('should login and return a token', async () => {
        const res = await request(app)
        .post('/auth/login')
        .send({
            username: 'username',
            password: 'password',
        });
        expect(res.status).to.equal(200);
        expect(res.body.data.token).to.be.a('string');
        expect(res.body.data.user.id).to.equal(testUserId);
        expect(res.body.data.user.username).to.equal('username');
        expect(res.body.data.user.email).to.equal('email@example.com');
        expect(res.body.data.user.password).to.not.exist;
        authToken = res.body.data.token;
    });

    it('should create a new post with authentication', async () => {
        const res = await request(app)
        .post('/posts')
        .send({
            title: 'New Post',
            content: 'This is content for new post.',
        })
        .set('Authorization', `Bearer ${authToken}`);
        expect(res.status).to.equal(200);
        expect(res.body.data.title).to.equal('New Post');
        expect(res.body.data.content).to.equal('This is content for new post.');
        expect(res.body.data.author).to.equal(testUserId);
        testPostId = res.body.data.id;
    });

    it('should get a post by id', async () => {
        const res = await request(app).get(`/posts/${testPostId}`);
        expect(res.status).to.equal(200);
        expect(res.body.data.id).to.equal(testPostId);
        expect(res.body.data.title).to.equal('New Post');
        expect(res.body.data.content).to.equal('This is content for new post.');
        expect(res.body.data.author.id).to.equal(testUserId);
        expect(res.body.data.author.username).to.equal('username');
    });

    it('should update a post by id with authentication', async () => {
        const res = await request(app)
        .put(`/posts/${testPostId}`)
        .send({
            title: 'Updated Post',
            content: 'This is an updated post.',
        })
        .set('Authorization', `Bearer ${authToken}`);
        expect(res.status).to.equal(200);
        expect(res.body.data.id).to.equal(testPostId);
        expect(res.body.data.title).to.equal('Updated Post');
        expect(res.body.data.content).to.equal('This is an updated post.');
        expect(res.body.data.author.id).to.equal(testUserId);
        expect(res.body.data.author.username).to.equal('username');
    });

    it('should delete a post by id with authentication', async () => {
        const res = await request(app)
        .delete(`/posts/${testPostId}`)
        .set('Authorization', `Bearer ${authToken}`);
        expect(res.status).to.equal(204);
    });
});
Enter fullscreen mode Exit fullscreen mode

We will create our final file and name it combine.test.ts , this will help us run our tests in the specific order that we want.

// combine.test.ts
import './user.test';
import './post.test';
Enter fullscreen mode Exit fullscreen mode

In this file, We decided to run the user test first before testing other modules. The order of running tests modules-to-modules can be switched in any desired manner.

Running Tests

To run our tests, we will be adding a command script to our package.json file, we call it test

// package.json
{
  "name": "devto-tutorial-project",
  "version": "1.0.0",
  "scripts": {
    "test": "mocha --require ts-node/register './src/test/combine.test.ts' --clearCache --timeout 60000 --exit"
    ......
    ......
  },
  ......
  ......
}
Enter fullscreen mode Exit fullscreen mode

The code added defines a "test" script in the scripts section, it runs mocha tests using the ts-node/register compiler for TypeScript executing combine.test.ts. Other options such as --clearCache will tell Mocha to clear the required cache before running the tests. This ensures that any changes made to the required modules are reflected in the tests, --timeout60000 will set the timeout for the tests to 60 seconds (60000 milliseconds). If any test takes longer than this, it will fail and --exit tells Mocha to exit the test process once all tests have been completed. 
To run the Mocha tests, you can use the following command in your terminal or command prompt:

npm test
Enter fullscreen mode Exit fullscreen mode

This command will execute the test script defined in our package.json file and run our Mocha tests in the test folder and on combine.test.ts.
On your terminal or command prompt, you should have something similar to this

screenshot of tests ran for the node.js project

Summary

In this article, we've explored how to write tests for a TypeScript backend API using Mocha, Chai, and Supertest. We used a simple API for managing users and posts to demonstrate some best practices for testing a Node.js server. 
By writing tests, we have ensured that our API behaves as expected and can catch errors early in the development process.
In conclusion, software testing is a critical process in software development that helps to ensure the quality and reliability of the software. It is an ongoing process that should be an integral part of the software development cycle.
Now go ahead and write some more tests for some of your other Node.js projects.

💖 💪 🙅 🚩
ifedayo
Ifedayo Adesiyan

Posted on February 24, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related