Как выполнить модульное тестирование модуля Node.js, который требует других модулей, и как смоделировать глобальную функцию require?

156

Это тривиальный пример, который иллюстрирует суть моей проблемы:

var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.doComplexStuff();
}

module.exports = underTest;

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

Мэтью Тейлор
источник
Вы должны перезаписать global.require. Переменные записываются moduleпо умолчанию, так как модули находятся в области видимости модулей.
Рэйнос
@Raynos Как бы я это сделал? global.require не определено? Даже если бы я заменил его своей собственной функцией, другие функции никогда бы не использовали это, не так ли?
HMR

Ответы:

175

Ты можешь сейчас!

Я опубликовал proxyquire, который позаботится о переопределении глобальных требований внутри вашего модуля, пока вы его тестируете.

Это означает, что вам не нужно никаких изменений в вашем коде , чтобы внедрить макеты для необходимых модулей.

Proxyquire имеет очень простой API, который позволяет определить модуль, который вы пытаетесь протестировать, и передать макеты / заглушки для необходимых модулей за один простой шаг.

@Raynos прав, что традиционно вам приходилось прибегать к не очень идеальным решениям, чтобы достичь этого, или вместо этого заниматься разработкой снизу вверх.

Который является главной причиной, по которой я создал proxyquire - чтобы позволить разработку сверху вниз без каких-либо хлопот.

Посмотрите документацию и примеры, чтобы оценить, соответствует ли она вашим потребностям.

Торстен Лоренц
источник
5
Я пользуюсь proxyquire и не могу сказать достаточно хороших вещей. Это спасло меня! Мне было поручено написать тесты с жасминовыми узлами для приложения, разработанного в appcelerator Titanium, который заставляет некоторые модули иметь абсолютные пути и множество циклических зависимостей. proxyquire позволил мне прекратить пропустить тех и высмеивать ненужную мне информацию для каждого теста. (Объяснено здесь ). Большое спасибо!
Sukima
Рад слышать, что proxyquire помог вам правильно протестировать ваш код :)
Торстен Лоренц
1
очень мило @ThorstenLorenz, я определюсь. использовать proxyquire!
Беваква
Фантастика! Когда я увидел принятый ответ, что «ты не можешь», я подумал: «О Боже, серьезно ?!» но это действительно спасло его.
Чедвик
3
Для тех из вас, кто использует Webpack, не тратьте время на изучение proxyquire. Он не поддерживает Webpack. Я смотрю в Inject-загрузчик вместо ( github.com/plasticine/inject-loader ).
Artif3x
116

Лучшим вариантом в этом случае является макет методов возвращаемого модуля.

Что бы там ни было, большинство модулей 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 возвращает только функцию.

Другой вариант - использовать шутки. Следите за их страницей

Эллиот Фостер
источник
1
К сожалению, модули node.js НЕ гарантированно будут одиночными, как объяснено здесь: justjs.com/posts/…
FrontierPsycho
4
@FrontierPsycho несколько вещей: во-первых, что касается тестирования, статья не имеет значения. Пока вы тестируете свои зависимости (а не зависимости зависимостей), весь ваш код будет возвращать один и тот же объект, когда вы require('some_module'), потому что весь ваш код использует один и тот же каталог node_modules. Во-вторых, статья объединяет пространство имен с синглетонами, что является своего рода ортогональным. В-третьих, эта статья чертовски старая (с точки зрения node.js), так что то, что могло быть верным в тот день, возможно, сейчас недействительно.
Эллиот Фостер
2
Гектометр Если один из нас на самом деле не выкопает код, подтверждающий ту или иную точку, я бы пошел с вашим решением внедрения зависимости или просто просто передавал объекты, это более безопасно и более перспективно для будущего.
FrontierPsycho
1
Я не уверен, что вы просите доказать. Синглтонная (кэшированная) природа узловых модулей общеизвестна. Внедрение зависимостей, хотя и является хорошим маршрутом, может изрядно увеличить количество котлов и больше кода. DI чаще встречается в статически типизированных языках, где сложнее динамически вставлять шпионы / заглушки / насмешки в ваш код. Несколько проектов, которые я делал за последние три года, используют метод, описанный в моем ответе выше. Это самый простой из всех методов, хотя я использую его экономно.
Эллиот Фостер
1
Я предлагаю вам прочитать на sinon.js. Если вы используете Sinon (как в примере выше) , вы бы либо innerLib.toCrazyCrap.restore()и restub, или позвоните Sinon через sinon.stub(innerLib, 'toCrazyCrap')который позволяет изменять как тупиковые ведет себя: innerLib.toCrazyCrap.returns(false). Кроме того, rewire, кажется, очень похож на proxyquireрасширение выше.
Эллиот Фостер
11

