How to do API Testing using Mocha and Chai for JavaScript Applications
Tags: node, javascript, api, testing
Published: 2022-06-24 20:05:11


Ever wonder how to conduct API testing in an efficient and effective manner using Mocha and Chai for Node.js applications? If so, keep reading to find out!


![Here are the technologies we'll be using for this tutorial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dczc3d02mx0ce00q4e07.png)

_Figure 1: Here are the technologies we'll be using for this tutorial_

## Setup

First, here is the example Node.js application we're going to use for this tutorial: [https://github.com/ShehanAT/nodejs-api-testing-mocha-chai](https://github.com/ShehanAT/nodejs-api-testing-mocha-chai)

Start by cloning the project and opening it up in VSCode or a similar coding editor.

Note: If you want to follow along with a different Node.js app feel free to do so.

## Introduction

Before diving into development, let's have an overview of the sample application above:

- This is a Node.js application with a Library Management System theme
- The Node.js server application that we're testing in this tutorial is contained in the `server` directory of our GitHub repository. Accordingly, our tests are contained in the `server/test` folder
- For the sake of simplicity, we won't be connecting the app to an external database. We'll be using seeder files and `.txt` files to cover our database functionality
- There are three main APIs for this application:
  - User API: handles all requests related to users, authentication and registration
  - Books API: handles all requests related to creating books, borrowing books, listing borrowed books, listing all books
  - Category API: handles all requests related to listing categories of books
- The main testing technologies used for this tutorial will be the Mocha JavaScript test framework and the Chai BDD/TDD JavaScript assertion library
- We won't go over the routes and middleware for this application but rather will cover the tests written for this app's routes and middleware

## Testing

First, let's start by going over the API tests in the `homepageTests.test.js` file:

```
describe('Server should: ', () => {
    it('return success message after GET / request', (done) => {
        server
            .get('/')
            .set('Connection', 'keep alive')
            .set('Content-Type', 'application/json')
            .expect(200)
            .end((err, res) => {
                if(err){
                    console.log(err);
                }
                res.status.should.equal(200);
                done();
            });
    });
});
```

The above test, encompassed in a `it()` method, is testing for whether a `200` status code is received after making a `GET` request to the URL: `/`

Pretty simple right?

Let's move on to testing the User API...

In the `new_server/test/userAuthTest.test.js` file, we have our first test:

```
// validRegisterDetails: {
//  fullName: 'Cleanthes Stoic',
//  username: 'testuser1',
//  password: 'password1',
//  email: 'cleanthes123@gmail.com',
//  passwordConfirm: 'password1'
// }

describe('User Api: ', () => {
  it('should return valid HTML and 200 Response Code', (done) => {
    server
      .post('/api/v1/users/signup')
      .set('Connection', 'keep alive')
      .set('Content-Type', 'application/json')
      .type('form')
      .send(validRegisterDetails)
      .expect(201)
      .end((err, res) => {
        if(err){
          console.log(err);
        }
        res.status.should.equal(201);
        done();
      });
  });
  ...
```

Now for an explanation of the above test:

- We are sending a `POST` request to the URL: `/api/v1/users/signup`
- We are sending the `validRegisterDetails` object as the request body for this request. This object contains the following fields: `username`, `password`, `email`, `passwordConfirm` and `fullName`
- The `.type('form')` call will set the `application/x-www-form-urlencoded` request header for the request
- The validation of the request takes place in the `end()` call, where we assert that the response code should equal `201`
- Finally, the `done()` call ensures that Mocha will wait for the current test to finish before moving onto the next test. This `done()` call is important in coordinating the test execution order in the asynchronous environment that we're running tests in

The next test in the `new_server/test/userAuthTest.test.js` file is the following:

```
// invalidUsernameMin5: {
//  fullName: 'Cleanthes Stoic',
//  username: 'test',
//  password: 'password2',
//  email: 'cleanthes456@gmail.com',
//  passwordConfirm: 'password2'
// }

 it('should throw error if username is less than 5 characters', (done) => {
    server
      .post('/api/v1/users/signup')
      .set('Connection', 'keep alive')
      .set('Content-Type', 'application/json')
      .type('form')
      .send(invalidUsernameMin5)
      .expect(400)
      .end((err, res) => {
        if(err){
          console.log(err);
        }
        res.status.should.equal(400);
        res
          .body[0]
          .error
          .should.equal('Please provide a username with at least 5 characters.');
        done();
      });
  });
```

Ok, let's go over the above test now:

- This test sends a request to the same URL as the previous one
- The only difference between this test and the previous one is the request body
- The request body contains a purposeful error: The username value is less than 5 characters in length. This is done intentionally to test the username validation feature of the corresponding server route
- Once the request is sent, we expect a `400` error status code. This assertion is done via the `res.status.should.equal(400)` statement
- Finally, we also assert that the `res.body[0].error` field should contain the username length validation error that we are expecting

On to the next test in `userAuthTest.test.js` file:

```
//  noFullName: {
//    username: 'cato123',
//    password: '123456',
//    email: 'cato123@gmail.com',
//    passwordConfirm: '123456'
//  },

 it('Should throw error if fullname is empty', (done) => {
    server
      .post('/api/v1/users/signup')
      .set('Connection', 'keep alive')
      .set('Content-Type', 'application/json')
      .type('form')
      .send(noFullName)
      .expect(400)
      .end((err, res) => {
        res.status.should.equal(400);
        res.body[0].error.should.equal('Your Fullname is required');
        done();
      });
  });
```

Now for an explanation of the above test:

- This test is very similar to the previously added test with the only notable difference being that we're testing whether a validation error is returned in response to excluding the `fullName` field from the request body
- The assertion for the presence of the `fullName` validation error is done via the statement: `res.body[0].error.should.equal('Your Fullname is required');`

On to the fourth test in the `userAuthTest.test.js` file:

```
// signUp: {
//  fullName: 'Zeno of Citium',
//  username: 'zeno123',
//  password: '123456',
//  email: 'zeno123@gmail.com',
//  isAdmin: true,
//  passwordConfirm: '123456'
// },

  it('Should register a new user when provided request body is valid', (done) => {
    server
      .post('/api/v1/users/signup')
      .set('Connection', 'keep alive')
      .set('Content-Type', 'application/json')
      .type('form')
      .send(signUp)
      .expect(201)
      .end((err, res) => {
        if(err){
          console.log(err);
        }
        res.status.should.equal(201);
        res.body.message.should.equal('Signed up successfully');
        const currentUser = jwt.decode(res.body.token);
        // const currentUser = res.body.token;
        expect(currentUser.currentUser.email).toEqual('zeno123@gmail.com');
        expect(currentUser.currentUser.username).toEqual('zeno123');
        expect(currentUser.currentUser.fullName).toEqual('Zeno of Citium');
        done();
      });
  });
```

Now for an explanation for the above test:

- This test is unlike the previous tests we've added because we're testing the happy path scenario for the `POST /api/v1/users/signup` route: Successful user registration
- As the `signup` request body object contains valid data, we use the `expect()` method to assert the `email`, `username` and `fullName` fields

And now for the last test in the `userAuthTest.test.js` file:

```
 it('Should Check for existing username', (done) => {
    server
      .post('/api/v1/users/validate')
      .set('Connection', 'keep alive')
      .set('Content-Type', 'application/json')
      .type('form')
      .send({ username: 'rufus' })
      .expect(409)
      .end((err, res) => {
        res.status.should.equal(409);
        res.body.message.should.equal('Username already exist');
        done();
      });
  });
```

Here is its explanation:

- This test is checking whether the route can detect duplicate usernames. The expected response we're looking for is an error message indicating the user of duplicate usernames
- As see in the `send()` call, we only need to pass an object with a single `username` field containing the duplicate username
- On top of asserting for the `'Username already exist'` message we assert that the response status code is `409`

Now that we're done with the tests for the Users API, we can now cover the tests for the Books API.

These tests are contained in the `/new_server/test/bookRouteTest.test.js` file.

Here is one such test:

```
  it('If user is logged in then request: GET /users/:userId/books should return a list of books held by the user :userId', (done) => {
      server
        .get('/api/v1/users/3/books')
        .set('Connection', 'keep alive')
        .set('Content-Type', 'application/json')
        .set('x-access-token', 'Bearer ' + xAccessToken)
        .type('form')
        .expect(200)
        .end((err, res) => {
          if(err){
            console.log(err);
          }
          res.status.should.equal(200);
          res.body.message.length.should.equal(3);
          done();
        });
  });
```

Here's an explanation for the above test:

- This test sends a `GET` request to the `/api/v1/users/{userId}/books` route
- The expected response is a list of books that are currently held by the user. For this test, we're using the `userId` of 3 and are expecting the list to contain 3 objects. Hence, our assertion checks the length of the `res.body.message` object for a value of 3

Here is a second test for the `bookRouteTest.test.js` file:

```
const expect = chai.expect;

it('Should allow the user to create a new book and return it if the user is logged, via the request: POST /books', (done) => {
    server
      .post('/api/v1/books')
      .set('Connection', 'keep alive')
      .set('Content-Type', 'application/json')
      .set('x-access-token', 'Bearer ' + xAccessToken)
      .send([ addBook, adminUser ])
      .type('form')
      .expect(201)
      .end((err, res) => {
        if(err){
          console.log(err);
        }

        expect(res.body.book.bookId).to.not.be.null;
        expect(res.body.book.name).to.not.be.null;
        expect(res.body.book.isbn).to.not.be.null;
        expect(res.body.book.description).to.not.be.null;
        expect(res.body.book.productionYear).to.not.be.null;
        expect(res.body.book.categoryId).to.not.be.null;
        expect(res.body.book.author).to.not.be.null;
        expect(res.body.book.total).to.not.be.null;

        done();
      });
});
```

Now for an explanation of the above test:

- This test sends a `POST` request to the `/api/v1/books` route. This route is supposed to create a new book based on the data provided in the request body and return that book object in the response body. Therefore, all of our assertions check of the exsitence of all the fields in the response body object
- If you're wondering why there is an array with two objects in the `send()` method for this test, its because both the new book details(contained in `addBook`) and the logged in user's details(contained in `adminUser`) are needed by the API to create the book. Therefore, the most convienient way to send both objects was to add them to an array and send the entire array as the request body. I will admit that this is not the cleanest, most modular and maintainable way of sending the request body, but for the purposes of this small example application we can make an exception
- One final note on the `expect()` method: This is not the `expect()` method from the Jest testing framework and instead from the Chai JavaScript testing library. As shown on top of the test, we make sure to define `expect` with `chai.expect` in order to use Chai's `expect()` method. We use Chai's `expect()` over the one provided by Jest because it makes it much easier to check for the existence of a value via its chainable getters i.e. `.to.not.be.null`. More on Chai's chainable getters in [their official documentation](https://www.chaijs.com/api/bdd/)

Now for the third test in the `bookRouteTest.test.js` file:

```
 it('Should allow the user to borrow a book if the user is logged in, via the request: POST /users/{userId}/books', (done) => {
        server
          .post('/api/v1/users/4/books')
          .set('Connection', 'keep alive')
          .set('Content-Type', 'application/json')
          .set('x-access-token', 'Bearer ' + xAccessToken)
          .send([ addBook, nonAdminUser ])
          .type('form')
          .expect(200)
          .end((err, res) => {
            if(err){
              console.log(err);
            }

            expect(res.body.rentedBook.bookId).to.not.be.null;
            expect(res.body.rentedBook.userId).to.not.be.null;
            expect(res.body.rentedBook.returned).to.be.false;

            done();
          });
      });
```

Now for an explanation for the above test:

- This test will make a `POST` request to the URL `/user/{userId}/books`. This route's main purpose is to let the logged in user borrow a book. The borrowed book will be returned in the response body
- The request body will contain an array of the same format as the previous test, as both the details of the book to be borrowed(`addBook`) and the user details(`nonAdminUser`) are needed by the route's middleware
- The response body should contain the `rentedBook` object, which represents the book rented by the user. The Chai testing library's `expect()` method is used to conduct the field existence check validation via the chainable getter: `.to.not.be.null`

Lastly, let's quickly cover the Category API by going over the last test covered in this tutorial, contained in the `/new_server/test/categoryRouteTest.test.js` file:

```
 it('If user is logged in then request: GET /books should return a list of 3 books', (done) => {
        server
          .get('/api/v1/category')
          .set('Connection', 'keep alive')
          .set('Content-Type', 'application/json')
          .set('x-access-token', 'Bearer ' + xAccessToken)
          .type('form')
          .expect(200)
          .end((err, res) => {
            if(err){
              console.log(err);
            }
            res.status.should.equal(200);
            expect(res.body.categories.length).to.be.greaterThan(5);
            done();
          });
    });
```

Now for an explanation for the above test:

- This test will make a `GET` request to the URL `/api/v1/category`. This route's main purpose is to provide a list of book categories currently present in the library management system as its response
- All we're checking for here is the length of the `res.body.categories` object. As we know that there are 5 book categories in the system, we setup the assertion with the appropriate chainable getter of: `to.be.greaterThan(5)`

If you made it this far, congrats! You now have some idea of how to write effective API tests for JavaScript applications.

## Conclusion

Well that's it for this post! Thanks for following along in this article and if you have any questions or concerns please feel free to post a comment in this post and I will get back to you when I find the time.

If you found this article helpful please share it and make sure to follow me on [Twitter](https://twitter.com/SeanAtukorala) and [GitHub](https://github.com/ShehanAT), connect with me on [LinkedIn](https://www.linkedin.com/in/shehan-atukorala-8631491a5/) and subscribe to my [YouTube channel](https://www.youtube.com/channel/UCtxed_NljgtAXrQMMdLvhrQ).