0

0

深入理解与实践:使用Jest测试Node.js REST GET请求封装函数

霞舞

霞舞

发布时间:2025-07-13 19:42:38

|

381人浏览过

|

来源于php中文网

原创

深入理解与实践:使用jest测试node.js rest get请求封装函数

本文详细介绍了如何使用Jest框架为Node.js中封装的REST GET请求函数编写单元测试。我们将深入探讨如何模拟HTTP请求(如https.get),处理异步回调,以及验证不同响应场景(成功、错误、JSON/非JSON数据)下的函数行为。通过具体的代码示例,帮助读者掌握高效、可靠的Node.js异步代码测试方法。

1. 理解待测试的REST GET封装函数

在Node.js环境中,进行外部REST API调用是常见的操作。为了提高代码的可维护性和可测试性,通常会将原生的HTTP请求逻辑封装起来。以下是我们将要测试的函数模块:

// crud.js (或类似文件)
const https = require('https'); // 假设这里是 https 模块

function getOptions(req, url) {
  // 实际项目中这里可能根据 req 和 url 生成请求选项
  return url; // 简化处理,直接返回 url
}

function handleResponse(response, callback, errorCallback) {
  let rawData = '';
  response.on('data', (chunk) => {
    rawData += chunk;
  });

  response.on('end', () => {
    if (response.statusCode >= 200 && response.statusCode < 300) {
      callback(checkJSONResponse(rawData));
    } else if (errorCallback) {
      errorCallback(rawData);
    }
  });
}

function checkJSONResponse(rawData) {
  if (typeof rawData === 'object') {
    return rawData; // 如果已经是对象,直接返回
  }

  let data = {};
  if (rawData.length > 0) {
    try {
      data = JSON.parse(rawData);
    } catch (e) {
      console.log('Response is not JSON.');
      if (e) {
        console.log(e);
      }
      data = {}; // 解析失败返回空对象
    }
  }
  return data;
}

module.exports.get = function(req, url, callback, errorCallback) {
  https.get(getOptions(req, url), (response) => {
    handleResponse(response, callback, errorCallback);
  }).on('error', (e) => {
    console.error('MYAPP-GET Request.', e);
    if (errorCallback) {
      errorCallback(e);
    }
  });
};

该模块的核心是 module.exports.get 函数,它负责发起HTTPS GET请求,并通过回调函数 callback 和 errorCallback 处理成功和失败的响应。handleResponse 处理HTTP响应流,而 checkJSONResponse 则尝试将响应数据解析为JSON。

2. 为什么需要单元测试及挑战

对于上述异步操作和外部依赖(如 https 模块)的代码,编写单元测试至关重要。

  • 确保功能正确性: 验证在不同HTTP状态码、不同响应体(JSON、非JSON、空)以及网络错误等场景下,函数是否按预期执行。
  • 隔离性: 单元测试应该只测试单个单元(这里是 module.exports.get 函数及其辅助函数),而不依赖外部网络。这意味着我们需要模拟 https 模块的行为。
  • 可重复性与速度: 真实的网络请求慢且不稳定,模拟请求可以确保测试快速且结果可预测。

主要挑战在于:

  • 异步操作: https.get 是异步的,并通过回调函数传递结果。
  • 外部依赖: https 模块是外部依赖,需要被模拟。
  • 流式响应: response.on('data') 和 response.on('end') 处理响应流,这在模拟时需要特别注意。

3. 使用Jest进行测试

Jest是一个流行的JavaScript测试框架,它提供了强大的断言库、模拟(mocking)功能和异步测试支持。

3.1 准备测试环境

首先,确保你的项目中安装了Jest:

npm install --save-dev jest

在你的测试文件中(例如 crud.test.js),你需要引入待测试的模块和 https 模块(以便进行模拟)。

// crud.test.js
const crud = require('./crud'); // 假设你的封装函数在 crud.js 中
const https = require('https'); // 引入 https 模块,用于模拟

3.2 模拟 https 模块

这是单元测试的关键一步。我们不希望测试真正发起网络请求,因此需要模拟 https.get 方法。Jest提供了 jest.mock() 和 mockImplementation() 来实现这一点。

// 在测试文件的顶部
jest.mock('https'); // 模拟整个 https 模块

当 https 模块被模拟后,https.get 将不再是其原始实现。我们可以在每个测试用例中定义其模拟行为。

ModelScope
ModelScope

魔搭开源模型社区旨在打造下一代开源的模型即服务共享平台

下载

3.3 编写测试用例

我们将针对不同的场景编写测试用例。

场景一:成功获取JSON响应 (200 OK)

在这个场景中,我们模拟 https.get 返回一个成功的HTTP响应,其中包含有效的JSON数据。

