首页 > web前端 > js教程 > 正文

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

霞舞
发布: 2025-07-13 19:42:38
原创
320人浏览过

深入理解与实践:使用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 将不再是其原始实现。我们可以在每个测试用例中定义其模拟行为。

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异步代码。

以上就是深入理解与实践:使用Jest测试Node.js REST GET请求封装函数的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

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