首页  >  文章  >  web前端  >  使用 Mocha 和 Chai 对 NodeJS 进行单元测试

使用 Mocha 和 Chai 对 NodeJS 进行单元测试

Patricia Arquette
Patricia Arquette原创
2024-10-26 06:58:03625浏览

Unit testing for NodeJS using Mocha and Chai
单元测试很重要,因为它会检查一小部分代码,以确保它们正常工作并尽早发现错误。在发布应用程序之前进行这些测试非常重要。本指南将涵盖 Mocha 和 Chai 的单元测试。

为什么选择摩卡和柴茶?

Mocha 是一个功能丰富的 JavaScript 测试框架,运行在 Node.js 上,使异步测试变得简单而愉快。它提供按特定顺序执行、收集测试结果并提供准确报告的功能。

Chai 是一个 BDD/TDD 断言库,可以与任何 JavaScript 测试框架一起使用。它提供了多种接口,允许开发人员选择他们认为最舒服的断言样式。

使用摩卡和柴的好处

  1. 可读且富有表现力的断言

    Chai 提供了与 Mocha 配合良好的不同断言样式和语法选项,允许您选择适合您的清晰度和可读性需求的样式。

  2. 支持异步测试

    Mocha 可以轻松处理异步测试,让您可以在 Node.js 应用程序中测试异步代码,而无需额外的库或复杂的设置。

设置测试环境

安装依赖项

首先,让我们建立一个新的 Node.js 项目并安装必要的依赖项:

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

设置测试脚本

将以下脚本添加到您的package.json:

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

项目概况

在深入测试之前,让我们先了解一下我们将要测试的应用程序。我们正在构建一个简单但安全的身份验证 API,具有以下功能。

应用结构

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端点

  1. 身份验证端点:
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. 用户端点:
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

测试环境搭建

为特定于测试的配置创建 .env.test 文件:

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

了解测试结构

让我们创建第一个测试文件 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
});

测试生命周期挂钩

Mocha 提供了几个用于测试设置和清理的钩子:

  • before():在所有测试之前运行一次

  • after():在所有测试后运行一次

  • beforeEach():在每次测试之前运行

  • afterEach():每次测试后运行

Mocha 的describe() 和it() 块

使用describe()块来组织相关测试,并使用it()块来组织单个测试用例:

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

编写我们的第一个测试套件

测试注册和登录

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

测试认证

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

数据库测试

设置测试数据库

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 }

测试 CRUD 操作

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

最佳实践

保持测试原子性

  • 每个测试应该独立,不依赖于其他测试

  • 测试应该能够以任何顺序运行

  • 使用 before、after、beforeEach 和 afterEach 挂钩进行正确的设置和清理

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

    遵循 AAA 模式

    • 安排:设置测试数据和条件
    • Act:执行正在测试的代码
    • 断言:验证结果
    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
    });
    

    测试边缘情况

    测试边界条件、错误场景、无效输入以及空值或空值。

    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
        });
      });
    });
    

    使用描述性测试名称

    测试名称应清楚地描述正在测试的场景,遵循一致的命名约定,并包含预期的行为。

    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');
      });
    });
    

    模拟外部依赖项

    • 使用外部服务的存根和模拟
    • 隔离正在测试的代码
    • 控制测试环境
    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);
      });
    });
    

    正确处理承诺

    • 始终返回 Promise 或使用 async/await
    • 使用正确的错误处理
    • 测试成功和失败的案例
    const mongoose = require('mongoose');
    
    before(async () => {
      await mongoose.connect(process.env.MONGODB_URI_TEST);
    });
    
    after(async () => {
      await mongoose.connection.dropDatabase();
      await mongoose.connection.close();
    });
    

    设置适当的超时

    • 为异步操作设置实际的超时
    • 在 mocha 配置中配置全局超时
    • 在需要时覆盖特定测试的超时
    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;
      });
    });
    

介绍 Keploy 单元测试生成器

