Home  >  Article  >  Web Front-end  >  Unit testing for NodeJS using Mocha and Chai

Unit testing for NodeJS using Mocha and Chai

Patricia Arquette
Patricia ArquetteOriginal
2024-10-26 06:58:03719browse

Unit testing for NodeJS using Mocha and Chai
Unit testing is important because it checks small parts of code to make sure they work right and finds bugs early. It's important to do these tests before releasing an app. This guide will cover unit testing with Mocha and Chai.

Why Mocha and Chai?

Mocha is a feature-rich JavaScript test framework that runs on Node.js, making asynchronous testing simple and enjoyable. It provides functions that execute in a specific order, collecting test results and offering accurate reporting.

Chai is a BDD/TDD assertion library that can be used with any JavaScript testing framework. It offers several interfaces, allowing developers to choose the assertion style they find most comfortable.

Benefits of using Mocha and Chai

  1. Readable and expressive assertions

    Chai provides different assertion styles and syntax options that work well with Mocha, allowing you to choose the style that suits your needs for clarity and readability.

  2. Support for asynchronous testing

    Mocha handles asynchronous testing easily, letting you test asynchronous code in Node.js applications without needing extra libraries or complex setups.

Setting Up the Testing Environment

Installing dependencies

First, let's set up a new Node.js project and install the necessary dependencies:

mkdir auth-api-testing
cd auth-api-testing
npm init -y

# Install production dependencies
npm install express jsonwebtoken mongoose bcryptjs dotenv

# Install development dependencies
npm install --save-dev mocha chai chai-http supertest nyc

Setup testing scripts

Add the following scripts to your package.json:

{
  "scripts": {
    "test": "NODE_ENV=test mocha --timeout 10000 --exit",
    "test:coverage": "nyc npm test"
  }
}

Project Overview

Before diving into testing, let's understand the application we'll be testing. We're building a simple but secure authentication API with the following features.

Application Structure

src/
├── models/
│   └── user.model.js       # User schema and model
├── routes/
│   ├── auth.routes.js      # Authentication routes
│   └── user.routes.js      # User management routes
├── middleware/
│   ├── auth.middleware.js  # JWT verification middleware
│   └── validate.js         # Request validation middleware
├── controllers/
│   ├── auth.controller.js  # Authentication logic
│   └── user.controller.js  # User management logic
└── app.js                  # Express application setup

API Endpoints

  1. Authentication Endpoints:
POST /api/auth/register
- Registers new user
- Accepts: { email, password, name }
- Returns: { token, user }

POST /api/auth/login
- Authenticates existing user
- Accepts: { email, password }
- Returns: { token, user }
  1. User Endpoints:
GET /api/users/profile
- Gets current user profile
- Requires: JWT Authentication
- Returns: User object

PUT /api/users/profile
- Updates user profile
- Requires: JWT Authentication
- Accepts: { name, email }
- Returns: Updated user object

Environment setup for testing

Create a .env.test file for test-specific configuration:

PORT=3001
MONGODB_URI=mongodb://localhost:27017/auth-api-test
JWT_SECRET=your-test-secret-key

Understanding Test Structure

Let's create our first test file test/auth.test.js

const chai = require('chai');
const chaiHttp = require('chai-http');
const app = require('../src/app');
const User = require('../src/models/user.model');

chai.use(chaiHttp);
const expect = chai.expect;

describe('Auth API Tests', () => {
  // Runs before all tests
  before(async () => {
    await User.deleteMany({});
  });

  // Runs after each test
  afterEach(async () => {
    await User.deleteMany({});
  });

  // Test suites will go here
});

Test Lifecycle Hooks

Mocha provides several hooks for test setup and cleanup:

  • before(): Runs once before all tests

  • after(): Runs once after all tests

  • beforeEach(): Runs before each test

  • afterEach(): Runs after each test

Mocha's describe() and it() Blocks

Tests are organized using describe() blocks for grouping related tests and it() blocks for individual test cases:

mkdir auth-api-testing
cd auth-api-testing
npm init -y

# Install production dependencies
npm install express jsonwebtoken mongoose bcryptjs dotenv

# Install development dependencies
npm install --save-dev mocha chai chai-http supertest nyc

Writing our first test suite

Testing Registration and Login

{
  "scripts": {
    "test": "NODE_ENV=test mocha --timeout 10000 --exit",
    "test:coverage": "nyc npm test"
  }
}

Testing Authentication

src/
├── models/
│   └── user.model.js       # User schema and model
├── routes/
│   ├── auth.routes.js      # Authentication routes
│   └── user.routes.js      # User management routes
├── middleware/
│   ├── auth.middleware.js  # JWT verification middleware
│   └── validate.js         # Request validation middleware
├── controllers/
│   ├── auth.controller.js  # Authentication logic
│   └── user.controller.js  # User management logic
└── app.js                  # Express application setup

