Як перевірити модуль Node.js, який вимагає інших модулів, і як знущатися з функції глобальної потреби?


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 має рацію, що традиційно вам доводилося вдаватися до не дуже ідеальних рішень, щоб досягти цього або замість цього зробити розробку знизу вгору

Яка основна причина, чому я створив проксі-сервер - щоб дозволити розробку тестування зверху вниз без зайвих проблем.

Перегляньте документацію та приклади, щоб визначити, чи відповідає вона вашим потребам.


5
Я використовую проксі-запит і не можу сказати достатньо хороших речей. Це врятувало мене! Мені доручили написати тести на вузол жасмину для програми, розробленої в додатку Titanium, який змушує деякі модулі бути абсолютними шляхами та багатьма круговими залежностями. proxyquire, дозвольте мені зупинити пробіл і знущатися над суворими, які мені не потрібні були для кожного тесту. (Пояснено тут ). Велике спасибі!
Sukima

Раді почути, що проксіквайр допомогло вам перевірити свій код :)
Thorsten Lorenz

1
дуже приємно @ThorstenLorenz, я деф. використовуйте proxyquire!
bevacqua

Фантастичний! Коли я побачив прийняту відповідь, що "ти не можеш", я подумав "Боже, серйозно ?!" але це справді його врятувало.
Чадвік

3
Для тих, хто використовує Webpack, не витрачайте час на дослідження проксі-запиту. Він не підтримує Webpack. Я замість цього шукаю навантажувач для ін'єкцій ( github.com/plasticine/inject-loader ).
Artif3x

116

Кращим варіантом у цьому випадку є глузування з методів повернення модуля.

На краще чи гірше, більшість модулів node.js є одинаковими; два фрагменти коду, які вимагають () того самого модуля, отримують однакове посилання на цей модуль.

Ви можете скористатися цим і використовувати щось на зразок синона, щоб знущатися над необхідними предметами. мок- тест наступний:

// 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 для висловлювання тверджень, і я написав модуль для інтеграції синона з моккою, щоб полегшити очищення шпигуна / заглушки (щоб уникнути тестового забруднення.)

Зауважте, що під UnderTest не можна знущатися так само, як underTest повертає лише функцію.

Ще один варіант - використовувати макети Jest. Слідкувати на їхній сторінці


1
На жаль, модулі node.js НЕ гарантовано бути одинаковими, як пояснено тут: justjs.com/posts/…
FrontierPsycho

4
@FrontierPsycho кілька речей: По-перше, що стосується тестування, стаття не має значення. Поки ви тестуєте свої залежності (а не залежності), весь ваш код поверне той самий об'єкт, коли ви require('some_module'), оскільки весь ваш код має один і той же dode_modules dir. По-друге, стаття поєднує простір імен з одинаковими, що є своєрідним ортогональним. По-третє, ця стаття є досить проклятою (що стосується node.js), тому те, що могло бути дійсним ще в той день, можливо, зараз не дійсне.
Елліот Фостер

2
Гм. Якщо хтось із нас насправді не розкопає код, який підтверджує ту чи іншу точку, я б пішов із вирішенням проблеми введення залежності або просто просто передавав об'єкти навколо, це безпечніше і більше доказів у майбутньому.
FrontierPsycho

1
Я не впевнений, що ви просите довести. Однотонний (кешований) характер вузлових модулів зазвичай розуміється. Хоча хороший маршрут, введення залежності може бути значно більшим за плиту котла та більше коду. DI частіше зустрічається в мовах, що набираються статично, де складніше динамічно вибивати шпигунів / заглушок / макетів у свій код. У кількох проектах, які я робив за останні три роки, використовується метод, описаний у моїй відповіді вище. Це найпростіший з усіх методів, хоча я його використовую економно.
Елліот Фостер

1
Я пропоную вам прочитати на sinon.js. Якщо ви використовуєте Sinon (як в прикладі вище) , ви б або innerLib.toCrazyCrap.restore()й restub, або зателефонуйте Sinon через sinon.stub(innerLib, 'toCrazyCrap')який дозволяє змінювати як тупикові поводиться: innerLib.toCrazyCrap.returns(false). Крім того, схоже, що перемотування про кабель дуже збігається з proxyquireрозширенням вище.
Елліот Фостер

