RequireJS'de birim testi için bağımlılıkları nasıl taklit edebilirim?


127

Test etmek istediğim bir AMD modülüm var, ancak gerçek bağımlılıkları yüklemek yerine bağımlılıklarıyla dalga geçmek istiyorum. Requjs kullanıyorum ve modülümün kodu şuna benzer:

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

Nasıl alay edebilirim hurpve durpböylece etkili bir şekilde birim testi yapabilirim?


defineFonksiyonu taklit etmek için node.js'de bazı çılgın değerlendirme işlemleri yapıyorum . Yine de birkaç farklı seçenek var. Yardımcı olacağı ümidiyle bir cevap göndereceğim.
jergason

1
Jasmine ile birim testi için Jasq'a da hızlıca göz atmak isteyebilirsiniz . [Sorumluluk reddi:
Kitaplığı

1
Düğüm env de test ediyorsanız, gerektiren-sahte paketi kullanabilirsiniz. Bağımlılıklarınızı kolayca taklit etmenize, modülleri değiştirmenize vb. İzin verir. Async modül yüklü tarayıcı ortamına
ValeriiVasin'i

Yanıtlar:


64

Yani okuduktan sonra bu yazıyı ben basitçe bağımlılıkları taklit test için yeni bir bağlam oluşturmak için requirejs yapılandırma işlevini kullanın bir çözüm geldi:

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

Bunun için tanımları yeni bağlamı oluşturur Yani Hurpve Durpişleve geçirilen nesneler tarafından ayarlanacaktır. İsim için Math.random biraz kirli olabilir ama işe yarıyor. Çünkü bir sürü testiniz olacaksa, taklitlerinizi yeniden kullanmayı önlemek veya gerçek requirejs modülünü istediğinizde taklitler yüklemek için her paket için yeni bağlam oluşturmanız gerekir.

Senin durumunda şöyle görünecek:

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

Bu yüzden bu yaklaşımı üretimde bir süredir kullanıyorum ve gerçekten sağlam.


1
Burada yaptığınız işi beğendim ... özellikle her test için farklı bir bağlam yükleyebildiğiniz için. Değiştirebilmeyi dilediğim tek şey, tüm bağımlılıkları alay edersem işe yarıyor gibi görünmesidir. Varsa, sahte nesneleri geri döndürmenin bir yolunu biliyor musunuz, ancak bir sahte sağlanmadıysa gerçek .js dosyasından geri alınmaya geri dönüyor musunuz? Bunu anlamak için gerekli kodu araştırmaya çalışıyorum ama biraz kayboluyorum.
Glen Hughes

5
Yalnızca createContextişleve verdiğiniz bağımlılıkla alay eder . Yani sizin durumunuzda sadece {hurp: 'hurp'}işleve geçerseniz , durpdosya normal bir bağımlılık olarak yüklenecektir.
Andreas Köberle

1
Bunu Rails'de kullanıyorum (jasminerice / phantomjs ile) ve RequireJS ile alay etmek için bulduğum en iyi çözüm oldu.
Ben Anderson

13
+1 Güzel değil, ancak olası tüm çözümler arasında bu en az çirkin / dağınık olanı gibi görünüyor. Bu sorun daha fazla ilgiyi hak ediyor.
Chris Salzberg

1
Güncelleme: Bu çözümü düşünen herkese, aşağıda belirtilen squire.js'yi ( github.com/iammerrick/Squire.js ) kontrol etmenizi öneririm . Bu, buna benzer bir çözümün güzel bir uygulamasıdır ve stub'lara ihtiyaç duyulan her yerde yeni bağlamlar oluşturur.
Chris Salzberg

44

yeni Squire.js lib'ye göz atmak isteyebilirsiniz

dokümanlardan:

Squire.js, Require.js kullanıcılarının alay bağımlılıklarını kolaylaştırması için bir bağımlılık enjektörüdür!


2
Kesinlikle önerilir! Squire.js kullanmak için kodumu güncelliyorum ve şimdiye kadar çok beğeniyorum. Çok çok basit bir kod, kaputun altında büyük bir sihir yok, ancak anlaşılması (nispeten) kolay bir şekilde yapıldı.
Chris Salzberg

1
Diğer testleri etkileyen squire tarafıyla ilgili birçok sorun yaşadım ve bunu tavsiye edemem. Tavsiye ederim npmjs.com/package/requirejs-mock
Jeff Whiting

17

Bu soruna üç farklı çözüm buldum, hiçbiri hoş değil.

Bağımlılıkları Satır İçi Tanımlama

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

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

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

Fugly. Testlerinizi birçok AMD standart metniyle karıştırmanız gerekir.

Farklı Yollardan Sahte Bağımlılıkları Yükleme

Bu, orijinal bağımlılıklar yerine taklitlere işaret eden bağımlılıkların her biri için yolları tanımlamak için ayrı bir config.js dosyası kullanmayı içerir. Bu aynı zamanda çirkin, tonlarca test dosyası ve konfigürasyon dosyası oluşturulmasını gerektiriyor.

Düğümde Taklit Et

Bu benim şu anki çözümüm, ancak yine de korkunç bir çözüm.

defineModüle kendi taklitlerinizi sağlamak için kendi işlevinizi oluşturursunuz ve testlerinizi geri aramaya koyarsınız. Sonra evaltestlerinizi çalıştıracak modül sizsiniz , şöyle:

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

