Jest代码安全的第一道关卡

161 阅读17分钟

Jest 使用文档

一、引言

1.1 软件测试的重要性

在软件开发的生命周期中,软件测试是确保软件质量、稳定性和可靠性的关键环节。通过有效的测试,可以发现代码中的潜在缺陷、错误和逻辑漏洞,避免在生产环境中出现严重问题,降低维护成本,提高用户满意度。软件测试不仅有助于提升产品质量,还能增强团队的信心,促进项目的顺利推进。

1.2 Jest 的诞生背景与发展历程

Jest 由 Facebook 开发并开源,旨在解决 JavaScript 项目在测试过程中面临的各种挑战。随着 JavaScript 在前端和后端开发中的广泛应用,对高效、易用的测试框架的需求日益增长。Jest 应运而生,它凭借其简洁的语法、强大的功能和良好的性能,迅速在 JavaScript 开发者社区中获得了广泛的认可和使用。自发布以来,Jest 不断更新迭代,添加了许多新特性和功能,逐渐成为 JavaScript 测试领域的主流框架之一。

二、Jest 简介

2.1 定义与功能概述

Jest 是一个 JavaScript 测试框架,它提供了一系列工具和功能,帮助开发者编写和运行测试用例。Jest 的核心功能包括简单易用的断言库、自动模拟机制、快照测试、测试覆盖率报告生成等。通过这些功能,Jest 能够让测试过程变得更加简单、高效,同时提高测试的准确性和可靠性。

2.2 特点与优势

  • 简洁易用:Jest 的语法简洁明了,易于上手。开发者可以通过简单的 API 编写测试用例,减少学习成本。
  • 自动模拟:Jest 能够自动模拟模块和函数,大大简化了测试过程中对依赖项的处理。
  • 快照测试:快照测试功能可以方便地对比代码输出的变化,确保代码的行为符合预期。
  • 快速高效:Jest 采用了并行测试和缓存机制,能够显著提高测试的执行速度。
  • 丰富的插件生态:Jest 拥有丰富的插件和扩展,开发者可以根据项目需求进行定制化配置。

2.3 与其他测试框架的对比

在 JavaScript 测试领域,除了 Jest,还有 Mocha、Jasmine 等知名测试框架。与 Mocha 相比,Jest 具有更简洁的语法和更强大的自动模拟功能,无需额外配置复杂的断言库和模拟工具。与 Jasmine 相比,Jest 在性能和功能上更具优势,尤其是在处理大型项目和复杂测试场景时表现更为出色。

三、安装与配置

3.1 环境要求

在安装 Jest 之前,确保你的开发环境满足以下要求:

  • Node.js:Jest 是基于 Node.js 运行的,因此需要安装 Node.js 环境。建议使用 Node.js 的长期支持版本(LTS)。
  • npm 或 yarn:用于安装 Jest 及其依赖包。

3.2 安装方式

  • 全局安装:通过 npm 或 yarn 全局安装 Jest,可以在任何项目中使用 Jest 命令。
npm install -g jest
# 或者
yarn global add jest
  • 项目内安装:在项目根目录下安装 Jest,将其作为开发依赖项。
npm install --save-dev jest
# 或者
yarn add --dev jest

3.3 配置文件

Jest 的配置文件通常为jest.config.js,位于项目根目录下。以下是一些常见的配置项:

  • testMatch:指定测试文件的匹配模式,默认值为["/tests//.js?(x)", "**/?(.)+(spec|test).js?(x)"]。
  • coveragePathIgnorePatterns:指定哪些文件不生成测试覆盖率报告,例如["/node_modules/"]。
  • moduleNameMapper:用于模块路径映射,例如将@/映射到项目的src/目录。
module.exports = {
    testMatch: ["**/*.test.js"],
    coveragePathIgnorePatterns: ["/node_modules/"],
    moduleNameMapper: {
        "^@/(.*)$": "<rootDir>/src/$1"
    }
};

3.4 自定义配置

除了上述常见配置项,Jest 还支持许多其他配置选项,如测试环境、测试脚本、模拟函数的配置等。开发者可以根据项目的具体需求进行自定义配置,以满足不同的测试场景。

