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


127

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

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

どのように私はモックできるhurpdurp私は効果的にユニットテストできるように?


私はdefine関数をモックアウトするために、node.jsでいくつかのクレイジーな評価を行っています。ただし、いくつかの異なるオプションがあります。お役に立てれば幸いです。
ジャーガソン2012

1
Jasmineでの単体テストについては、Jasqをざっと見てみることもできます。[免責事項:私はlibを維持しています]
biril

1
ノード環境でテストしている場合は、require-mockパッケージを使用できます。それはあなたが簡単にあなたの依存関係を模擬することができ、モジュールなどを交換しますが、非同期モジュールのロードでのブラウザのenvが必要な場合-あなたは試みることができるSquire.jsを
ValeriiVasin

回答:


64

したがって、この投稿を読んだ後、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/'
  });
}

定義新しいコンテキストを作成それはそう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ファイルは通常の依存関係として読み込まれます。
AndreasKöberle2012

1
私はこれをRailsで(jasminerice / phantomjsとともに)使用しており、RequireJSでモックを作成するために見つけた最良のソリューションでした。
ベンアンダーソン

13
+1きれいではありませんが、考えられるすべての解決策の中で、これは最も醜く/厄介なもののようです。この問題はもっと注意に値する。
Chris Salzberg、2012年

1
更新:このソリューションを検討している人には、以下のsquire.js(github.com/iammerrick/Squire.js)をチェックすることをお勧めします。これは、スタブが必要な場所に新しいコンテキストを作成する、このソリューションと同様のソリューションの優れた実装です。
Chris Salzberg 2013年

44

あなたは新しいSquire.js libをチェックアウトしたいかもしれません

ドキュメントから:

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


2
強く推奨する!私はsquire.jsを使用するようにコードを更新していますが、これまでのところ私はそれを大いに気に入っています。非常に単純なコードであり、内部には大きな魔法はありませんが、(比較的)理解しやすい方法で実行されます。
Chris Salzberg

1
私は他のテストに影響を与える大地主の副作用で多くの問題を抱えており、それを推奨することはできません。私が推薦するnpmjs.com/package/requirejs-mock
ジェフ・ホワイティング

17

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

依存関係をインラインで定義する

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のための良い解決策を持っていません。

結論

requirejsでdepsをモックするのは難しいです。私は一種の動作する方法を見つけましたが、それでもまだあまり満足していません。もっと良いアイデアがありましたら教えてください。


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 / usefulModule': '/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-dependencies-for-unit-testing/


2
私は本当にtestr.jsが機能することを望んでいましたが、まだタスクに完全には対応できていません。最後に、@ AndreasKöberleのソリューションを使用します。これにより、ネストされたコンテキストがテストに追加されますが(きれいではありません)、一貫して機能します。私は誰かがこのソリューションをよりエレガントな方法で解決することに集中できることを願っています。testr.jsを引き続き監視し、動作するかどうかを切り替えます。
Chris Salzberg、2012年

@shioyamaやあ、フィードバックありがとう!テストスタック内でtestr.jsをどのように構成したかを見てみたいと思います。発生する可能性のある問題の修正をお手伝いします。何かをログに記録したい場合は、githubの問題ページもあります。ありがとう、
Matty F

1
@MattyF申し訳ありませんが、testr.jsが機能しなかった正確な理由は今のところ思い出せませんが、余分なコンテキストの使用は実際にはまったく問題なく、実際には正しいという結論に達しましたrequire.jsがモック/スタブに使用されることを想定していました。
Chris Salzberg、2013年

2

この回答は、AndreasKöberleの回答に基づいています
私が彼のソリューションを実装して理解するのはそれほど簡単ではなかったので、それがどのように機能するか、および回避するいくつかの落とし穴についてもう少し詳しく説明し、将来の訪問者に役立つことを期待します。

つまり、まず最初に、Karmaをテストランナーとして、MochaJをテストフレームワークとして
使用しています。

Squireなどを使用しても機能しませんでした。何らかの理由で、それを使用すると、テストフレームワークでエラーが発生しました。

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

RequireJsは、モジュールIDを他のモジュールID にマップする可能性があります。また、グローバルとは異なる構成を使用する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

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;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.