You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

测试子类时如何监视祖父类中导入的Node模块函数?

问题:无法监视祖父类中使用的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 05:24:45