describe('crud.get', () => {
  let mockCallback;
  let mockErrorCallback;

  beforeEach(() => {
    // 在每个测试用例前重置模拟函数
    mockCallback = jest.fn();
    mockErrorCallback = jest.fn();
    // 清除 https.get 的所有模拟,确保每个测试用例都是独立的
    https.get.mockClear();
  });

  it('should call callback with parsed JSON data on successful 2xx response', (done) => {
    const mockUrl = 'https://example.com/api/data';
    const mockResponseData = { id: 1, name: 'Test Data' };
    const mockRawData = JSON.stringify(mockResponseData);

    // 模拟 response 对象,包括 statusCode 和 on 方法
    const mockResponse = {
      statusCode: 200,
      on: jest.fn((event, handler) => {
        if (event === 'data') {
          handler(mockRawData); // 模拟数据块
        } else if (event === 'end') {
          handler(); // 模拟响应结束
        }
      }),
    };

    // 模拟 https.get 方法
    https.get.mockImplementation((options, responseCallback) => {
      responseCallback(mockResponse); // 立即调用响应回调
      return {
        on: jest.fn(), // 模拟 .on('error'),避免未定义错误
      };
    });

    crud.get(null, mockUrl, mockCallback, mockErrorCallback);

    // 使用 setTimeout 或 process.nextTick 确保异步回调被执行
    // 或者在 Jest 11+ 中使用 done 回调
    process.nextTick(() => {
      expect(https.get).toHaveBeenCalledTimes(1);
      expect(https.get).toHaveBeenCalledWith(mockUrl, expect.any(Function)); // 检查URL和回调
      expect(mockResponse.on).toHaveBeenCalledWith('data', expect.any(Function));
      expect(mockResponse.on).toHaveBeenCalledWith('end', expect.any(Function));
      expect(mockCallback).toHaveBeenCalledTimes(1);
      expect(mockCallback).toHaveBeenCalledWith(mockResponseData);
      expect(mockErrorCallback).not.toHaveBeenCalled();
      done(); // 标记异步测试完成
    });
  });

  // 场景一变种:成功获取空JSON响应 (例如 {})
  it('should call callback with empty object if response is empty JSON', (done) => {
    const mockUrl = 'https://example.com/api/empty';
    const mockRawData = '{}';

    const mockResponse = {
      statusCode: 200,
      on: jest.fn((event, handler) => {
        if (event === 'data') { handler(mockRawData); }
        else if (event === 'end') { handler(); }
      }),
    };

    https.get.mockImplementation((options, responseCallback) => {
      responseCallback(mockResponse);
      return { on: jest.fn() };
    });

    crud.get(null, mockUrl, mockCallback, mockErrorCallback);

    process.nextTick(() => {
      expect(mockCallback).toHaveBeenCalledWith({});
      done();
    });
  });

  // 场景一变种:成功获取非JSON响应
  it('should call callback with empty object if response is non-JSON', (done) => {
    const mockUrl = 'https://example.com/api/text';
    const mockRawData = 'This is plain text.';

    const mockResponse = {
      statusCode: 200,
      on: jest.fn((event, handler) => {
        if (event === 'data') { handler(mockRawData); }
        else if (event === 'end') { handler(); }
      }),
    };

    https.get.mockImplementation((options, responseCallback) => {
      responseCallback(mockResponse);
      return { on: jest.fn() };
    });

    crud.get(null, mockUrl, mockCallback, mockErrorCallback);

    process.nextTick(() => {
      expect(mockCallback).toHaveBeenCalledWith({}); // checkJSONResponse 会返回空对象
      done();
    });
  });
});

代码解析:

  • beforeEach: 在每个测试运行前初始化 mockCallback 和 mockErrorCallback 为 jest.fn(),并清除 https.get 的模拟历史,确保测试的独立性。
  • mockResponse: 创建一个模拟的HTTP响应对象,它具有 statusCode 属性和 on 方法。on 方法被模拟为在收到 data 事件时调用处理程序并传递模拟数据,在收到 end 事件时调用处理程序。
  • https.get.mockImplementation(): 这是核心模拟逻辑。当 crud.get 调用 https.get 时,Jest会调用我们提供的这个函数。我们在这里立即调用 responseCallback 并传入 mockResponse,模拟服务器响应。同时,返回一个带有 on 方法的对象,以处理 https.get().on('error') 调用链。
  • process.nextTick(() => { ... }) 或 done(): 由于 handleResponse 中的 response.on('data') 和 response.on('end') 是异步的,即使我们同步调用 responseCallback,其内部的事件处理仍然会在下一个事件循环周期中执行。process.nextTick 或 setTimeout(..., 0) 可以确保我们等待这些异步操作完成再进行断言。使用 done() 回调是Jest推荐的异步测试方式。
