web-dev-qa-db-ja.com

RequireJSで単体テストの依存関係をモックするにはどうすればよいですか?

テストしたいAMDモジュールがありますが、実際の依存関係をロードするのではなく、その依存関係を模擬したいです。私はrequirejsを使用していますが、私のモジュールのコードは次のようになります:

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

hurpdurpをモックアウトして、単体テストを効果的に行うにはどうすればよいですか?

126
jergason

この投稿 を読んだ後、requirejs config関数を使用してテスト用の新しいコンテキストを作成し、依存関係を簡単にモックできるソリューションを思い付きました:

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/'
  });
}

そのため、HurpDurpの定義が関数に渡したオブジェクトによって設定される新しいコンテキストが作成されます。名前の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');
      })
    });
  });
})();

そのため、私はしばらくの間、このアプローチを実稼働環境で使用していますが、非常に堅牢です。

64

新しい Squire.js lib をチェックアウトすることをお勧めします

ドキュメントから:

Squire.jsは、Require.jsユーザーが依存関係のモックを簡単にするための依存関係インジェクターです!

44
busticated

私はこの問題に対する3つの異なる解決策を見つけましたが、どれも快適なものではありません。

依存関係のインライン定義

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

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

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

Fuい。多くの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を怒りで使用し、Crockfordが怒りで爆発することを想像してください。

明らかに、まだいくつかの欠点があります。

  1. ノードでテストしているため、ブラウザーイベントやDOM操作では何もできません。ロジックのテストにのみ適しています。
  2. 設定するにはまだ少し不格好です。テストが実際に実行されるので、すべてのテストでdefineをモックアウトする必要があります。

この種のより良い構文を提供するためにテストランナーに取り組んでいますが、問題1の良い解決策はまだありません。

結論

Requirejsのモックデプスは非常にひどいものです。 sortofが機能する方法を見つけましたが、まだあまり満足していません。もっと良いアイデアがあれば教えてください。

16
jergason

あります 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モジュールを使用できます。これは、実際のモジュール参照であり、それに応じてスタブになります。

15
Artem Oboturov

testr.js を使用して依存関係をモックできます。元の依存関係の代わりにモック依存関係をロードするようにテスターを設定できます。以下に使用例を示します。

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/

9
janith

この回答は、 AndreasKöberleの回答 に基づいています。

それで、最初にすべてのセットアップ:
テストランナーとしてKarmaMochaJsテストフレームワークとして。

Squireのようなものを使用しても機能しませんでした。何らかの理由で、使用したときにテストフレームワークがエラーをスローしました。

TypeError:未定義のプロパティ 'call'を読み取れません

RequireJs は、モジュールIDを他のモジュールIDに map する可能性があります。また、グローバルrequireよりも 異なる設定 を使用する require function を作成することもできます。
これらの機能は、このソリューションが機能するために重要です。

ここに、(多くの)コメントを含む、私のバージョンのモックコードを示します(理解できることを望みます)。テストで簡単に要求できるように、モジュール内にラップしました。

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);
                        }
                    });
                });
            });
        });
    });
});
2
Domysee

1つのユニットを分離する単純な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;
0
user3033599