SINON存根在同一文件中定义的函数

2022-06-17 00:00:00 unit-testing javascript sinon

我的代码大致如下:

// example.js
export function doSomething() {
  if (!testForConditionA()) {
    return;
  }

  performATask();
}

export function testForConditionA() {
  // tests for something and returns true/false
  // let's say this function hits a service or a database and can't be run in tests 
  ...
}

export function performATask() {
  ...
}


// example.test.js
import * as example from 'example';

it('validates performATask() runs when testForConditionA() is true', () => {
  const testForConditionAStub = sinon.stub(example, 'testForConditionA').returns(true);
  const performATaskSpy = sinon.stub(example, 'performATask');

  example.doSomething();
  expect(performATaskSpy.called).to.equal(true);
});

(我知道,这是一个做作的例子,但我尽量简短)

我还没有找到使用SINON模拟testForConditionA()的方法。

我知道有解决办法,比如

A)将Example.js中的所有内容放入一个类中,然后就可以将该类的函数存根。

B)将testForConditionA()(和其他依赖项)从Example.js移到新文件中,然后使用proxyquire

C)将依赖项注入DoSomething()

然而,这些选项都不可行--我使用的代码库很大,许多文件都需要重写和彻底修改。我已经搜索了这个主题,我看到了其他几篇文章,比如Stubbing method in same file using Sinon,但是除了将代码重构到一个单独的类(或一个人建议的工厂),或者重构到一个单独的文件并使用proxyquire,我还没有找到解决方案。我以前也用过其他的测试和模仿库,所以SINON不能做到这一点是令人惊讶的。还是真的是这样?对于如何在不重构正在测试的代码的情况下存根函数,有什么建议吗?


解决方案

a very related answer(我的)中的这一位,说明了为什么它并不令人惊讶:

ES模块默认情况下是不可变的,这意味着SINON无法执行Zilch。

ECMAScript规范规定了这一点,因此当前改变导出的唯一方法是让运行时不遵守规范。这实际上就是Jest所做的:它提供自己的运行时,将导入调用转换为等价的CJS调用(require)调用,并在该运行时中提供自己的require实现,该实现与加载过程挂钩。生成的";模块";通常具有可覆盖的可变导出(即存根)。

Jest也不支持本机(因为没有转换/修改源代码)ESM。跟踪问题4842和9430的复杂程度(需要更改节点)。

所以,不,SINON不能单独完成这项工作。它只是存根库。它不会触及运行库或执行任何魔术操作,因为它必须与环境无关地工作。


现在回到最初的问题:测试您的模块。我认为发生这种情况的唯一方式是通过某种依赖注入机制(在备选方案C中涉及到)。显然,您的模块依赖于某些(内部/外部)状态,因此这意味着您需要一种方法来从外部更改该状态或注入一个测试替身(您正在尝试的内容)。

一种简单的方法是创建一个严格用于测试的setter:

function callNetworkService(...args){
   // do something slow or brittle
}

let _doTestForConditionA = callNetworkService;

export function __setDoTestForConditionA(fn){
   _doTestForConditionA = fn;
}

export function __reset(){
   _doTestForConditionA = callNetworkService;
}

export function testForConditionA(...args) {
   return _doTestForConditionA(...args);
}

然后您可以像这样简单地测试模块:

afterEach(() => {
    example.__reset();
});

test('that my module calls the outside and return X', async () => {
    const fake = sinon.fake.resolves({result: 42});
    example.__setDoTestForConditionA(fake);
    const pendingPromise = example.doSomething();
    expect(fake.called).to.equal(true);

    expect((await pendingPromise).result).toEqual(42);
});

是的,您确实修改了您的SUT以允许测试,但我从来没有发现所有这些都是无礼的。该技术与框架(Jasmine、Mocha、Jest)或运行时(Browser、Node、JVM)无关,读起来也很好。

可选插入的依赖项

您确实提到了将依赖项实际依赖于它们注入到函数中,这有一些问题会传播到整个代码库。

我想通过展示我过去使用过的一种技术来挑战一下这一点。请看我对SINON问题跟踪器的评论:https://github.com/sinonjs/sinon/issues/831#issuecomment-198081263

我使用这个例子来说明如何在构造函数中注入存根,而这个构造函数的通常使用者都不需要关心它。当然,它要求您使用某种Object来不添加其他参数。

/**
 * Request proxy to intercept and cache outgoing http requests
 *
 * @param {Number} opts.maxAgeInSeconds how long a cached response should be valid before being refreshed
 * @param {Number} opts.maxStaleInSeconds how long we are willing to use a stale cache in case of failing service requests
 * @param {boolean} opts.useInMemCache default is false
 * @param {Object} opts.stubs for dependency injection in unit tests
 * @constructor
 */
function RequestCacher (opts) {
    opts = opts || {};
    this.maxAge = opts.maxAgeInSeconds || 60 * 60;
    this.maxStale = opts.maxStaleInSeconds || 0;
    this.useInMemCache = !!opts.useInMemCache;
    this.useBasicToken = !!opts.useBasicToken;
    this.useBearerToken = !!opts.useBearerToken;

    if (!opts.stubs) {
        opts.stubs = {};
    }

    this._redisCache = opts.stubs.redisCache || require('./redis-cache');
    this._externalRequest = opts.stubs.externalRequest || require('../request-helpers/external-request-handler');
    this._memCache = opts.stubs.memCache || SimpleMemCache.getSharedInstance();
}

(有关展开的评论,请参阅问题跟踪器)

没有任何内容强制任何人提供存根,但测试可以提供这些存根以覆盖依赖项的工作方式。

相关文章