Я использую макет-требуют . Убедитесь, что вы определили свои макеты перед requireтем, как тестировать модуль.

Кунал
источник
Также хорошо сделать сразу же stop (<file>) или stopAll (), чтобы вы не получили кэшированный файл в тесте, где вы не хотите использовать макет.
Джастин Крузе
1
Это очень помогло.
Уоллоп
2

Насмешка 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метод для проверки вашего кода. Код становится более подробным, но также и более простым для тестирования.

AlexM
источник
1
Я не думаю, что это глупо, как вы предполагаете ... в этом суть насмешки. Пересмешивание требуемых зависимостей делает вещи настолько простыми, что дает контроль разработчику, не меняя структуру кода. Ваши методы слишком многословны и поэтому трудно рассуждать. Я выбираю proxyrequire или mock-require для этого; Я не вижу здесь никаких проблем. Код чистый и легко рассуждать и помнить, что большинство людей, которые читают это, уже написали код, который вы хотите, чтобы они усложняли. Если эти библиотеки взломаны, то, по вашему определению, издевательства и окурки также должны быть прекращены.
Эммануэль Махуни
1
Проблема с подходом №1 заключается в том, что вы передаете детали внутренней реализации в стек. С несколькими слоями становится намного сложнее быть потребителем вашего модуля. Он может работать с подходом, подобным контейнеру IOC, так что зависимости автоматически вводятся для вас, однако, похоже, что у нас уже есть зависимости, введенные в модули узлов с помощью оператора import, тогда имеет смысл иметь возможность их высмеивать на этом уровне. ,
Магрит
1) Это просто перемещает проблему в другой файл. 2) Все еще загружает другой модуль и, таким образом, снижает производительность и, возможно, вызывает побочные эффекты (например, популярный colorsмодуль, который String.prototype
портится
2

Если вы когда-либо использовали шутку, то вы, вероятно, знакомы с шутливой функцией шутки.

Используя "jest.mock (...)", вы можете просто указать строку, которая будет встречаться в операторе require где-нибудь в вашем коде, и всякий раз, когда требуется модуль, использующий эту строку, вместо этого будет возвращен mock-объект.

Например

jest.mock("firebase-admin", () => {
    const a = require("mocked-version-of-firebase-admin");
    a.someAdditionalMockedMethod = () => {}
    return a;
})

полностью заменит все операции импорта / требования «firebase-admin» на объект, который вы вернули из этой «фабричной» функции.

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

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

allesklarbeidir
источник
1

Ты не можешь Вы должны создать свой набор модульных тестов так, чтобы сначала тестировались самые младшие модули, а потом - модули более высокого уровня, для которых требуются модули.

Вы также должны предположить, что любой сторонний код и сам 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, и любой код может получить доступ к модульной области на свою собственную опасность.

Raynos
источник
2
Предполагая, что сторонний код хорошо протестирован, это не лучший способ работы IMO.
henry.oswald
5
@ бек, это отличный способ работать. Это заставляет вас работать только с высококачественным сторонним кодом или писать все фрагменты кода, чтобы каждая зависимость была хорошо протестирована
Raynos
Хорошо, я думал, что вы имели в виду не проводить интеграционные тесты между вашим кодом и кодом стороннего производителя. Согласовано.
henry.oswald
1
«Набор модульных тестов» - это просто набор модульных тестов, но модульные тесты должны быть независимы друг от друга, следовательно, модуль в модульном тесте. Чтобы их можно было использовать, модульные тесты должны быть быстрыми и независимыми, чтобы вы могли четко видеть, где код нарушается при сбое модульного теста.
Андреас Берхайм Брудин
Это не сработало для меня. Объект модуля не предоставляет «var innerLib ...» и т. Д.
AnitKryst
1

Вы можете использовать издевательскую библиотеку:

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'
Hirurg103
источник
1

Простой код для макетов модулей для любопытных

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

Джейсон Себринг
источник