Stubbing method in same file using Sinon

2019-01-20 12:33发布

问题:

I'm trying to unit test a function in a file while stubbing another function in the SAME file, but the mock is not being applied and the real method is being called. Here's an example:

// file: 'foo.js'

export function a() {
   // .....
}

export function b() { 
   let stuff = a(); // call a
   // ...do stuff
}

And my test:

import * as actions from 'foo';

const aStub = sinon.stub(actions, 'a').returns('mocked return');
actions.b(); // b() is executed, which calls a() instead of the expected aStub()

回答1:

Some restructuring can make this work.

I've used commonJS syntax. Should work in the same way in ES6 as well.

foo.js

const factory = {
  a,
  b,
}
function a() {
  return 2;
}

function b() {
  return factory.a();
}

module.exports = factory;

test.js

const ser = require('./foo');
const sinon = require('sinon');

const aStub = sinon.stub(ser, 'a').returns('mocked return');
console.log(ser.b());
console.log(aStub.callCount);

Output

mocked return

1



回答2:

While the above does work, it's definitely a workaround as my linter was quick to inform.

I ended up separating modules and using proxyquire. This library allows you to easily substitute any / all exports with those of your choosing, sinon stub spy or mocks included. e.g. :

in b.js

export const fnB = () => 'hey there!';

in a.js

import { fbB } from 'b.js';
export const fnA = () => fbB();

in a.test.js

import { noCallThru } from 'proxyquire';
const proxyquireStrict = noCallThru();
const stubB = stub().returns('forced result');
const moduleA = proxyquireStrict('a.js', {
    'b.js' : { fnB: stubB }
}).fnA; 

console.log(fnA()); // 'forced result'


回答3:

The method mentioned above (using a factory to collect the functions) works well; however, eslint will not like the use of a variable/function that has not yet been declared. Therefore I would recommend a slight modification:

// my-functions.js
export const factory = {};
export const funcA = () => {
  return facory.funcB();
};
factory.funcA = funcA;
export const funcB = () => true;
factory.funcB = funcB;

// my-functions-test.js
import {factory, funcA, funcB} from './path/to/my-functions';

describe('MyFunctions | funcA', () => {
  test('returns result from funcB call', () => {
    const funcBStub = sinon.stub(factory, 'funcB').returns(false);

    // Test that the function does not throw errors
    let result;
    expect(() => (result = funcA())).not.toThrow();

    // Test that the return value is that of the mock rather than the original function
    expect(result).toEqual(false);

    // Test that the stub was called
    expect(funcBStub.called).toEqual(true);
  });
});

// Don't forget to test funcB independently ;)

The important distinction is to add the functions within the file to the factory as they are defined to avoid break eslint rules. The only case where this could cause issues is if you tried calling one of those functions within the same file before they have all been defined. Example:

// my-functions-1.js
export const factory = {};

export const funcA = () => {
  factory.funcB();
};
factory.funcA = funcA;

// Since the code execution runs from top to bottom, calling funcA here means that funcB has not yet been added to factory
funcA(); // Throws an error since factory.funcB() is not a function (yet)

export const funcB = () => true;
factory.funcB = funcB;

I prefer this technique of using a "collector" to call functions within the same file since it is not always a great idea to create separate files for EACH function that you write. Often, I find that I will create many related utility functions in order to make my code more readable, reusable, and composable; putting each function into a separate file would make the code slightly more difficult to understand since a reader could not see the definitions of these functions without bouncing between different files.