Bagaimana saya bisa mengejek dependensi untuk pengujian unit di RequireJS?

127

Saya memiliki modul AMD yang ingin saya uji, tetapi saya ingin mengejek dependensinya alih-alih memuat dependensi yang sebenarnya. Saya menggunakan requirejs, dan kode untuk modul saya terlihat seperti ini:

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

Bagaimana saya bisa mengejek hurpdan durpjadi saya bisa secara efektif menguji unit?

jergason
sumber
Saya hanya melakukan beberapa hal gila eval di node.js untuk mengejek definefungsi. Ada beberapa opsi berbeda. Saya akan mengirim jawaban dengan harapan itu akan membantu.
jergason
1
Untuk pengujian unit dengan Jasmine Anda mungkin juga ingin melihat Jasq dengan cepat . [Penafian: Saya mempertahankan lib]
biril
1
Jika Anda menguji dalam simpul env Anda bisa menggunakan paket mock-butuhkan . Hal ini memungkinkan Anda untuk dengan mudah mengejek dependensi Anda, mengganti modul dll. Jika Anda memerlukan browser env dengan memuat modul async - Anda dapat mencoba Squire.js
ValeriiVasin

Jawaban:

64

Jadi setelah membaca posting ini saya datang dengan solusi yang menggunakan fungsi config requirejs untuk membuat konteks baru untuk pengujian Anda di mana Anda dapat dengan mudah mengejek dependensi Anda:

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

Jadi itu menciptakan konteks baru di mana definisi untuk Hurpdan Durpakan ditetapkan oleh objek yang Anda berikan ke fungsi. Math.random untuk namanya mungkin agak kotor tapi berhasil. Karena jika Anda akan memiliki banyak tes, Anda perlu membuat konteks baru untuk setiap suite untuk mencegah penggunaan kembali ejekan Anda, atau untuk memuat ejekan saat Anda menginginkan modul yang diperlukan.

Dalam kasus Anda akan terlihat seperti ini:

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

Jadi saya menggunakan pendekatan ini dalam produksi untuk sementara waktu dan sangat kuat.

Andreas Köberle
sumber
1
Saya suka apa yang Anda lakukan di sini ... terutama karena Anda dapat memuat konteks yang berbeda untuk setiap tes. Satu-satunya hal yang saya harap bisa saya ubah adalah sepertinya itu hanya berfungsi jika saya mengejek semua dependensi. Apakah Anda tahu cara mengembalikan objek tiruan jika ada, tetapi mundur untuk mengambil dari file .js yang sebenarnya jika tiruan tidak disediakan? Saya sudah mencoba menggali melalui kode yang diperlukan untuk mencari tahu, tapi saya agak tersesat.
Glen Hughes
5
Itu hanya mengolok-olok ketergantungan yang Anda berikan ke createContextfungsi. Jadi dalam kasus Anda jika Anda hanya {hurp: 'hurp'}berfungsi, durpfile akan dimuat sebagai ketergantungan normal.
Andreas Köberle
1
Saya menggunakan ini di Rails (dengan jasminerice / phantomjs) dan telah menjadi solusi terbaik yang saya temukan untuk mengejek dengan RequireJS.
Ben Anderson
13
+1 Tidak cantik, tetapi dari semua solusi yang mungkin, ini tampaknya menjadi yang paling jelek / berantakan. Masalah ini patut mendapat perhatian lebih.
Chris Salzberg
1
Mutakhirkan: kepada siapa pun yang mempertimbangkan solusi ini, saya sarankan untuk memeriksa squire.js ( github.com/iammerrick/Squire.js ) yang disebutkan di bawah ini. Ini adalah implementasi bagus dari solusi yang mirip dengan yang ini, menciptakan konteks baru di mana pun bertopik diperlukan.
Chris Salzberg
44

Anda mungkin ingin memeriksa lib Squire.js baru

dari dokumen:

Squire.js adalah injektor dependensi bagi pengguna Require.js untuk membuat dependensi mengejek mudah!

bangkrut
sumber
2
Sangat direkomendasikan! Saya memperbarui kode saya untuk menggunakan squire.js dan sejauh ini saya sangat menyukainya. Kode yang sangat sangat sederhana, tidak ada keajaiban besar di bawah tenda, tetapi dilakukan dengan cara yang (relatif) mudah dimengerti.
Chris Salzberg
1
Saya punya banyak masalah dengan sisi pengawal yang mempengaruhi tes lain dan tidak bisa merekomendasikannya. saya ingin merekomendasikan npmjs.com/package/requirejs-mock
Jeff Whiting
17

Saya telah menemukan tiga solusi berbeda untuk masalah ini, tidak ada yang menyenangkan.

Mendefinisikan Dependensi Inline

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

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

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

Jelek Anda harus mengacaukan tes Anda dengan banyak boilerplate AMD.

Memuat Ketergantungan Mock Dari Berbagai Jalur

Ini melibatkan menggunakan file config.js terpisah untuk menentukan jalur untuk setiap dependensi yang menunjuk ke mengejek alih-alih dependensi asli. Ini juga jelek, membutuhkan penciptaan banyak file uji dan file konfigurasi.

Fake It In Node

Ini adalah solusi saya saat ini, tetapi masih merupakan solusi yang mengerikan.

Anda membuat definefungsi Anda sendiri untuk memberikan tiruan Anda sendiri ke modul dan menempatkan tes Anda dalam panggilan balik. Maka Anda evalmodul untuk menjalankan tes Anda, seperti:

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

