测试子类时如何监视祖父类中导入的Node模块函数?
我最近在测试子类时遇到了一个棘手的问题:我需要监视一个从Node模块导入、且在祖父类中通过getter暴露的函数,想验证它的调用参数,但不管用Jasmine内置的spyOn还是Sinon的spy都无法触发。代码本身运行完全正常,就是测试断言始终失败。
原代码结构
ChildClass.js
const ParentClass = require('./ParentClass'); module.exports = class ChildClass extends ParentClass { constructor () { super(); this.foo(); } foo () { this.message = this.importGetter('Some input'); } };
ParentClass.js
const GrandparentClass = require('./GrandparentClass'); module.exports = class ParentClass extends GrandparentClass { constructor () { super(); this._message = null; } get message () { return this._message; } set message (value) { this._message = value; } };
GrandparentClass.js
const nodeModuleFunction = require('./nodeModule').nodeModuleFunction; module.exports = class GrandparentClass { get importGetter () { return nodeModuleFunction; } };
nodeModule.js
async function nodeModuleFunction (input) { console.error('Do something with input: ', input); return new Promise((resolve, reject) => { setTimeout(() => { resolve('Returned from node module.'); }, 300); }); } exports.nodeModuleFunction = nodeModuleFunction;
初始测试代码(失败)
test.spec.js
const ChildClass = require('./ChildClass'); const nodeModule = require('./nodeModule'); const sinon = require('sinon'); describe('ChildClass test', () => { describe('Jasmine spy', () => { it('should call nodeModule.nodeModuleFunction with given value', done => { spyOn(nodeModule, 'nodeModuleFunction').and.callThrough(); const object = new ChildClass(); expect(object.message).not.toBeNull(); expect(nodeModule.nodeModuleFunction).toHaveBeenCalled(); expect(nodeModule.nodeModuleFunction).toHaveBeenCalledWith('Some input'); object.message.then(message => { expect(message).toBe('Returned from node module.'); done(); }); }); }); describe('Sinon spy', () => { it('should call nodeModule.nodeModuleFunction with given value', done => { const spy = sinon.spy(nodeModule, 'nodeModuleFunction'); const object = new ChildClass(); expect(object.message).not.toBeNull(); expect(spy.called).toBe(true); expect(spy.withArgs('Some input').calledOnce).toBe(true); object.message.then(message => { expect(message).toBe('Returned from node module.'); done(); }); }); }); });
测试失败输出
Jasmine started
Do something with input: Some input
ChildClass test
Jasmine spy
✗ should call nodeModule.nodeModuleFunction with given value
- Expected spy nodeModuleFunction to have been called.
- Expected spy nodeModuleFunction to have been called with [ 'Some input' ] but it was never called.
Do something with input: Some input
Sinon spy
✗ should call nodeModule.nodeModuleFunction with given value
- Expected false to be true.
- Expected false to be true.
问题原因
问题出在Node模块的缓存机制:当我们先导入ChildClass时,它会依次加载ParentClass和GrandparentClass,而GrandparentClass会立即导入并保存nodeModuleFunction的原始引用。之后我们再给nodeModule.nodeModuleFunction创建spy,已经无法改变GrandparentClass中保存的那个引用了——所以实际调用的还是原始函数,spy根本没被触发。
解决方案:调整导入顺序
按照Brian的建议,我修改了测试代码,先创建spy,再导入ChildClass,这样当GrandparentClass加载时,它拿到的就是已经被spy包装过的函数引用了。
修改后的Jasmine测试代码
const nodeModule = require('./nodeModule'); describe('ChildClass test', () => { let ChildClass; beforeAll(() => { spyOn(nodeModule, 'nodeModuleFunction').and.callThrough(); // 先创建spy ChildClass = require('./ChildClass'); // 再导入子类,触发依赖链加载 }); afterEach(() => { nodeModule.nodeModuleFunction.calls.reset(); // 每个测试后重置spy调用历史 }); describe('Jasmine spy', () => { it('should call nodeModule.nodeModuleFunction with given value', done => { const object = new ChildClass(); expect(object.message).not.toBeNull(); expect(nodeModule.nodeModuleFunction).toHaveBeenCalled(); expect(nodeModule.nodeModuleFunction).toHaveBeenCalledWith('Some input'); object.message.then(message => { expect(message).toBe('Returned from node module.'); done(); }); }); it('should still call nodeModule.nodeModuleFunction with given value', done => { const object = new ChildClass(); expect(object.message).not.toBeNull(); expect(nodeModule.nodeModuleFunction).toHaveBeenCalled(); expect(nodeModule.nodeModuleFunction).toHaveBeenCalledWith('Some input'); object.message.then(message => { expect(message).toBe('Returned from node module.'); done(); }); }); }); });
修改后的Sinon测试代码
const nodeModule = require('./nodeModule'); const sinon = require('sinon'); describe('ChildClass test', () => { let spy; let ChildClass; beforeAll(() => { spy = sinon.spy(nodeModule, 'nodeModuleFunction'); // 先创建spy ChildClass = require('./ChildClass'); // 再导入子类 }); afterEach(() => { spy.resetHistory(); // 重置调用历史 }); afterAll(() => { spy.restore(); // 测试全部结束后恢复原始函数 }); describe('Sinon spy', () => { it('should call nodeModule.nodeModuleFunction with given value', done => { const object = new ChildClass(); expect(object.message).not.toBeNull(); expect(spy.called).toBe(true); expect(spy.withArgs('Some input').calledOnce).toBe(true); object.message.then(message => { expect(message).toBe('Returned from node module.'); done(); }); }); it('should still call nodeModule.nodeModuleFunction with given value', done => { const object = new ChildClass(); expect(object.message).not.toBeNull(); expect(spy.called).toBe(true); expect(spy.withArgs('Some input').calledOnce).toBe(true); object.message.then(message => { expect(message).toBe('Returned from node module.'); done(); }); }); }); });
这样修改后,spy就能正确捕获到函数的调用了,所有测试断言都能通过。
内容的提问来源于stack exchange,提问作者Barry Crookes