场景二:处理非2xx状态码的错误响应

当HTTP请求返回非2xx状态码(如404 Not Found, 500 Internal Server Error)时,errorCallback 应该被调用。

describe('crud.get', () => {
  let mockCallback;
  let mockErrorCallback;

  beforeEach(() => {
    mockCallback = jest.fn();
    mockErrorCallback = jest.fn();
    https.get.mockClear();
  });

  it('should call errorCallback on non-2xx response status', (done) => {
    const mockUrl = 'https://example.com/api/notfound';
    const mockRawErrorData = 'Not Found';

    const mockResponse = {
      statusCode: 404,
      on: jest.fn((event, handler) => {
        if (event === 'data') { handler(mockRawErrorData); }
        else if (event === 'end') { handler(); }
      }),
    };

    https.get.mockImplementation((options, responseCallback) => {
      responseCallback(mockResponse);
      return { on: jest.fn() };
    });

    crud.get(null, mockUrl, mockCallback, mockErrorCallback);

    process.nextTick(() => {
      expect(mockCallback).not.toHaveBeenCalled();
      expect(mockErrorCallback).toHaveBeenCalledTimes(1);
      expect(mockErrorCallback).toHaveBeenCalledWith(mockRawErrorData);
      done();
    });
  });
});
场景三:处理网络请求错误

当 https.get 本身发生网络错误(例如DNS解析失败、连接超时)时,其返回的EventEmitter会触发 error 事件。

describe('crud.get', () => {
  let mockCallback;
  let mockErrorCallback;

  beforeEach(() => {
    mockCallback = jest.fn();
    mockErrorCallback = jest.fn();
    https.get.mockClear();
  });

  it('should call errorCallback when https.get emits an error', (done) => {
    const mockUrl = 'https://example.com/api/error';
    const mockError = new Error('Network error occurred');

    // 模拟 https.get 返回一个 EventEmitter,并立即触发 'error' 事件
    https.get.mockImplementation((options, responseCallback) => {
      // 返回一个模拟的 EventEmitter
      const reqEmitter = {
        on: jest.fn((event, handler) => {
          if (event === 'error') {
            // 在下一个事件循环中触发错误,模拟真实异步行为
            process.nextTick(() => handler(mockError));
          }
        }),
      };
      return reqEmitter;
    });

    crud.get(null, mockUrl, mockCallback, mockErrorCallback);

    // 等待异步错误处理完成
    process.nextTick(() => {
      expect(mockCallback).not.toHaveBeenCalled();
      expect(mockErrorCallback).toHaveBeenCalledTimes(1);
      expect(mockErrorCallback).toHaveBeenCalledWith(mockError);
      done();
    });
  });
});

重要注意事项:

  • 单元测试 vs. 集成测试: 上述测试是典型的单元测试,它们隔离了 crud.get 函数与外部网络依赖。原始问题中提供的测试用例直接调用 module.exports.get 并期望真实的外部URL响应,这更接近于集成测试。集成测试有其价值,但单元测试更适合快速反馈和定位代码逻辑问题。
  • 异步测试: Jest提供了多种异步测试方法,如 done() 回调、返回Promise、async/await。对于回调风格的代码,使用 done() 或 process.nextTick 结合 done() 是常见的做法。
  • 细致的模拟: 模拟外部模块时,需要尽可能地模拟其真实行为,包括返回的对象、事件的触发顺序等。例如,https.get 返回一个请求对象,该对象也有 on('error') 方法。

4. 总结

通过本文,我们学习了如何使用Jest框架为Node.js中封装的REST GET请求函数编写全面的单元测试。关键步骤包括:

  1. 理解函数逻辑: 明确函数的输入、输出、异步行为和依赖。
  2. 模拟外部依赖: 使用 jest.mock() 和 mockImplementation() 精确模拟 https 模块的行为,避免真实网络请求。
  3. 模拟响应流: 特别注意模拟 response.on('data') 和 response.on('end') 来模拟HTTP响应数据的接收过程。
  4. 处理异步回调: 利用 done() 或 process.nextTick 确保在异步操作完成后再进行断言。
  5. 覆盖多种场景: 编写测试用例覆盖成功响应、不同状态码的错误响应以及网络错误等多种情况。

掌握这些技术,将有助于你编写出更健壮、可维护且易于测试的Node.js异步代码。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

544

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

372

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

728

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

470

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

393

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

990

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

655

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

545

2023.09.20

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

150

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
10分钟--Midjourney创作自己的漫画
10分钟--Midjourney创作自己的漫画

共1课时 | 0.1万人学习

Midjourney 关键词系列整合
Midjourney 关键词系列整合

共13课时 | 0.9万人学习

AI绘画教程
AI绘画教程

共2课时 | 0.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号