使用 chai 为 mocha 编写手动测试用例虽然有效,但通常会遇到一些挑战:

  1. 耗时:手动制作详细的测试套件可能需要很多时间,尤其是对于大型代码库。

  2. 难以维护:随着应用程序的变化,更新和维护手动测试变得更加复杂并且容易出错。

  3. 覆盖范围不一致:开发人员可能会关注主要路径、缺失的边缘情况或可能导致生产中出现错误的错误场景。

  4. 取决于技能:手动测试的质量和有效性在很大程度上取决于开发人员的测试技能和对代码库的熟悉程度。

  5. 重复且乏味:为多个组件或函数编写类似的测试结构可能很无聊,可能会导致对细节的关注较少。

  6. 延迟反馈:编写手动测试所需的时间会减慢开发速度,延迟对代码质量和功能的重要反馈。

为了解决这些问题,Keploy 推出了 ut-gen,它使用 AI 来自动化和改进测试过程,这是 Meta LLM 研究论文的第一个实现,可以理解代码语义并创建有意义的单元测试。

它的目标是通过快速生成彻底的单元测试来自动化单元测试生成(UTG)。这减少了对重复性手动工作的需求,通过扩展测试以覆盖经常手动错过的更复杂的场景来改善边缘情况,并增加测试覆盖率以确保随着代码库的增长而完全覆盖。

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

主要特点

  • 自动化单元测试生成(UTG):快速生成全面的单元测试并减少冗余的手动工作。

  • 改进边缘情况:扩展和改进测试范围,以涵盖手动经常错过的更复杂的场景。

  • 提高测试覆盖率:随着代码库的增长,确保详尽的覆盖率变得可行。

结论

总之,掌握使用 Mocha 和 Chai 进行 Node.js 后端测试对于希望其应用程序可靠且强大的开发人员来说非常重要。通过使用 Mocha 的测试框架和 Chai 的清晰断言库,开发人员可以创建详细的测试套件,涵盖其代码的许多部分,从简单的单元测试到复杂的异步操作。遵循最佳实践(例如保持测试重点、使用清晰的名称和正确处理承诺)可以极大地改进您的测试过程。通过在开发工作流程中使用这些工具和技术,您可以及早发现错误、提高代码质量并交付更安全、更高效的应用程序。

常见问题解答

Mocha 和 Chai 之间有什么区别,我需要两者吗?

虽然两者都是测试工具,但它们的用途不同。 Mocha 是一个测试框架,提供组织和运行测试的结构(使用describe() 和it() 块)。 Chai 是一个断言库,提供用于验证结果的函数(如expect()、should 和assert)。虽然您可以在没有 Chai 的情况下使用 Mocha,但将它们一起使用可以为您提供更完整且更具表现力的测试解决方案。

如何在测试之间设置和清理测试数据?

Mocha 提供了多个生命周期挂钩来管理测试数据:

  • before():在所有测试之前运行一次

  • beforeEach():在每次测试之前运行

  • afterEach():每次测试后运行

  • after():在所有测试后运行一次

示例:

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

我应该如何构建我的测试以便更好地组织和维护?

  1. 使用describe()块对相关测试进行分组

  2. 使用清楚说明预期行为的描述性测试名称

  3. 在每个测试中遵循 AAA(安排-执行-断言)模式

  4. 保持测试原子性和独立性

  5. 组织测试文件以反映源代码结构

示例:

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

before、beforeEach、after 和 afterEach 之间有什么区别?

'before' 在所有测试之前运行一次,'beforeEach' 在每个单独的测试之前运行,'afterEach' 在每个单独的测试之后运行,'after' 在所有测试完成后运行一次。这些对于设置和清理测试数据很有用。

如何测试异步代码?

您可以在测试中使用异步/等待或返回承诺。只需在测试函数之前添加“async”,并在调用异步操作时使用“await”即可。确保为较长的操作设置适当的超时。

以上是使用 Mocha 和 Chai 对 NodeJS 进行单元测试的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn