Node.js可继承类的正确导出及测试桩化问题解决
兼顾类继承与测试桩化的解决方案
这个问题我之前也碰到过——核心矛盾就是基类必须导出类本身才能支持继承,但测试时想桩化它的方法却因为实例独立性屡屡失败。下面给你几个实用的方案,按推荐程度排序:
方案1:依赖注入(最推荐,符合SOLID设计原则)
把数据库操作抽成独立的依赖模块,注入到基类中。这样测试时只需替换这个依赖,不用直接桩化类方法,彻底解耦业务逻辑和数据访问。
步骤1:抽离数据库操作
新建一个数据库服务模块:
// db.service.js class DbService { getData(request) { // 原有的数据库查询逻辑 return errorRequest; } } module.exports = new DbService();
步骤2:修改基类支持依赖注入
const dbService = require('./db.service'); class BaseReport { // 新增可选参数,默认用全局的dbService,测试时可传入mock constructor(reportName, db = dbService) { this.reportName = reportName; this.db = db; } async generateReport(accountId, request) { // 改用注入的db服务获取数据 let results = await this.db.getData(request); results = this.formatResults(results); return results; // 修复你原代码里的笔误:updatedResults → results } formatResults(data) { // 原有逻辑 return data; } } module.exports = BaseReport;
步骤3:测试时注入Mock依赖
以Jest为例,测试代码可以这样写:
const BaseReport = require('./base.service'); const DataReport = require('./data.service'); // 定义mock的数据库服务 const mockDbService = { getData: jest.fn().mockResolvedValue({ mock: 'data' }) }; test('BaseReport使用mock数据库', async () => { const report = new BaseReport('test-report', mockDbService); await report.generateReport('123', {}); expect(mockDbService.getData).toHaveBeenCalled(); }); test('DataReport使用mock数据库', async () => { // 给已导出的DataReport实例替换db依赖 DataReport.db = mockDbService; await DataReport.generateReport('123', {}); expect(mockDbService.getData).toHaveBeenCalled(); });
这个方案的优势是代码解耦性强,不仅解决测试问题,还让后续替换数据库实现、扩展功能变得更简单。
方案2:直接桩化基类的原型方法
如果不想修改业务代码,可以直接桩化BaseReport.prototype上的方法——因为所有子类实例都会共享基类原型上的方法,桩化后所有实例都会使用桩化后的逻辑。
以Jest为例的测试代码:
const BaseReport = require('./base.service'); const DataReport = require('./data.service'); test('桩化BaseReport的getDataFromDb方法', async () => { // 桩化原型上的方法,并记录调用 const mockGetData = jest.spyOn(BaseReport.prototype, 'getDataFromDb') .mockResolvedValue({ mock: 'data' }); await DataReport.generateReport('123', {}); expect(mockGetData).toHaveBeenCalled(); // 测试后恢复原方法,避免影响其他测试用例 mockGetData.mockRestore(); });
这个方案的优点是零业务代码修改,适合快速解决测试问题;缺点是耦合度高,桩化会影响所有基于BaseReport的实例,必须记得在测试后恢复原方法。
方案3:子类导出类而非实例
调整子类的导出方式,从导出实例改成导出类。这样测试时可以自己创建实例,灵活桩化方法,同时保留子类的可继承性。
修改DataReport的导出
const BaseReport = require("./base.service"); class DataReport extends BaseReport { constructor() { super('dataReport'); } formatResults(data) { // 自定义格式化逻辑 return data; } } module.exports = DataReport; // 导出类,不是实例
应用中使用子类的方式调整
在应用代码里创建实例并导出:
// data.report.instance.js const DataReport = require('./data.service'); module.exports = new DataReport();
测试时的用法
const DataReport = require('./data.service'); test('测试DataReport的桩化方法', async () => { const reportInstance = new DataReport(); // 桩化当前实例的getDataFromDb方法 const mockGetData = jest.spyOn(reportInstance, 'getDataFromDb') .mockResolvedValue({ mock: 'data' }); await reportInstance.generateReport('123', {}); expect(mockGetData).toHaveBeenCalled(); });
这个方案保留了类的可继承性,测试灵活性高,但需要调整应用中使用子类的代码结构。
内容的提问来源于stack exchange,提问作者DoArNa
相关产品推荐
相关产品推荐