四、基本使用

4.1 测试文件命名规则

Jest 默认会查找符合特定命名规则的测试文件,常见的命名规则为[文件名].test.js或[文件名].spec.js。例如,要测试math.js文件,可创建math.test.js或math.spec.js。

4.2 编写测试用例

  • 测试函数:使用test函数来定义一个测试用例,test函数接受两个参数:测试用例的描述和测试函数。
test('adds 1 + 2 to equal 3', () => {
    // 测试逻辑
});
  • 断言函数:使用expect函数来进行断言,expect函数接受一个实际值,然后通过调用各种匹配器(如toBe、toEqual等)来判断实际值是否符合预期。
function add(a, b) {
    return a + b;
}
test('adds 1 + 2 to equal 3', () => {
    expect(add(1, 2)).toBe(3);
});

4.3 运行测试

在项目根目录下,通过命令行运行测试:

npm test
# 或者
yarn test

如果安装了全局的 Jest,也可以直接运行jest命令。

五、断言与匹配器

5.1 常用断言方法

  • toBe:判断两个值是否严格相等(使用===)。
test('two plus two is four', () => {
    expect(2 + 2).toBe(4);
});
  • toEqual:用于比较对象或数组的内容是否相等。
test('objects are equal', () => {
    const obj1 = { a: 1 };
    const obj2 = { a: 1 };
    expect(obj1).toEqual(obj2);
});
  • toBeTruthy:判断值是否为真。
test('true is truthy', () => {
    expect(true).toBeTruthy();
});
  • toBeFalsy:判断值是否为假。
test('false is falsy', () => {
    expect(false).toBeFalsy();
});
  • toBeGreaterThan:判断值是否大于某个值。
test('5 is greater than 3', () => {
    expect(5).toBeGreaterThan(3);
});
  • toBeLessThan:判断值是否小于某个值。
test('3 is less than 5', () => {
    expect(3).toBeLessThan(5);
});

5.2 高级断言技巧

  • 嵌套断言:可以在一个断言中嵌套多个断言,以更细粒度地验证复杂数据结构。
test('nested objects are equal', () => {
    const obj1 = { a: { b: 1 } };
    const obj2 = { a: { b: 1 } };
    expect(obj1).toEqual({
        a: expect.objectContaining({
            b: expect.any(Number)
        })
    });
});
  • 自定义匹配器:Jest 允许开发者自定义匹配器,以满足特定的测试需求。
expect.extend({
    toBeEven: (received) => {
        const pass = received % 2 === 0;
        if (pass) {
            return {
                message: () => `expected ${received} not to be even`,
                pass: true
            };
        } else {
            return {
                message: () => `expected ${received} to be even`,
                pass: false
            };
        }
    }
});
test('2 is even', () => {
    expect(2).toBeEven();
});

六、测试异步代码

6.1 异步函数返回 Promise

  • 使用 async/await:通过async/await语法可以方便地测试返回 Promise 的异步函数。
function asyncAdd(a, b) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(a + b);
        }, 1000);
    });
}
test('async add works', async () => {
    const result = await asyncAdd(1, 2);
    expect(result).toBe(3);
});
  • 使用.then () :也可以使用.then()方法来处理 Promise。
test('async add works with then', () => {
    return asyncAdd(1, 2).then((result) => {
        expect(result).toBe(3);
    });
});

6.2 使用回调函数

  • 使用 done () :在测试使用回调函数的异步代码时,需要调用done()方法来通知 Jest 测试完成。
function addWithCallback(a, b, callback) {
    setTimeout(() => {
        callback(a + b);
    }, 1000);
}
test('add with callback works', (done) => {
    addWithCallback(1, 2, (result) => {
        expect(result).toBe(3);
        done();
    });
});

6.3 处理异步错误

  • 使用 try/catch:在async/await中,可以使用try/catch来捕获异步错误。
test('async function throws error', async () => {
    async function asyncError() {
        throw new Error('Async error');
    }
    try {
        await asyncError();
    } catch (error) {
        expect(error.message).toBe('Async error');
    }
});
  • 使用.rejectWith () :在使用.then()时,可以使用.rejectWith()来处理 Promise 被拒绝的情况。