Database Testing

Setting Up Test Database

POST /api/auth/register
- Registers new user
- Accepts: { email, password, name }
- Returns: { token, user }

POST /api/auth/login
- Authenticates existing user
- Accepts: { email, password }
- Returns: { token, user }

Testing CRUD Operations

GET /api/users/profile
- Gets current user profile
- Requires: JWT Authentication
- Returns: User object

PUT /api/users/profile
- Updates user profile
- Requires: JWT Authentication
- Accepts: { name, email }
- Returns: Updated user object

Best Practices

Keep Test Atomic

  • Each test should stand alone and not depend on other tests

  • Tests should be able to run in any sequence

  • Use the before, after, beforeEach, and afterEach hooks for proper setup and cleanup

    PORT=3001
    MONGODB_URI=mongodb://localhost:27017/auth-api-test
    JWT_SECRET=your-test-secret-key
    

    Follow the AAA Pattern

    • Arrange: Set up test data and conditions
    • Act: Execute the code being tested
    • Assert: Verify the results
    const chai = require('chai');
    const chaiHttp = require('chai-http');
    const app = require('../src/app');
    const User = require('../src/models/user.model');
    
    chai.use(chaiHttp);
    const expect = chai.expect;
    
    describe('Auth API Tests', () => {
      // Runs before all tests
      before(async () => {
        await User.deleteMany({});
      });
    
      // Runs after each test
      afterEach(async () => {
        await User.deleteMany({});
      });
    
      // Test suites will go here
    });
    

    Test Edge Cases

    Test boundary conditions, error scenarios, invalid inputs, and empty or null values.

    describe('Auth API Tests', () => {
      describe('POST /api/auth/register', () => {
        it('should register a new user successfully', async () => {
          // Test implementation
        });
    
        it('should return error when email already exists', async () => {
          // Test implementation
        });
      });
    });
    

    Use Descriptive Test Names

    Test names should clearly describe the scenario being tested, follow a consistent naming convention, and include the expected behavior.

    describe('POST /api/auth/register', () => {
      it('should register a new user successfully', async () => {
        const res = await chai
          .request(app)
          .post('/api/auth/register')
          .send({
            email: 'test@example.com',
            password: 'Password123!',
            name: 'Test User'
          });
    
        expect(res).to.have.status(201);
        expect(res.body).to.have.property('token');
        expect(res.body).to.have.property('user');
        expect(res.body.user).to.have.property('email', 'test@example.com');
      });
    
      it('should return 400 when email already exists', async () => {
        // First create a user
        await chai
          .request(app)
          .post('/api/auth/register')
          .send({
            email: 'test@example.com',
            password: 'Password123!',
            name: 'Test User'
          });
    
        // Try to create another user with same email
        const res = await chai
          .request(app)
          .post('/api/auth/register')
          .send({
            email: 'test@example.com',
            password: 'Password123!',
            name: 'Test User 2'
          });
    
        expect(res).to.have.status(400);
        expect(res.body).to.have.property('error');
      });
    });
    

    Mock External Dependencies

    • Use stubs and mocks for external services
    • Isolate the code being tested
    • Control test environment
    describe('Protected Routes', () => {
      let token;
      let userId;
    
      beforeEach(async () => {
        // Create a test user and get token
        const res = await chai
          .request(app)
          .post('/api/auth/register')
          .send({
            email: 'test@example.com',
            password: 'Password123!',
            name: 'Test User'
          });
    
        token = res.body.token;
        userId = res.body.user._id;
      });
    
      it('should get user profile with valid token', async () => {
        const res = await chai
          .request(app)
          .get('/api/users/profile')
          .set('Authorization', `Bearer ${token}`);
    
        expect(res).to.have.status(200);
        expect(res.body).to.have.property('email', 'test@example.com');
      });
    
      it('should return 401 with invalid token', async () => {
        const res = await chai
          .request(app)
          .get('/api/users/profile')
          .set('Authorization', 'Bearer invalid-token');
    
        expect(res).to.have.status(401);
      });
    });
    

    Handle Promises Properly

    • Always return promises or use async/await
    • Use proper error handling
    • Test both success and failure cases
    const mongoose = require('mongoose');
    
    before(async () => {
      await mongoose.connect(process.env.MONGODB_URI_TEST);
    });
    
    after(async () => {
      await mongoose.connection.dropDatabase();
      await mongoose.connection.close();
    });
    

    Set Appropriate Timeouts

    • Set realistic timeouts for async operations
    • Configure global timeouts in mocha config
    • Override timeouts for specific tests when needed
    describe('User CRUD Operations', () => {
      it('should update user profile', async () => {
        const res = await chai
          .request(app)
          .put(`/api/users/${userId}`)
          .set('Authorization', `Bearer ${token}`)
          .send({
            name: 'Updated Name'
          });
    
        expect(res).to.have.status(200);
        expect(res.body).to.have.property('name', 'Updated Name');
      });
    
      it('should delete user account', async () => {
        const res = await chai
          .request(app)
          .delete(`/api/users/${userId}`)
          .set('Authorization', `Bearer ${token}`);
    
        expect(res).to.have.status(200);
    
        // Verify user is deleted
        const user = await User.findById(userId);
        expect(user).to.be.null;
      });
    });
    