Ini adalah solusi pilihan saya. Itu terlihat sedikit ajaib, tetapi memiliki beberapa manfaat.

  1. Jalankan tes Anda di node, jadi jangan mengotak-atik otomatisasi browser.
  2. Kurang perlu untuk boilerplate AMD berantakan dalam tes Anda.
  3. Anda bisa menggunakannya eval kemarahan, dan bayangkan Crockford meledak dengan amarah.

Itu masih memiliki beberapa kelemahan, jelas.

  1. Karena Anda menguji dalam simpul, Anda tidak dapat melakukan apa pun dengan peristiwa browser atau manipulasi DOM. Hanya bagus untuk menguji logika.
  2. Masih sedikit kikuk untuk diatur. Anda perlu mengejek definedalam setiap tes, karena di situlah tes Anda benar-benar berjalan.

Saya sedang mengerjakan test runner untuk memberikan sintaksis yang lebih bagus untuk hal-hal semacam ini, tetapi saya masih belum memiliki solusi yang baik untuk masalah 1.

Kesimpulan

Mengejek deps dalam Requirejs menyebalkan. Saya menemukan cara semacam itu berfungsi, tetapi saya masih tidak begitu senang dengannya. Tolong beri tahu saya jika Anda memiliki ide yang lebih baik.

jergason
sumber
15

Ada config.mapopsi http://requirejs.org/docs/api.html#config-map .

Tentang cara menggunakannya:

  1. Tentukan modul normal;
  2. Tentukan modul rintisan;
  3. Konfigurasikan PersyaratanJS secara tak terduga;

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

Dalam hal ini untuk kode tes normal dan Anda dapat menggunakan foomodul yang akan menjadi referensi modul nyata dan rintisan sesuai.

Artem Oboturov
sumber
Pendekatan ini bekerja sangat baik untuk saya. Dalam kasus saya, saya menambahkan ini ke html dari halaman pengujian pelari -> map: { '*': { 'Umum / Modul / usefulModule': '/Tests/Specs/Common/usefulModuleMock.js'}}
Blok
9

Anda dapat menggunakan testr.js untuk mengejek dependensi. Anda dapat mengatur testr untuk memuat dependensi tiruan alih-alih yang asli. Berikut adalah contoh penggunaannya:

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

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

Lihat ini juga: http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/

janith
sumber
2
Saya benar-benar ingin testr.js bekerja, tetapi belum terasa sesuai dengan tugasnya. Pada akhirnya saya akan dengan solusi @ Andreas Köberle, yang akan menambahkan konteks bersarang untuk tes saya (tidak cantik) tetapi yang secara konsisten berfungsi. Saya berharap seseorang dapat fokus pada penyelesaian solusi ini dengan cara yang lebih elegan. Saya akan terus menonton testr.js dan jika / ketika bekerja, akan beralih.
Chris Salzberg
@shioyama hai, terima kasih atas umpan baliknya! Saya ingin melihat bagaimana Anda mengonfigurasi testr.js di dalam tumpukan pengujian Anda. Senang membantu Anda memperbaiki masalah yang mungkin Anda alami! Ada juga halaman Masalah github jika Anda ingin mencatat sesuatu di sana. Terima kasih,
Matty F
1
@MattyF maaf saya bahkan tidak ingat sekarang apa alasan tepatnya bahwa testr.js tidak bekerja untuk saya, tapi saya sampai pada kesimpulan bahwa penggunaan konteks ekstra sebenarnya cukup baik dan bahkan sejalan dengan bagaimana require.js dimaksudkan untuk digunakan untuk mengejek / mematikan.
Chris Salzberg
2

Jawaban ini didasarkan pada jawaban Andreas Köberle .
Itu tidak mudah bagi saya untuk menerapkan dan memahami solusinya, jadi saya akan menjelaskannya dengan sedikit lebih detail bagaimana cara kerjanya, dan beberapa jebakan yang harus dihindari, berharap itu akan membantu pengunjung masa depan.

Jadi, pertama-tama pengaturan:
Saya menggunakan Karma sebagai pelari uji dan MochaJs sebagai kerangka uji.

Menggunakan sesuatu seperti Squire tidak berhasil untuk saya, untuk beberapa alasan, ketika saya menggunakannya, kerangka uji melemparkan kesalahan:

TypeError: Tidak dapat membaca properti 'panggilan' yang tidak ditentukan

RequireJs memiliki kemungkinan untuk memetakan id modul ke id modul lainnya. Ini juga memungkinkan untuk membuat requirefungsi yang menggunakan konfigurasi berbeda dari global require.
Fitur-fitur ini sangat penting untuk solusi ini untuk bekerja.

Ini adalah versi saya dari kode mock, termasuk (banyak) komentar (saya harap ini dapat dimengerti). Saya membungkusnya di dalam modul, sehingga tes dapat dengan mudah membutuhkannya.

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

Perangkap terbesar yang saya temui, yang benar-benar menghabiskan waktu berjam-jam, adalah menciptakan konfigurasi RequireJs. Saya mencoba (mendalam) menyalinnya, dan hanya menimpa properti yang diperlukan (seperti konteks atau peta). Ini tidak bekerja! Hanya salin baseUrl, ini berfungsi dengan baik.

Pemakaian

Untuk menggunakannya, minta itu dalam pengujian Anda, buat tiruannya, dan kemudian berikan createMockRequire. Sebagai contoh:

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

Dan di sini contoh file uji lengkap :

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

jika Anda ingin membuat beberapa tes js sederhana yang mengisolasi satu unit, maka Anda cukup menggunakan cuplikan ini:

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;
pengguna3033599
sumber