Как выполнить модульное тестирование модуля Node.js, который требует других модулей, и как смоделировать глобальную функцию require?
Это тривиальный пример, иллюстрирующий суть моей проблемы:
var innerLib = require('./path/to/innerLib');
function underTest() {
return innerLib.doComplexStuff();
}
module.exports = underTest;
Я пытаюсь написать unit тест для этого кода. Как можно смоделировать требование для innerLib
, не отключив полностью функцию require
?
Так что это я пытаюсь смоделировать глобальный require
и выяснить, что даже это не сработает:
var path = require('path'),
vm = require('vm'),
fs = require('fs'),
indexPath = path.join(__dirname, './underTest');
var globalRequire = require;
require = function(name) {
console.log('require: ' + name);
switch(name) {
case 'connect':
case indexPath:
return globalRequire(name);
break;
}
};
Проблема в том, что функция require
внутри файла underTest.js
фактически не была отключена. Он по-прежнему указывает на глобальную функцию require
. Поэтому кажется, что я могу только смоделировать функцию require
в том же файле, в котором я выполняю макетирование. Если я использую глобальный require
для включения чего-либо, даже после того, как я переопределил локальную копию, требуемые файлы все равно будут иметь глобальную ссылку require
.
Ответы
Ответ 1
Теперь вы можете!
Я опубликовал proxyquire, который позаботится о том, чтобы переопределить глобальное требование внутри вашего модуля во время его тестирования.
Это означает, что вам нужно никаких изменений в вашем коде, чтобы вводить макеты для требуемых модулей.
Proxyquire имеет очень простой api, который позволяет разрешить модуль, который вы пытаетесь протестировать, и пропустить с помощью mocks/stubs необходимые модули за один простой шаг.
@Raynos прав, что традиционно вам приходилось прибегать к не очень идеальным решениям, чтобы достичь этого или сделать развитие снизу вверх
Какова основная причина, по которой я создал proxyquire, - чтобы без проблем справляться с разработкой с нисходящим тестом.
Взгляните на документацию и примеры, чтобы оценить, насколько она будет соответствовать вашим потребностям.
Ответ 2
В этом случае лучшим вариантом будет макетирование методов возвращаемого модуля.
Что бы там ни было, большинство модулей node.js являются синглетонами; две части кода, для которых требуется() один и тот же модуль, получают одну и ту же ссылку на этот модуль.
Вы можете использовать это и использовать что-то вроде sinon, чтобы макетировать предметы, которые необходимы. мокко следует тест:
// in your testfile
var innerLib = require('./path/to/innerLib');
var underTest = require('./path/to/underTest');
var sinon = require('sinon');
describe("underTest", function() {
it("does something", function() {
sinon.stub(innerLib, 'toCrazyCrap').callsFake(function() {
// whatever you would like innerLib.toCrazyCrap to do under test
});
underTest();
sinon.assert.calledOnce(innerLib.toCrazyCrap); // sinon assertion
innerLib.toCrazyCrap.restore(); // restore original functionality
});
});
Sinon имеет хорошую интеграцию с chai для создания утверждений, и я написал модуль для интеграции sinon с mocha, чтобы упростить очистку шпиона/заглушки (чтобы избежать загрязнения тестом.)
Обратите внимание, что underTest нельзя смоделировать таким же образом, поскольку underTest возвращает только функцию.
Другой вариант - использовать шутки Jest. Следите за их страницей
Ответ 3
Я использую макет-требуют. Убедитесь, что вы определили свои макеты, прежде чем require
проверки модуля.
Ответ 4
Издевательская require
чувствует себя как неприятный хак для меня. Я лично попытался бы избежать этого и реорганизовать код, чтобы сделать его более проверяемым.
Существуют различные подходы к обработке зависимостей.
1) передать зависимости в качестве аргументов
function underTest(innerLib) {
return innerLib.doComplexStuff();
}
Это сделает код универсальным. Недостатком является то, что вам нужно передавать зависимости вокруг, что может сделать код более сложным.
2) реализовать модуль как класс, затем использовать методы/свойства класса для получения зависимостей
(Это надуманный пример, когда использование класса не является разумным, но оно передает идею)
(Пример ES6)
const innerLib = require('./path/to/innerLib')
class underTestClass {
getInnerLib () {
return innerLib
}
underTestMethod () {
return this.getInnerLib().doComplexStuff()
}
}
Теперь вы можете легко заглушить метод getInnerLib
, чтобы проверить свой код.
Код становится более подробным, но также легче тестировать.
Ответ 5
Вы не можете. Вы должны создать свой unit test набор, чтобы сначала тестировать самые низкие модули, а затем тестировать модули более высокого уровня, требующие модулей.
Вы также должны предположить, что любой сторонний код и сам node.js хорошо протестированы.
Я предполагаю, что вы увидите, что в ближайшем будущем насмехаются фреймворки, которые перезаписывают global.require
Если вы действительно должны ввести макет, вы можете изменить свой код, чтобы выставить модульную область.
// underTest.js
var innerLib = require('./path/to/innerLib');
function underTest() {
return innerLib.toCrazyCrap();
}
module.exports = underTest;
module.exports.__module = module;
// test.js
function test() {
var underTest = require("underTest");
underTest.__module.innerLib = {
toCrazyCrap: function() { return true; }
};
assert.ok(underTest());
}
Будьте предупреждены, что предоставляет .__module
в ваш API, и любой код может получить доступ к модульной области при своей собственной опасности.
Ответ 6
Вы можете использовать mockery:
describe 'UnderTest', ->
before ->
mockery.enable( warnOnUnregistered: false )
mockery.registerMock('./path/to/innerLib', { doComplexStuff: -> 'Complex result' })
@underTest = require('./path/to/underTest')
it 'should compute complex value', ->
expect(@underTest()).to.eq 'Complex result'
Ответ 7
Простой код для макетов модулей для любопытных
Обратите внимание на части, где вы манипулируете require.cache
и отметьте метод require.resolve
как это секретный соус.
class MockModules {
constructor() {
this._resolvedPaths = {}
}
add({ path, mock }) {
const resolvedPath = require.resolve(path)
this._resolvedPaths[resolvedPath] = true
require.cache[resolvedPath] = {
id: resolvedPath,
file: resolvedPath,
loaded: true,
exports: mock
}
}
clear(path) {
const resolvedPath = require.resolve(path)
delete this._resolvedPaths[resolvedPath]
delete require.cache[resolvedPath]
}
clearAll() {
Object.keys(this._resolvedPaths).forEach(resolvedPath =>
delete require.cache[resolvedPath]
)
this._resolvedPaths = {}
}
}
Используйте как:
describe('#someModuleUsingTheThing', () => {
const mockModules = new MockModules()
beforeAll(() => {
mockModules.add({
// use the same require path as you normally would
path: '../theThing',
// mock return an object with "theThingMethod"
mock: {
theThingMethod: () => true
}
})
})
afterAll(() => {
mockModules.clearAll()
})
it('should do the thing', async () => {
const someModuleUsingTheThing = require('./someModuleUsingTheThing')
expect(someModuleUsingTheThing.theThingMethod()).to.equal(true)
})
})
НО... Proxyquire довольно круто, и вы должны использовать это. Он сохраняет ваши требования только для тестов, и я настоятельно рекомендую это сделать.
Ответ 8
Если вы когда-либо использовали шутку, то вы, вероятно, знакомы с функцией шутки.
Используя "jest.mock(...)", вы можете просто указать строку, которая будет встречаться в операторе require где-нибудь в вашем коде, и всякий раз, когда требуется модуль, использующий эту строку, вместо этого будет возвращен mock-объект.
Например
jest.mock("firebase-admin", () => {
const a = require("mocked-version-of-firebase-admin");
a.someAdditionalMockedMethod = () => {}
return a;
})
полностью заменит все операции импорта/требования "firebase-admin" на объект, который вы вернули с этой "фабрики" -function.
Что ж, вы можете сделать это при использовании jest, потому что jest создает среду выполнения для каждого модуля, который он запускает, и внедряет в модуль "зацепленную" версию require, но вы не сможете сделать это без jest.
Я пытался достичь этого с помощью mock-require, но для меня это не сработало для вложенных уровней в моем источнике. Посмотрите на следующую проблему на github: mock-require не всегда вызывается с Mocha.
Для решения этой проблемы я создал два npm-модуля, которые вы можете использовать для достижения желаемого.
Вам нужен один Babel-плагин и модуль пересмешника.
В вашем .babelrc используйте плагин babel-plugin-mock-require со следующими параметрами:
...
"plugins": [
["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
...
]
...
и в вашем тестовом файле используйте модуль jestlike-mock следующим образом:
import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
const firebase = new (require("firebase-mock").MockFirebaseSdk)();
...
return firebase;
});
...
Модуль jestlike-mock
все еще очень элементарен и не имеет много документации, но также не так много кода. Я ценю любые PR для более полного набора функций. Цель состоит в том, чтобы воссоздать всю функцию "jest.mock".
Чтобы увидеть, как jest реализует это, можно посмотреть код в пакете "jest-runtime". См., Например, https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734, здесь они генерируют "автоматическую блокировку" модуля.
Надеюсь, это поможет ;)