test('async function throws error with then', () => {
    async function asyncError() {
        throw new Error('Async error');
    }
    return asyncError().catch((error) => {
        expect(error.message).toBe('Async error');
    });
});

七、模拟函数(Mock Functions)

7.1 创建模拟函数

  • jest.fn() :使用jest.fn()创建一个模拟函数,模拟函数可以记录调用次数、参数等信息。
const mockFunction = jest.fn();
mockFunction();
expect(mockFunction).toHaveBeenCalled();
  • 自定义返回值:可以通过mockReturnValue或mockResolvedValue(用于异步函数)来设置模拟函数的返回值。
const mockFunction = jest.fn().mockReturnValue(42);
const result = mockFunction();
expect(result).toBe(42);

7.2 模拟模块

  • jest.mock() :使用jest.mock()来自动模拟一个模块,Jest 会自动生成一个模拟模块,其中所有导出的函数都被替换为模拟函数。
// module.js
export function originalFunction() {
    return 'original';
}
// test.js
jest.mock('./module');
const { originalFunction } = require('./module');
test('mocked function', () => {
    originalFunction.mockReturnValue('mocked');
    expect(originalFunction()).toBe('mocked');
});
  • 手动模拟模块:除了自动模拟,也可以手动创建一个模拟模块,然后通过jest.mock()指定使用该模拟模块。
// __mocks__/module.js
export function originalFunction() {
    return'mocked';
}
// test.js
jest.mock('./module');
const { originalFunction } = require('./module');
test('manually mocked function', () => {
    expect(originalFunction()).toBe('mocked');
});

7.3 模拟函数的使用场景

  • 隔离测试:通过模拟依赖项,将测试对象与外部依赖隔离开来,确保测试的独立性和准确性。
  • 测试异步操作:模拟异步操作的结果,以便在测试中控制和验证异步行为。
  • 测试边缘情况:通过模拟不同的输入和返回值,测试函数在各种边缘情况下的表现。

八、快照测试

8.1 基本原理与使用方法

  • 原理:快照测试通过生成一个包含代码输出的快照文件,在后续测试中对比实际输出与快照文件的内容,从而判断代码的行为是否发生变化。
  • 使用方法:使用toMatchSnapshot()匹配器来进行快照测试。
function formatUser(user) {
    return `Name: ${user.name}, Age: ${user.age}`;
}
test('format user snapshot', () => {
    const user = { name: 'John', age: 30 };
    expect(formatUser(user)).toMatchSnapshot();
});

8.2 快照文件管理

  • 生成快照文件:首次运行快照测试时,Jest 会在__snapshots__目录下生成一个快照文件,文件名与测试文件相对应。
  • 更新快照文件:当代码发生变化,需要更新快照文件时,可以使用jest --updateSnapshot命令或在测试运行时按下u键。
  • 删除快照文件:如果不再需要某个快照文件,可以手动删除__snapshots__目录下对应的文件。

8.3 快照测试的应用场景

  • UI 组件测试:在测试 React、Vue 等前端框架的组件时,快照测试可以方便地验证组件的渲染结果是否符合预期。
  • 数据格式化测试:对于数据格式化函数,快照测试可以确保数据格式化的结果在代码修改后保持一致。
  • API 响应测试:在测试 API 调用时,快照测试可以验证 API 响应的结构和内容是否发生变化。

九、测试覆盖率

9.1 测试覆盖率的概念与意义

测试覆盖率是指测试代码对被测试代码的覆盖程度,它是衡量测试质量的一个重要指标。较高的测试覆盖率意味着更多的代码被测试到,从而降低了代码中存在未被发现的缺陷的风险。通过分析测试覆盖率报告,开发者可以了解哪些代码没有被充分测试,进而针对性地编写测试用例,提高测试的全面性和有效性。

9.2 Jest 的测试覆盖率工具

Jest 内置了测试覆盖率工具,通过--coverage参数可以生成测试覆盖率报告。在运行测试时,添加--coverage参数:

npm test -- --coverage
# 或者
yarn test -- --coverage

Jest 会在项目根目录下生成一个coverage目录,其中包含详细的测试覆盖率报告,包括 HTML 格式和文本格式的报告。

