Метод заглушки в том же файле с использованием Sinon

#javascript #unit-testing #reactjs #redux #sinon

#javascript #модульное тестирование #reactjs #redux #sinon

Вопрос:

Я пытаюсь выполнить модульное тестирование функции в файле, одновременно заглушая другую функцию в ТОМ ЖЕ файле, но макет не применяется, и вызывается реальный метод. Вот пример:

 // file: 'foo.js'

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

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

И мой тест:

 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. Как общее эмпирическое правило, вы не должны заглушать / издеваться над функцией в том же модуле, который вы тестируете. Если вы обнаружите, что вам нужно это сделать, это хороший признак того, что две функции на самом деле должны быть в разных модулях. Конечно, правила созданы для того, чтобы их нарушать, и есть ситуации, когда это может быть нормально, но в целом вам следует избегать.

2. @AndrewEisenberg В настоящее время я нахожусь в такой ситуации и вижу, что все еще немного сложно имитировать функцию в том же модуле, который я тестирую. Не могли бы вы пояснить, почему, как правило, я должен разделить две функции на отдельные модули?

Ответ №1:

Некоторая реструктуризация может заставить это работать.

Я использовал синтаксис CommonJS. Должен работать таким же образом и в ES6.

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);
  

Вывод

издевательский возврат

1

Комментарии:

1. не могли бы вы привести пример, когда функции a и b получают некоторый аргумент и используют синтаксис экспорта es6

2. Любое объяснение?

3. почему это работает?

4. @gaurav5430 что вы имеете в виду? Вы находите какие-либо проблемы?

5. нет, я имею в виду, почему эта реструктуризация работает, а исходный код — нет

Ответ №2:

Хотя вышеупомянутое работает, это определенно обходной путь, поскольку мой линтер быстро сообщил.

В итоге я разделил модули и использовал proxyquire. Эта библиотека позволяет вам легко заменять любые / все экспортные файлы на те, которые вы выбираете, включая sinon stub spy или mocks. например :

в b.js

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

в .js

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

в 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'
  

Комментарии:

1. Разве это не обходной путь? Что, если неправильно разделять модули?

Ответ №3:

Метод, упомянутый выше (с использованием a factory для сбора функций), работает хорошо; однако eslint не понравится использование переменной / функции, которая еще не была объявлена. Поэтому я бы рекомендовал небольшую модификацию:

 // 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 resu<
    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 ;)
  

Важным отличием является добавление функций в файле в том factory виде, в котором они определены, чтобы избежать нарушения правил eslint. Единственный случай, когда это может вызвать проблемы, — это если вы попытались вызвать одну из этих функций в том же файле до того, как все они были определены. Пример:

 // 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;
  

Я предпочитаю этот метод использования «сборщика» для вызова функций в одном файле, поскольку не всегда хорошая идея создавать отдельные файлы для КАЖДОЙ функции, которую вы пишете. Часто я обнаруживаю, что создаю множество связанных служебных функций, чтобы сделать мой код более читаемым, многоразовым и составным; помещение каждой функции в отдельный файл немного усложнило бы понимание кода, поскольку читатель не мог видеть определения этих функций, не переходя между разными файлами.

Ответ №4:

Я столкнулся с той же проблемой и нашел один метод. Вы можете изменить свой foo.js файл к этому:

 // file: 'foo.js'

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

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

Пожалуйста, обратитесь к https://codeburst.io/stub-dependencies-with-sinon-js-259ac12379b9 .