Как выполнить модульное тестирование модуля 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, здесь они генерируют "автоматическую блокировку" модуля.

Надеюсь, это поможет ;)