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

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

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

Как я могу издеваться над hurp и durp, чтобы эффективно unit test?

Ответ 1

Итак, прочитав этот пост, я придумал решение, использующее функцию конфигурации 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');
      })
    });
  });
})();

Итак, я использую этот подход в производстве на некоторое время и его действительно надежный.

Ответ 2

вы можете проверить новый Squire.js lib

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

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

Ответ 3

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

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

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

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

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

Fugly. Вы должны загромождать свои тесты множеством шаблонов AMD.

Загрузка Mock-зависимостей с разных путей

Это связано с использованием отдельного файла config.js для определения путей для каждой из зависимостей, которые указывают на mocks вместо исходных зависимостей. Это также уродливо, требуя создания множества тестовых файлов и файлов конфигураций.

Подделка в Node

Это мое текущее решение, но все еще ужасное.

Вы создаете свою собственную функцию 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);

Это мое предпочтительное решение. Это выглядит немного волшебным, но у него есть несколько преимуществ.

  • Запустите ваши тесты в node, поэтому не путайтесь с автоматизацией браузера.
  • Меньше необходимости для грязного шаблона AMD в ваших тестах.
  • Вы можете использовать eval в гневе и представить себе, как Крокфорд взрывается с яростью.

У него все еще есть некоторые недостатки, очевидно.

  • Поскольку вы тестируете в node, вы ничего не можете делать с событиями браузера или манипуляциями с DOM. Только полезно для тестирования логики.
  • Все еще немного неудобно настраивать. Вам нужно издеваться над define в каждом тесте, так как именно там ваши тесты действительно выполняются.

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

Заключение

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

Ответ 4

Здесь есть опция config.map http://requirejs.org/docs/api.html#config-map.

О том, как использовать его:

  • Определить нормальный модуль;
  • Определить модуль заглушки;
  • Настроить RequireJS на неопределенный срок;

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

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

Ответ 5

Вы можете использовать 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/

Ответ 6

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

Итак, прежде всего настройка:
Я использую Karma как тестовый бегун и MochaJs как тестовую среду.

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

TypeError: Невозможно прочитать свойство "вызов" undefined

RequireJs имеет возможность map id модуля для других идентификаторов модулей. Это также позволяет создать require функцию, в которой используется different config, чем глобальный 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, это отлично работает.

Использование

Чтобы использовать его, попросите его в своем тесте, создайте mocks и передайте его 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);
                        }
                    });
                });
            });
        });
    });
});

Ответ 7

если вы хотите сделать некоторые простые 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;