2012-07-11 202 views
126

我有一個AMD模塊我想測試,但我想嘲笑它的依賴而不是加載實際的依賴關係。我使用requirejs,爲我的模塊的代碼看起來是這樣的:如何在RequireJS中模擬單元測試的依賴關係?

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

我怎樣才能模擬出hurpdurp這樣我就可以有效的單元測試?

+0

我只是做一些瘋狂的東西的eval在node.js中嘲笑了'define'功能。儘管有幾個不同的選擇。我會發佈一個答案,希望它會有幫助。 – jergason 2012-07-27 16:10:53

+1

對於使用Jasmine進行單元測試,您可能還想快速查看[Jasq](https://github.com/biril/jasq)。 [免責聲明:我維護庫] – biril 2014-10-08 16:43:23

+1

如果你在節點env中測試,你可以使用[require-mock](https://github.com/ValeriiVasin/requirejs-mock)包。它允許你輕鬆地模擬你的依賴關係,替換模塊等。如果你需要瀏覽器env與異步模塊加載 - 你可以嘗試[Squire.js](https://github.com/iammerrick/Squire.js/) – ValeriiVasin 2015-03-23 10:27:35

回答

16

我發現了三個不同的解決方案來解決這個問題,他們都不愉快。

的依賴項內嵌

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.

結論

在requirejs懲戒DEPS吸硬。我發現了一種可行的方法,但對它仍然不滿意。請讓我知道你是否有更好的想法。

64

所以讀this post後,我想出了使用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/' 
    }); 
} 

所以它會創建一個新的上下文,其中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'); 
     }) 
    }); 
    }); 
})(); 

所以我在生產中使用這種方法了一段時間,其真正強大的。

+1

我喜歡你在這裏做什麼...特別是因爲你可以爲每個測試加載不同的上下文。我希望我能改變的唯一的事情是它似乎只在我嘲笑所有的依賴關係時才起作用。你知道一種方法來返回模擬對象,如果他們在那裏,但如果沒有提供模擬,退回到實際的.js文件中?我一直試圖挖掘需要的代碼來弄清楚,但我有點迷路。 – 2012-07-29 00:36:36

+5

它只是嘲笑你傳遞給'createContext'函數的依賴。所以在你的情況下,如果你只通過'{hurp:'hurp'}'來運行,'durp'文件將作爲正常的依賴項加載。 – 2012-07-29 08:48:06

+0

你是對的......不確定我第一次做錯了什麼。偉大的工作,這將派上用場......非常感謝! – 2012-07-30 14:11:41

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模塊,這將是真正的模塊引用和相應的存根。

+0

這種方法對我來說真的很好。就我而言,我加入到測試亞軍頁面的HTML - >圖:{\t \t \t '*':{ '通用/模塊/ usefulModule': '/Tests/Specs/Common/usefulModuleMock.js'}} – Aligned 2014-08-18 16:20:26

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/

+2

我真的希望testr.js能夠工作,但是它還沒有完成任務。最後,我要和@AndreasKöberle的解決方案一起使用,它會將嵌套的上下文添加到我的測試中(而不是很漂亮),但它始終如一。我希望有人可以專注於以更優雅的方式解決這個問題。我會繼續觀看testr.js,如果/它什麼時候工作,將會進行切換。 – 2012-10-28 03:19:35

+0

@shioyama嗨,感謝您的反饋!我很想看看你如何在測試棧中配置testr.js。很高興幫助您解決您可能遇到的任何問題!如果你想在那裏登錄,還有github的問題頁面。謝謝, – 2012-12-05 03:20:04

+1

@MattyF對不起,我不記得,即使現在有什麼確切的原因是testr.js並沒有爲我工作,但我得出的結論是,使用額外的背景其實是相當正常的,並事實上符合require.js是如何用於嘲弄/剔除的。 – 2013-06-24 14:10:03

44

你可能想從文檔看看新的Squire.js lib

Squire.js是Require.js用戶的依賴注入器使嘲笑依賴變得容易!

+5

Squire爲我工作得非常好。 – 2013-04-29 21:26:47

+2

強烈推薦!我正在更新我的代碼以使用squire.js,到目前爲止我非常喜歡它。非常非常簡單的代碼,在引擎蓋下沒有很棒的魔法,但以相對容易理解的方式完成。 – 2013-06-24 14:06:35

+0

我有很多問題與鄉紳側影響其他測試,並不能推薦它。 我會建議https://www.npmjs.com/package/requirejs-mock – 2016-09-13 14:36:25

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; 
2

這個答案是基於Andreas Köberle's answer
對我來說,實現和理解他的解決方案並不是那麼容易,所以我會更詳細地解釋它的工作方式,以及一些避免的陷阱,希望能夠幫助未來的訪問者。

因此,首先設置的:
我使用爲測試運行和MochaJs作爲測試框架。

使用類似鄉紳我沒有工作,出於某種原因,當我用它,測試框架扔錯誤:

TypeError: Cannot read property 'call' of undefined

RequireJs有可能性map模塊ID到其他模塊IDS。它還允許創建一個使用different config的全局requirerequire 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); 
         } 
        }); 
       }); 
      }); 
     }); 
    }); 
}); 
相關問題