Bu benim tercih ettiğim çözüm. Biraz sihir gibi görünüyor, ancak birkaç faydası var.

  1. Testlerinizi düğümde çalıştırın, böylece tarayıcı otomasyonu ile uğraşmayın.
  2. Testlerinizde dağınık AMD standart şablonuna daha az ihtiyaç.
  3. evalÖfkeyle kullanacaksın ve Crockford'un öfkeyle patladığını hayal et.

Açıkçası, hala bazı dezavantajları var.

  1. Düğümde test yaptığınız için tarayıcı olayları veya DOM manipülasyonu ile hiçbir şey yapamazsınız. Sadece mantığı test etmek için iyidir.
  2. Hala ayarlamak için biraz hantal. defineHer testte alay etmeniz gerekir , çünkü testlerinizin gerçekte yapıldığı yer burasıdır.

Bu tür şeyler için daha güzel bir sözdizimi vermek için bir test çalıştırıcısı üzerinde çalışıyorum, ancak problem 1 için hala iyi bir çözümüm yok.

Sonuç

Requjs içinde alay etmek çok berbat. İşe yarayan bir yol buldum ama yine de pek memnun değilim. Daha iyi fikirleriniz varsa lütfen bana bildirin.


15

Bir var config.mapbir seçenek http://requirejs.org/docs/api.html#config-map .

Nasıl kullanılacağı hakkında:

  1. Normal modülü tanımlayın;
  2. Saplama modülünü tanımlayın;
  3. RequireJS'yi açık bir şekilde yapılandırın;

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

Bu durumda normal ve test kodu fooiçin gerçek modül referansı olacak modülü kullanabilir ve buna göre saplama yapabilirsiniz.


Bu yaklaşım benim için gerçekten işe yaradı. Benim durumumda, ben testi koşucu sayfasının html bu eklendi -> map: { '*': { 'Ortak / Modüller / usefulModule': '/Tests/Specs/Common/usefulModuleMock.js'}}
Bağlantısızlar

9

Bağımlılıkları taklit etmek için testr.js'yi kullanabilirsiniz . Testr'yi orijinal bağımlılıklar yerine sahte bağımlılıkları yükleyecek şekilde ayarlayabilirsiniz. İşte örnek bir kullanım:

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

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

Bunu da kontrol edin: http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/


2
Testr.js'nin çalışmasını gerçekten istedim, ancak henüz göreve tam olarak gelmiyor. Sonunda @Andreas Köberle'nin testlerime iç içe bağlamlar ekleyecek (hoş değil) ama tutarlı bir şekilde çalışan çözümüyle gidiyorum. Birinin bu çözümü daha zarif bir şekilde çözmeye odaklanmasını diliyorum. Testr.js izlemeye devam edeceğim ve işe yararsa / çalışıyorsa geçişi yapacağım.
Chris Salzberg

@shioyama merhaba, geri bildiriminiz için teşekkürler! Test yığınınızda testr.js'yi nasıl yapılandırdığınıza bir göz atmak isterim. Yaşıyor olabileceğiniz sorunları çözmenize yardımcı olmaktan mutluluk duyarız! Orada bir şey kaydetmek istiyorsanız github Sorunları sayfası da var. Teşekkürler,
Matty F

1
@MattyF üzgünüm şu anda tam olarak neden testr.js'nin benim için işe yaramadığını hatırlamıyorum, ancak ekstra bağlamların kullanımının aslında oldukça iyi ve aslında aynı hizada olduğu sonucuna vardım. Required.js'nin alay / saplama için nasıl kullanılması gerektiği ile.
Chris Salzberg

2

Bu cevap Andreas Köberle'nin cevabına dayanmaktadır .
Çözümünü uygulamak ve anlamak benim için o kadar kolay olmadı, bu yüzden onu biraz daha ayrıntılı olarak açıklayacağım ve gelecekteki ziyaretçilere yardımcı olacağını umarak kaçınılması gereken bazı tuzaklar.

Yani, her şeyden önce kurulum: Karma'yı test koşucusu ve MochaJs olarak
kullanıyorum testi çerçeve olarak.

Squire gibi bir şeyi kullanmak benim için işe yaramadı, bazı nedenlerden dolayı onu kullandığımda test çerçevesi hatalar attı:

TypeError: Tanımlanmamış 'çağrısı' özelliği okunamıyor

RequireJs , modül kimliklerini diğer modül kimliklerine eşleme olanağına sahiptir . Ayrıca , globalden farklı bir yapılandırma kullanan bir requireişlev oluşturmaya izin verir . Bu özellikler, bu çözümün çalışması için çok önemlidir.require

İşte (çok sayıda) yorum içeren sahte kod sürümüm (umarım anlaşılırdır). Testlerin kolayca gerektirebilmesi için onu bir modülün içine sardım.

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

Karşılaştığım en büyük tuzak , kelimenin tam anlamıyla saatlere mal oldu, RequireJs yapılandırmasını oluşturmaktı. Onu (derinlemesine) kopyalamaya ve yalnızca gerekli özellikleri (bağlam veya harita gibi) geçersiz kılmaya çalıştım. Bu çalışmıyor! Yalnızca kopyalabaseUrl , bu iyi çalışıyor.

kullanım

Kullanmak için, testinizde gerekli kılın, alayları oluşturun ve ardından ona iletin createMockRequire. Örneğin:

var ModuleMock = function () {
    this.method = function () {
        methodCalled += 1;
    };
};
var mocks = {
    "ModuleIdOrPath": ModuleMock
}
var requireMocks = mocker.createMockRequire(mocks);

Ve burada eksiksiz bir test dosyası örneği :

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

Bir birimi izole eden bazı basit js testleri yapmak istiyorsanız, bu pasajı kullanabilirsiniz:

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;
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.