Как я могу имитировать зависимости для модульного тестирования в RequireJS?

127

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

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

Как я могу имитировать hurpи durpэффективно тестировать модули?

jergason
источник
Я просто делаю сумасшедшие eval в node.js, чтобы имитировать defineфункцию. Однако есть несколько разных вариантов. Я отправлю ответ в надежде, что это будет полезно.
jergason
1
Для модульного тестирования с помощью Jasmine вы также можете быстро взглянуть на Jasq . [Отказ от ответственности: я поддерживаю
библиотеку
1
Если вы тестируете в node env, вы можете использовать пакет require-mock . Это позволяет вам легко имитировать ваши зависимости, заменять модули и т. Д. Если вам нужен env браузера с загрузкой асинхронного модуля - вы можете попробовать Squire.js
ValeriiVasin

Ответы:

64

Итак, после прочтения этого поста я придумал решение, использующее функцию конфигурации requirejs для создания нового контекста для вашего теста, где вы можете просто имитировать свои зависимости:

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

В вашем случае это будет выглядеть так:

(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, если макет не предоставлен? Я пытался разобраться в коде require, чтобы понять это, но я немного теряюсь.
Glen Hughes
5
Он только издевается над зависимостью, которую вы передаете createContextфункции. Итак, в вашем случае, если вы перейдете только {hurp: 'hurp'}к функции, durpфайл будет загружен как обычная зависимость.
Андреас Кёберле
1
Я использую это в Rails (с jasminerice / phantomjs), и это было лучшее решение, которое я нашел для насмешек с RequireJS.
Бен Андерсон,
13
+1 Неприятно, но из всех возможных решений это кажется наименее уродливым / грязным. Эта проблема заслуживает большего внимания.
Крис Зальцберг
1
Обновление: всем, кто рассматривает это решение, я бы посоветовал проверить squire.js ( github.com/iammerrick/Squire.js ), упомянутый ниже. Это хорошая реализация решения, похожего на это, которое создает новые контексты везде, где нужны заглушки.
Крис Зальцберг
44

Вы можете попробовать новую библиотеку Squire.js

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

Squire.js - это инжектор зависимостей для пользователей Require.js, который упрощает имитацию зависимостей!

busticated
источник
2
Настоятельно рекомендуется! Я обновляю свой код, чтобы использовать squire.js, и пока он мне очень нравится. Очень-очень простой код, никакой магии под капотом, но сделано таким образом, чтобы (относительно) легко понять.
Крис Зальцберг
1
У меня было много проблем с побочными эффектами сквайра на других тестах, и я не могу рекомендовать это. Я бы порекомендовал npmjs.com/package/requirejs-mock
Джефф Уайтинг,
17

Я нашел три разных решения этой проблемы, ни одно из них не было приятным.

Встроенное определение зависимостей

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

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

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

Fugly. Вы должны загромождать свои тесты большим количеством шаблонов 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.

Вывод

Mocking deps в requirejs - отстой. Я нашел способ, которым это работает, но все еще не очень им доволен. Пожалуйста, дайте мне знать, если у вас есть идеи получше.

jergason
источник
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 страницы тестового бегуна -> карта: {'*': {'Common / Modules / полезноModule': '/Tests/Specs/Common/usefulModuleMock.js'}}
Выровнено
9

Вы можете использовать testr.js для имитации зависимостей. Вы можете настроить testr так, чтобы он загружал фиктивные зависимости вместо исходных. Вот пример использования:

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

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

Также проверьте это: http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/

janith
источник
2
Я действительно хотел, чтобы testr.js работал, но он пока не совсем подходит для этой задачи. В конце концов, я выберу решение @Andreas Köberle, которое добавит вложенные контексты в мои тесты (некрасиво), но которое стабильно работает. Я хотел бы, чтобы кто-то мог сосредоточиться на решении этого решения более элегантным способом. Я буду продолжать смотреть testr.js, и если / когда он сработает, переключусь.
Крис Зальцберг
@shioyama привет, спасибо за отзыв! Я хотел бы посмотреть, как вы настроили testr.js в своем тестовом стеке. Мы рады помочь вам решить любые проблемы, которые могут у вас возникнуть! Также есть страница с проблемами github, если вы хотите что-то там записать. Спасибо,
Matty F
1
@MattyF извините, я даже не помню прямо сейчас, в чем именно причина того, что testr.js не работал у меня, но я пришел к выводу, что использование дополнительных контекстов на самом деле вполне нормально и фактически соответствует с тем, как require.js должен был использоваться для имитации / заглушки.
Крис Зальцберг,
2

Этот ответ основан на ответе Андреаса Кёберле .
Мне было нелегко реализовать и понять его решение, поэтому я более подробно объясню, как оно работает, и некоторые подводные камни, которых следует избегать, надеясь, что это поможет будущим посетителям.

Итак, во-первых, настройка:
я использую Karma как средство запуска тестов и MochaJs как среду тестирования.

Использование чего-то вроде Squire у меня не сработало, по какой-то причине, когда я его использовал, тестовая среда выдавала ошибки:

TypeError: не удается прочитать вызов свойства undefined

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);
                        }
                    });
                });
            });
        });
    });
});
Domysee
источник
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;
user3033599
источник