Як я можу висміяти залежності для тестування одиниць у RequireJS?


127

У мене є модуль AMD, який я хочу протестувати, але я хочу висміяти його залежності, а не завантажувати фактичні залежності. Я використовую Requjs, і код для мого модуля виглядає приблизно так:

define(['hurp', 'durp'], function(Hurp, Durp) {
  return {
    foo: function () {
      console.log(Hurp.beans)
    },
    bar: function () {
      console.log(Durp.beans)
    }
  }
}

Як я можу знущатись hurpі durpщоб я міг ефективно провести тест?


Я просто роблю деякі божевільні речі eval в node.js, щоб висміяти defineфункцію. Однак є кілька різних варіантів. Я опублікую відповідь, сподіваючись, що вона буде корисною.
джерґасон

1
Для тестування одиниць з Жасмином ви також можете швидко поглянути на Jasq . [Відмова: Я підтримую
лібу

1
Якщо ви тестуєте в node env, ви можете скористатися пакетом requ-mock . Це дозволяє легко знущатися над своїми залежностями, замінювати модулі тощо. Якщо вам потрібна веб-переглядач із завантаженням модуля асинхронізації, ви можете спробувати Squire.js
ValeriiVasin

Відповіді:


64

Отже, прочитавши цю публікацію, я придумав рішення, яке використовує функцію конфігурації Requjs, щоб створити новий контекст для вашого тесту, де ви можете просто знущатися над своїми залежностями:

var cnt = 0;
function createContext(stubs) {
  cnt++;
  var map = {};

  var i18n = stubs.i18n;
  stubs.i18n = {
    load: sinon.spy(function(name, req, onLoad) {
      onLoad(i18n);
    })
  };

  _.each(stubs, function(value, key) {
    var stubName = 'stub' + key + cnt;

    map[key] = stubName;

    define(stubName, function() {
      return value;
    });
  });

  return require.config({
    context: "context_" + cnt,
    map: {
      "*": map
    },
    baseUrl: 'js/cfe/app/'
  });
}

Таким чином, він створює новий контекст, де визначення Hurpта Durpбудуть задані об'єктами, які ви передали у функцію. Math.random для імені, можливо, трохи брудне, але воно працює. Тому що якщо у вас буде купа тесту, вам потрібно створити новий контекст для кожного пакету, щоб запобігти повторному використанню ваших макетів або завантаженню макетів, коли вам потрібен реальний модуль Requjs.

У вашому випадку це виглядатиме так:

(function () {

  var stubs =  {
    hurp: 'hurp',
    durp: 'durp'
  };
  var context = createContext(stubs);

  context(['yourModuleName'], function (yourModule) {

    //your normal jasmine test starts here

    describe("yourModuleName", function () {
      it('should log', function(){
         spyOn(console, 'log');
         yourModule.foo();

         expect(console.log).toHasBeenCalledWith('hurp');
      })
    });
  });
})();

Тому я деякий час використовую цей підхід у виробництві та його дійсно надійний.


1
Мені подобається те, що ти тут робиш ... Тим більше що ти можеш завантажити інший контекст для кожного тесту. Єдине, що я хотів би змінити, це те, що здається, що це працює лише в тому випадку, якщо я знущаюся з усіх залежностей. Чи знаєте ви про спосіб повернення макетних об'єктів, якщо вони є, але резервне копіювання до виходу з фактичного .js-файлу, якщо макет не надається? Я намагався прокопати необхідний код, щоб зрозуміти це, але я трохи втрачаюсь.
Глен Х'юз

5
Це лише знущається над залежністю, яку ти передаєш createContextфункції. Тож у вашому випадку, якщо ви перейдете лише {hurp: 'hurp'}до функції, durpфайл буде завантажений як звичайна залежність.
Andreas Köberle

1
Я використовую це в Rails (з jasminerice / phantomjs), і це було найкращим рішенням, яке я знайшов для глузування з RequireJS.
Бен Андерсон

13
+1 Не дуже, але з усіх можливих рішень це здається найменш некрасивим / безладним. Ця проблема заслуговує більшої уваги.
Кріс Зальцберг

1
Оновлення: всім, хто розглядає це рішення, я б радив перевірити squire.js ( github.com/iammerrick/Squire.js ), згаданий нижче. Це приємна реалізація рішення, схожого на це, створюючи нові контексти, де потрібні заглушки.
Кріс Зальцберг

44

ви можете перевірити нову версію Squire.js

з документів:

Squire.js - це інжектор залежностей для користувачів Require.js, щоб зробити глузування залежностей простим!


2
Настійно рекомендуємо! Я оновлюю свій код, щоб використовувати squire.js, і поки що мені це дуже подобається. Дуже простий код, не велика магія під капотом, але зроблена таким чином, що (порівняно) легко зрозуміти.
Кріс Зальцберг

1
У мене було багато проблем зі стороною сквайра, що проводив інші тести, і не можу його рекомендувати. Я б рекомендував npmjs.com/package/requirejs-mock
Джефф

17

Я знайшов три різні рішення цієї проблеми, жодне з них не приємне.

Визначення залежностей Inline

define('hurp', [], function () {
  return {
    beans: 'Beans'
  };
});

define('durp', [], function () {
  return {
    beans: 'durp beans'
  };
});

require('hurpdhurp', function () {
  // test hurpdurp in here
});

Фуглі. Вам доведеться захаращувати свої тести великою кількістю котлів AMD.

Завантаження макетних залежностей з різних контурів

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

Підробляють це у вузлі

Це моє сьогоднішнє рішення, але все ще страшне.

Ви створюєте свою власну defineфункцію, щоб надавати власні макети модулю і ставити свої тести на зворотний дзвінок. Тоді ви evalмодуль для запуску тестів, наприклад:

var fs = require('fs')
  , hurp = {
      beans: 'BEANS'
    }
  , durp = {
      beans: 'durp beans'
    }
  , hurpDurp = fs.readFileSync('path/to/hurpDurp', 'utf8');
  ;



function define(deps, cb) {
  var TestableHurpDurp = cb(hurp, durp);
  // now run tests below on TestableHurpDurp, which is using your
  // passed-in mocks as dependencies.
}

// evaluate the AMD module, running your mocked define function and your tests.
eval(hurpDurp);

Це моє бажане рішення. Це виглядає трохи магічно, але має кілька переваг.

  1. Запускайте свої тести у вузлі, щоб не возитися з автоматизацією браузера.
  2. Менша потреба в брудному котлі AMD у ваших тестах.
  3. Ви можете використовувати evalв гніві, і уявіть, що Крокфорд вибухає від люті.

Вочевидь, він все ще має деякі недоліки.

  1. Оскільки ви тестуєте у вузлі, ви нічого не можете зробити з подіями браузера або маніпуляцією з DOM. Тільки добре для тестування логіки.
  2. Ще трохи незграбно налаштувати. Вам потрібно знущатися над defineкожним тестом, оскільки саме там фактично працюють ваші тести.

Я працюю над тестовим бігуном, щоб дати приємніший синтаксис для подібних матеріалів, але в мене все ще немає гарного рішення проблеми 1.

Висновок

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


15

Є config.mapваріант http://requirejs.org/docs/api.html#config-map .

Як використовувати його:

  1. Визначити нормальний модуль;
  2. Визначити модуль заглушки;
  3. Налаштування RequireJS експліцитно;

    requirejs.config({
      map: {
        'source/js': {
          'foo': 'normalModule'
        },
        'source/test': {
          'foo': 'stubModule'
        }
      }
    });
    

У цьому випадку для нормального та тестового коду ви можете використовувати fooмодуль, який буде реальним посиланням на модуль та заглушкою відповідно.


Такий підхід для мене спрацював дуже добре. У моєму випадку я додав це до html сторінки тестового бігуна -> map: {'*': {'Загальні / Модулі / корисніModule': '/Tests/Specs/Common/usefulModuleMock.js'}}
Вирівняний

9

Ви можете використовувати testr.js для висміювання залежностей. Ви можете встановити тест для завантаження макетних залежностей замість оригінальних. Ось приклад використання:

var fakeDep = function(){
    this.getText = function(){
        return 'Fake Dependancy';
    };
};

var Module1 = testr('module1', {
    'dependancies/dependancy1':fakeDep
});

Перевірте це також: http://cyberasylum.janithw.com/mocking-requirejs-dependitions-for-unit-testing/


2
Я дуже хотів, щоб тест.js працював, але це ще не дуже відповідає завданням. Врешті-решт я переходжу до рішення @Andreas Köberle, яке додасть вкладені контексти до моїх тестів (не дуже), але стабільно працює. Я б хотів, щоб хтось міг зосередитись на вирішенні цього рішення більш елегантним чином. Я продовжую дивитись testr.js, і якщо / коли він працює, переключусь.
Кріс Зальцберг

@shioyama привіт, дякую за відгук! Я хотів би поглянути на те, як ви налаштували testr.js у своєму тестовому стеку. Раді допомогти вам вирішити будь-які проблеми, які можуть виникнути! Існує також сторінка випусків github, якщо ви хочете щось там увійти. Спасибі,
Матті Ф

1
@MattyF Вибачте, я зараз навіть не згадую, яка саме причина полягала в тому, що testr.js не працював на мене, але я прийшов до висновку, що використання зайвих контекстів насправді цілком гаразд і насправді відповідає черзі з тим, як Requ.js мався на увазі використовувати для глузування / заглушки.
Кріс Зальцберг

2

Ця відповідь заснована на відповіді Андреаса Кеберле .
Мені було не так просто реалізувати та зрозуміти його рішення, тому я поясню його трохи детальніше, як воно працює, та деякі підводні камені, яких слід уникати, сподіваючись, що це допоможе майбутнім відвідувачам.

Отже, насамперед налаштування:
я використовую Karma в якості тестового бігуна та MochaJs як тестовий фреймворк.

Використовуючи щось на зразок Squire , у мене не вийшло, чомусь, коли я ним користувався, тестовий фреймворк викинув помилки:

TypeError: Неможливо прочитати властивість 'call' невизначеного

RequireJs має можливість зіставити ідентифікатори модуля на інші ідентифікатори модуля. Це також дозволяє створити requireфункцію, яка використовує інший конфігурацію, ніж глобальна require.
Ці функції мають вирішальне значення для роботи цього рішення.

Ось моя версія мок-коду, включаючи (багато) коментарів (сподіваюся, це зрозуміло). Я загорнув його всередину модуля, щоб тести могли його легко вимагати.

define([], function () {
    var count = 0;
    var requireJsMock= Object.create(null);
    requireJsMock.createMockRequire = function (mocks) {
        //mocks is an object with the module ids/paths as keys, and the module as value
        count++;
        var map = {};

        //register the mocks with unique names, and create a mapping from the mocked module id to the mock module id
        //this will cause RequireJs to load the mock module instead of the real one
        for (property in mocks) {
            if (mocks.hasOwnProperty(property)) {
                var moduleId = property;  //the object property is the module id
                var module = mocks[property];   //the value is the mock
                var stubId = 'stub' + moduleId + count;   //create a unique name to register the module

                map[moduleId] = stubId;   //add to the mapping

                //register the mock with the unique id, so that RequireJs can actually call it
                define(stubId, function () {
                    return module;
                });
            }
        }

        var defaultContext = requirejs.s.contexts._.config;
        var requireMockContext = { baseUrl: defaultContext.baseUrl };   //use the baseUrl of the global RequireJs config, so that it doesn't have to be repeated here
        requireMockContext.context = "context_" + count;    //use a unique context name, so that the configs dont overlap
        //use the mapping for all modules
        requireMockContext.map = {
            "*": map
        };
        return require.config(requireMockContext);  //create a require function that uses the new config
    };

    return requireJsMock;
});

Велика пастка я зіткнувся, що буквально коштувало мені годин, створював RequireJs конфігурації. Я спробував (глибоко) скопіювати його та лише перекреслити необхідні властивості (наприклад, контекст чи карту). Це не працює! Тільки скопіюйте baseUrl, це добре працює.

Використання

Щоб скористатись цим, зажадайте його у вашому тесті, створіть макети, а потім передайте їх createMockRequire. Наприклад:

var ModuleMock = function () {
    this.method = function () {
        methodCalled += 1;
    };
};
var mocks = {
    "ModuleIdOrPath": ModuleMock
}
var requireMocks = mocker.createMockRequire(mocks);

І ось приклад повного тестового файлу :

define(["chai", "requireJsMock"], function (chai, requireJsMock) {
    var expect = chai.expect;

    describe("Module", function () {
        describe("Method", function () {
            it("should work", function () {
                return new Promise(function (resolve, reject) {
                    var handler = { handle: function () { } };

                    var called = 0;
                    var moduleBMock = function () {
                        this.method = function () {
                            methodCalled += 1;
                        };
                    };
                    var mocks = {
                        "ModuleBIdOrPath": moduleBMock
                    }
                    var requireMocks = requireJsMock.createMockRequire(mocks);

                    requireMocks(["js/ModuleA"], function (moduleA) {
                        try {
                            moduleA.method();   //moduleA should call method of moduleBMock
                            expect(called).to.equal(1);
                            resolve();
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
            });
        });
    });
});

0

якщо ви хочете зробити кілька простих js-тестів, які виділяють одну одиницю, ви можете просто скористатися цим фрагментом:

function define(args, func){
    if(!args.length){
        throw new Error("please stick to the require.js api which wants a: define(['mydependency'], function(){})");
    }

    var fileName = document.scripts[document.scripts.length-1].src;

    // get rid of the url and path elements
    fileName = fileName.split("/");
    fileName = fileName[fileName.length-1];

    // get rid of the file ending
    fileName = fileName.split(".");
    fileName = fileName[0];

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