9.3 覆盖率报告解读

  • 语句覆盖率:表示被执行的语句占总语句数的比例。
  • 分支覆盖率:表示被执行的分支(如if-else语句、switch语句等)占总分支数的比例。
  • 函数覆盖率:表示被调用的函数占总函数数的比例。
  • 行覆盖率:表示被执行的代码行占总行数的比例。

9.4 提高测试覆盖率的方法

  • 补充测试用例:根据覆盖率报告,找出未被覆盖的代码部分,编写相应的测试用例。
  • 优化测试策略:合理设计测试用例,确保覆盖各种可能的输入和场景,避免测试用例的重复和冗余。
  • 使用条件覆盖:对于复杂的条件语句,使用条件覆盖策略,确保每个条件分支都被测试到。

十、Jest 在不同场景下的应用

10.1 前端开发中的应用

  • React 组件测试:使用 Jest 结合 React Testing Library 或 Enzyme,可以方便地测试 React 组件的渲染、交互和状态变化。
import React from'react';
import { render, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
test('renders MyComponent and clicks button', () => {
    const { getByText } = render(<MyComponent />);
    const button = getByText('Click me');
    fireEvent.click(button);
    // 断言组件状态变化
});
  • Vue 组件测试:对于 Vue 组件,可以使用 Jest 结合 Vue Test Utils 进行测试。
import { mount } from '@vue/test-utils';
import MyVueComponent from './MyVueComponent.vue';
test('renders Vue component and updates data', () => {
    const wrapper = mount(MyVueComponent);
    wrapper.find('button').trigger('click');
    // 断言组件数据变化
});

10.2 后端开发中的应用

  • Node.js 应用测试:在 Node.js 应用开发中,Jest 可以用于测试路由、中间件、数据库操作等功能。
const request = require('supertest');
const app = require('./app');
test('GET / returns 200', async () => {
    const response = await request(app).get('/

10.2 后端开发中的应用(续)

  • Node.js 应用测试
    • 路由测试:在测试路由时,除了验证响应状态码,还可以检查响应体的内容。例如,假设我们有一个简单的 Express 应用,包含一个返回用户列表的路由。
const express = require('express');
const app = express();
const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
];
app.get('/users', (req, res) => {
    res.json(users);
});
const request = require('supertest');
test('GET /users returns users list', async () => {
    const response = await request(app).get('/users');
    expect(response.status).toBe(200);
    expect(response.body).toEqual(users);
});
  • 中间件测试:中间件在 Node.js 应用中起着至关重要的作用,比如验证用户身份、记录日志等。以一个简单的身份验证中间件为例,使用 Jest 测试它是否能正确拦截未授权的请求。
const express = require('express');
const app = express();
// 模拟身份验证中间件
const authMiddleware = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    if (authHeader && authHeader.startsWith('Bearer ')) {
        const token = authHeader.split(' ')[1];
        // 这里简单模拟token验证
        if (token === 'validToken') {
            return next();
        }
    }
    res.status(401).send('Unauthorized');
};
app.get('/protected', authMiddleware, (req, res) => {
    res.send('Protected resource');
});
const request = require('supertest');
test('GET /protected rejects unauthenticated requests', async () => {
    const response = await request(app).get('/protected');
    expect(response.status).toBe(401);
    expect(response.text).toBe('Unauthorized');
});
test('GET /protected allows authenticated requests', async () => {
    const response = await request(app)
     .get('/protected')
     .set('Authorization', 'Bearer validToken');
    expect(response.status).toBe(200);
    expect(response.text).toBe('Protected resource');
});
  • 数据库操作测试:对于涉及数据库操作的函数,利用 Jest 的模拟函数来隔离测试。以使用mysql模块进行数据库查询为例。
const mysql = require('mysql');
// 模拟数据库连接池
const pool = mysql.createPool({
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'testdb'
});
// 假设一个获取用户信息的函数
function getUserById(id, callback) {
    const query = 'SELECT * FROM users WHERE id =?';
    pool.query(query, [id], (error, results) => {
        if (error) {
            return callback(error);
        }
        callback(null, results[0]);
    });
}
// 测试文件
const { getUserById } = require('./userService');
const mysql = require('mysql');
jest.mock('mysql');
test('getUserById retrieves user from database', (done) => {
    const mockResults = [{ id: 1, name: 'Alice' }];
    const mockQuery = jest.fn((sql, values, cb) => {
        cb(null, mockResults);
    });
    mysql.createPool.mockReturnValue({
        query: mockQuery
    });
    getUserById(1, (error, user) => {
        expect(error).toBe(null);
        expect(user).toEqual(mockResults[0]);
        done();
    });
});

10.3 测试 GraphQL API

随着 GraphQL 在前后端开发中的广泛应用,使用 Jest 测试 GraphQL API 也变得越来越重要。以express - graphql为例,展示如何使用 Jest 测试 GraphQL 的查询和变更。

const express = require('express');
const { buildSchema } = require('graphql');
const { graphqlHTTP } = require('express - graphql');
// 构建GraphQL schema
const schema = buildSchema(`
    type Query {
        hello: String
    }
`);
// 定义解析器
const root = {
    hello: () => 'Hello world!'
};
const app = express();
app.use('/graphql', graphqlHTTP({
    schema: schema,
    rootValue: root,
    graphiql: true // 开启GraphiQL调试工具
}));
const request = require('supertest');
test('GraphQL query hello returns "Hello world!"', async () => {
    const response = await request(app)
     .post('/graphql')
     .send({ query: '{ hello }' });
    expect(response.status).toBe(200);
    expect(response.body.data.hello).toBe('Hello world!');
});

10.4 测试工具与库的结合使用

在实际项目中,Jest 常常与其他工具和库结合使用,以提高测试效率和质量。

  • Sinon.js:Sinon.js 是一个功能强大的 JavaScript 测试库,提供了丰富的模拟、桩(stub)和间谍(spy)功能。与 Jest 结合使用,可以更灵活地控制测试环境。例如,在测试一个依赖外部 API 的函数时,使用 Sinon 的stub来模拟 API 调用,而不是实际发送请求。
const sinon = require('sinon');
const axios = require('axios');
const myFunction = require('./myFunction');
test('myFunction calls external API correctly', async () => {
    const mockResponse = { data: { message: 'Mocked response' } };
    const stub = sinon.stub(axios, 'get').resolves(mockResponse);
    const result = await myFunction();
    expect(result).toEqual(mockResponse.data);
    stub.restore();
});
  • Cypress:Cypress 是一个用于前端自动化测试的工具,专注于浏览器端的交互测试。虽然 Jest 主要用于单元测试,但在一些场景下,结合 Cypress 可以实现更全面的测试覆盖。例如,使用 Jest 进行组件的单元测试,使用 Cypress 进行端到端的集成测试,确保整个应用在浏览器环境中的行为符合预期。

10.5 持续集成中的 Jest

在持续集成(CI)环境中,Jest 扮演着关键角色,确保每次代码提交都经过充分的测试。以 GitHub Actions 为例,展示如何在 CI 环境中配置 Jest 测试。

name: Jest Tests
on:
  push:
    branches:
      - main
jobs:
  build:
    runs - on: ubuntu - latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup - node@v2
        with:
          node - version: '14'
      - name: Install dependencies
        run: npm install
      - name: Run Jest tests
        run: npm test -- --coverage

通过上述配置,每次代码推送到main分支时,GitHub Actions 会自动拉取代码,安装依赖,然后运行 Jest 测试,并生成测试覆盖率报告。这有助于及时发现代码中的问题,保证项目的稳定性和质量。

10.6 Jest 在微服务架构中的应用

在微服务架构中,各个服务相互独立又协同工作。Jest 可以用于对每个微服务进行单元测试和集成测试,确保服务的可靠性和稳定性。

10.6.1 服务接口测试

每个微服务都会暴露一些接口供其他服务调用。使用 Jest 结合supertest等库,可以测试这些接口的正确性。例如,假设我们有一个用户管理微服务,提供了获取用户信息的接口。

const express = require('express');
const app = express();
const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
];
app.get('/users/:id', (req, res) => {
    const userId = parseInt(req.params.id);
    const user = users.find(u => u.id === userId);
    if (user) {
        res.json(user);
    } else {
        res.status(404).send('User not found');
    }
});
const request = require('supertest');
test('GET /users/:id returns correct user', async () => {
    const response = await request(app).get('/users/1');
    expect(response.status).toBe(200);
    expect(response.body).toEqual(users[0]);
});
test('GET /users/:id returns 404 for non - existent user', async () => {
    const response = await request(app).get('/users/3');
    expect(response.status).toBe(404);
    expect(response.text).toBe('User not found');
});
10.6.2 服务间通信测试

微服务之间通常通过 HTTP、消息队列等方式进行通信。在测试时,可以使用 Jest 模拟其他服务的响应,测试当前服务在不同通信场景下的行为。例如,使用nock库来模拟 HTTP 请求的响应,测试一个依赖其他服务获取数据的微服务。

const axios = require('axios');
const nock = require('nock');
const myService = require('./myService');
test('myService calls other service and processes response', async () => {
    const mockData = { message: 'Mocked data from other service' };
    nock('http://other - service.com')
     .get('/data')
     .reply(200, mockData);
    const result = await myService.fetchData();
    expect(result).toEqual(mockData);
});

10.7 Jest 与测试报告工具的整合

为了更直观地展示测试结果,Jest 可以与一些测试报告工具进行整合。

10.7.1 Jest 与 Allure 的整合

Allure 是一个功能强大的测试报告工具,它可以生成美观、详细的测试报告。通过jest - allure - reporter插件,可以将 Jest 的测试结果集成到 Allure 报告中。

  1. 安装插件:
npm install --save - dev jest - allure - reporter
  1. 在jest.config.js中配置:
module.exports = {
    // 其他配置项
    reporters: [
        'default',
        'jest - allure - reporter'
    ]
};
  1. 运行测试后,在项目根目录下会生成allure - results目录,使用 Allure 命令行工具可以生成报告:
allure serve allure - results

生成的报告中会包含每个测试用例的详细信息,如执行时间、状态、断言结果等,方便团队成员查看和分析测试结果。

10.7.2 Jest 与 Cucumber 的整合(BDD 风格测试)

Cucumber 是一个支持行为驱动开发(BDD)的工具,它使用自然语言描述测试场景。通过cucumber - jest库,可以将 Jest 与 Cucumber 结合,实现 BDD 风格的测试。

  1. 安装依赖:
npm install --save - dev cucumber - jest cucumber
  1. 创建features目录,在其中编写 Cucumber 的特性文件(.feature),例如login.feature:
Feature: User Login
  As a user
  I want to log in to the application
  So that I can access my account
  Scenario: Successful login
    Given I am on the login page
    When I enter my username "testuser" and password "testpass"
    And I click the login button
    Then I should be redirected to my account page
  1. 创建相应的步骤定义文件(.js),在其中使用 Jest 的断言实现每个步骤的逻辑:
const { Given, When, Then } = require('cucumber - jest');
const { render, fireEvent } = require('@testing - library/react');
const LoginPage = require('./LoginPage');
Given('I am on the login page', () => {
    // 渲染登录页面组件
    const { container } = render(<LoginPage />);
    // 可以进行一些初始化操作,如保存页面容器
});
When('I enter my username "testuser" and password "testpass"', () => {
    // 找到用户名和密码输入框,模拟输入
    const usernameInput = document.getElementsByName('username')[0];
    const passwordInput = document.getElementsByName('password')[0];
    fireEvent.change(usernameInput, { target: { value: 'testuser' } });
    fireEvent.change(passwordInput, { target: { value: 'testpass' } });
});
When('I click the login button', () => {
    // 找到登录按钮,模拟点击
    const loginButton = document.querySelector('button[type="submit"]');
    fireEvent.click(loginButton);
});
Then('I should be redirected to my account page', () => {
    // 断言页面是否重定向到正确的URL
    expect(window.location.href).toContain('/account');
});

通过这种方式,可以将业务逻辑以自然语言的形式描述出来,使非技术人员也能理解测试的目的和场景,同时利用 Jest 强大的断言和测试功能实现具体的测试逻辑。