在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。
对于上述异步操作和外部依赖(如 https 模块)的代码,编写单元测试至关重要。
主要挑战在于:
Jest是一个流行的JavaScript测试框架,它提供了强大的断言库、模拟(mocking)功能和异步测试支持。
首先,确保你的项目中安装了Jest:
npm install --save-dev jest
在你的测试文件中(例如 crud.test.js),你需要引入待测试的模块和 https 模块(以便进行模拟)。
// crud.test.js const crud = require('./crud'); // 假设你的封装函数在 crud.js 中 const https = require('https'); // 引入 https 模块,用于模拟
这是单元测试的关键一步。我们不希望测试真正发起网络请求,因此需要模拟 https.get 方法。Jest提供了 jest.mock() 和 mockImplementation() 来实现这一点。
// 在测试文件的顶部 jest.mock('https'); // 模拟整个 https 模块
当 https 模块被模拟后,https.get 将不再是其原始实现。我们可以在每个测试用例中定义其模拟行为。
我们将针对不同的场景编写测试用例。
在这个场景中,我们模拟 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(); }); }); });
代码解析:
当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(); }); }); });
重要注意事项:
通过本文,我们学习了如何使用Jest框架为Node.js中封装的REST GET请求函数编写全面的单元测试。关键步骤包括:
掌握这些技术,将有助于你编写出更健壮、可维护且易于测试的Node.js异步代码。
以上就是深入理解与实践:使用Jest测试Node.js REST GET请求封装函数的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号