11

Я використовую макет-вимагати . Переконайтесь, що ви визначили свої макети перед requireтим, як перевірити модуль.


Також добре відразу зупинитись (<file>) або stopAll (), щоб ви не отримали кешований файл у тесті, де ви не хочете макет.
Джастін Крус

1
Це допомогло тоні.
wallop

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


1
Я не думаю, що це гакітно, як ви припускаєте ... це сама суть глузування. Знущання над необхідними залежностями робить речі настільки простими, що дає контроль розробнику без зміни структури коду. Ваші методи занадто багатослівні і тому важко їх міркувати. Я вибираю proxyrequire або макет-вимагати над цим; я не бачу тут жодної проблеми. Код чистий і простий в обґрунтуванні і запам'ятовування більшості людей, які читають це, вже написали код, який ви хочете, щоб вони ускладнилися. Якщо ці лайки хаккі, то знущання та заїдання також є хакерським за вашим визначенням, і їх слід припинити.
Еммануель Махуні

1
Проблема підходу №1 полягає в тому, що ви передаєте деталі внутрішньої реалізації в стек. З декількома шарами тоді споживачам вашого модуля стає набагато складніше. Він може працювати з підходом до IOC-контейнерів, хоча таким чином залежність автоматично вводиться для вас, однак це здається, оскільки у нас вже вбудовані залежності в модулі вузлів через заяву про імпорт, то є сенс мати можливість знущатися над ними на тому рівні .
magritte

1) Це просто переносить проблему на інший файл 2) все-таки завантажується інший модуль і, таким чином, накладає накладні витрати, і, можливо, спричиняє побічні ефекти (як, наприклад, популярний colorsмодуль, з яким String.prototype
псується

2

Якщо ви коли-небудь використовували жарт, то ви, напевно, знайомі з функцією макету джеу.

Використовуючи "jest.mock (...)", ви можете просто вказати рядок, який би виникав у операторі вимоги у вашому коді десь, і коли потрібен модуль, що використовує цей рядок, замість нього буде повернуто mock-об'єкт.

Наприклад

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

повністю замінить увесь імпорт / запит "firebase-admin" об'єктом, який ви повернули з цієї "фабричної" функції.

Ну, ви можете це зробити при використанні жарту, оскільки vice створює час виконання кожного модуля, який він працює, і вводить у модуль "підключену" версію вимагати, але ви не зможете це зробити без жарту.

Я намагався досягти цього за допомогою макет-вимоги, але для мене це не спрацювало для вкладених рівнів у моєму джерелі. Погляньте на наступну проблему на github: макет-вимагати не завжди викликається з Mocha .

Для вирішення цього питання я створив два npm-модулі, які можна використовувати для досягнення того, що ви хочете.

Вам потрібен один плагін-плагін і макет модуля.

У вашому .babelrc використовуйте плагін babel-plugin-mock-demand з наступними параметрами:

...
"plugins": [
        ["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
        ...
]
...

а у вашому тестовому файлі використовуйте модуль vicelike-mock так:

import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
            const firebase = new (require("firebase-mock").MockFirebaseSdk)();
            ...
            return firebase;
});
...

jestlike-mockМодуль ще дуже рудиментарний і не має багато документації , але там не багато коду або. Я вдячний за будь-які піари за більш повний набір функцій. Метою було б відтворити всю функцію "vice.mock".

Для того, щоб побачити, як реально реалізує, можна шукати код у пакеті "vice-runtime". Наприклад, див. Https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734 , тут вони генерують "автомобіль" модуля.

Сподіваюся, що це допомагає;)


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, і будь-який код може отримати доступ до модульної області на власну небезпеку.


2
Якщо припустити, що код третьої сторони добре перевірений, це не відмінний спосіб працювати з ІМО.
henry.oswald

5
@beck - це прекрасний спосіб роботи. Це змушує працювати лише з високоякісним стороннім кодом або писати всі фрагменти вашого коду, щоб кожна залежність була добре перевірена
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'

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 досить приголомшливий, і ви повинні використовувати це. Він зберігає ваші вимоги, що скасовуються, локалізовані лише для тестів, і я дуже рекомендую це.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.