Introducing Keploy Unit Test Generator

Writing manual test cases for mocha with chai, though effective, often has several challenges:

  1. Time-consuming: Making detailed test suites by hand can take a lot of time, especially for large codebases.

  2. Difficult to maintain: As your application changes, updating and maintaining manual tests becomes more complex and prone to errors.

  3. Inconsistent coverage: Developers might focus on the main paths, missing edge cases or error scenarios that could cause bugs in production.

  4. Skill-dependent: The quality and effectiveness of manual tests depend heavily on the developer's testing skills and familiarity with the codebase.

  5. Repetitive and tedious: Writing similar test structures for multiple components or functions can be boring, possibly leading to less attention to detail.

  6. Delayed feedback: The time needed to write manual tests can slow down development, delaying important feedback on code quality and functionality.

To tackle these issues, Keploy has introduced a ut-gen that uses AI to automate and improve the testing process, which is the first implementation of the Meta LLM research paper that understands code semantics and creates meaningful unit tests.

It aims to automate unit test generation (UTG) by quickly producing thorough unit tests. This reduces the need for repetitive manual work, improves edge cases by expanding the tests to cover more complex scenarios often missed manually, and increases test coverage to ensure complete coverage as the codebase grows.

%[https://marketplace.visualstudio.com/items?itemName=Keploy.keployio]

Key Features

  • Automate unit test generation (UTG): Quickly generate comprehensive unit tests and reduce redundant manual effort.

  • Improve edge cases: Extend and improve the scope of tests to cover more complex scenarios that are often missed manually.

  • Boost test coverage: As the codebase grows, ensuring exhaustive coverage becomes feasible.

Conclusion

In conclusion, mastering Node.js backend testing with Mocha and Chai is important for developers who want their applications to be reliable and strong. By using Mocha's testing framework and Chai's clear assertion library, developers can create detailed test suites that cover many parts of their code, from simple unit tests to complex async operations. Following best practices like keeping tests focused, using clear names, and handling promises correctly can greatly improve your testing process. By using these tools and techniques in your development workflow, you can find bugs early, improve code quality, and deliver more secure and efficient applications.

FAQ’s

What's the difference between Mocha and Chai, and do I need both?

While both are testing tools, they serve different purposes. Mocha is a testing framework that provides the structure for organizing and running tests (using describe() and it() blocks). Chai is an assertion library that provides functions for verifying results (like expect(), should, and assert). While you can use Mocha without Chai, using them together gives you a more complete and expressive testing solution.

How do I set up and clean up test data between tests?

Mocha provides several lifecycle hooks for managing test data:

  • before(): Run once before all tests

  • beforeEach(): Run before each test

  • afterEach(): Run after each test

  • after(): Run once after all tests

Example:

mkdir auth-api-testing
cd auth-api-testing
npm init -y

# Install production dependencies
npm install express jsonwebtoken mongoose bcryptjs dotenv

# Install development dependencies
npm install --save-dev mocha chai chai-http supertest nyc

How should I structure my tests for better organization and maintenance?

  1. Group related tests using describe() blocks

  2. Use descriptive test names that clearly state the expected behavior

  3. Follow the AAA (Arrange-Act-Assert) pattern in each test

  4. Keep tests atomic and independent

  5. Organize test files to mirror your source code structure

Example:

{
  "scripts": {
    "test": "NODE_ENV=test mocha --timeout 10000 --exit",
    "test:coverage": "nyc npm test"
  }
}

What's the difference between before, beforeEach, after, and afterEach?

'before' runs once before all tests, 'beforeEach' runs before each individual test, 'afterEach' runs after each individual test, and 'after' runs once after all tests are complete. These are useful for setting up and cleaning up test data.

How do I test async code?

You can use async/await or return promises in your tests. Just add 'async' before your test function and use 'await' when calling async operations. Make sure to set appropriate timeouts for longer operations.

The above is the detailed content of Unit testing for NodeJS using Mocha and Chai. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn