Làm cách nào tôi có thể giả định các phụ thuộc để kiểm tra đơn vị trong RequireJS?


127

Tôi có một mô-đun AMD tôi muốn thử nghiệm, nhưng tôi muốn mô phỏng các phụ thuộc của nó thay vì tải các phụ thuộc thực tế. Tôi đang sử dụng các yêu cầu và mã cho mô-đun của tôi trông giống như thế này:

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

Làm thế nào tôi có thể giả lập hurpdurpvì vậy tôi có thể kiểm tra đơn vị một cách hiệu quả?


Tôi chỉ đang thực hiện một số công cụ eval điên rồ trong node.js để mô phỏng definechức năng. Có một vài lựa chọn khác nhau mặc dù. Tôi sẽ đăng một câu trả lời với hy vọng nó sẽ hữu ích.
Jergason

1
Để thử nghiệm đơn vị với Jasmine, bạn cũng có thể muốn xem nhanh Jasq . [Tuyên bố miễn trừ trách nhiệm: Tôi đang duy trì lib]
biril

1
Nếu bạn đang kiểm tra trong nút env, bạn có thể sử dụng request-mock gói . Nó cho phép bạn dễ dàng chế nhạo các phụ thuộc của mình, thay thế các mô-đun, v.v ... Nếu bạn cần trình duyệt env với tải mô-đun async - bạn có thể thử Squire.js
ValeriiVasin

Câu trả lời:


64

Vì vậy, sau khi đọc bài đăng này, tôi đã đưa ra một giải pháp sử dụng chức năng cấu hình allowjs để tạo một bối cảnh mới cho bài kiểm tra của bạn, nơi bạn có thể đơn giản chế giễu các phụ thuộc của mình:

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

Vì vậy, nó tạo ra một bối cảnh mới trong đó các định nghĩa cho HurpDurpsẽ được đặt bởi các đối tượng bạn truyền vào hàm. Math.random cho tên có thể hơi bẩn nhưng nó hoạt động. Nguyên nhân là nếu bạn có một loạt các thử nghiệm, bạn cần tạo bối cảnh mới cho mọi bộ để ngăn chặn việc sử dụng lại các giả của bạn hoặc để tải các giả khi bạn muốn mô-đun yêu cầu thực sự.

Trong trường hợp của bạn, nó sẽ trông như thế này:

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

Vì vậy, tôi đang sử dụng phương pháp này trong sản xuất một thời gian và nó thực sự mạnh mẽ.


1
Tôi thích những gì bạn đang làm ở đây ... đặc biệt là vì bạn có thể tải một bối cảnh khác nhau cho mỗi bài kiểm tra. Điều duy nhất tôi ước tôi có thể thay đổi là có vẻ như nó chỉ hoạt động nếu tôi chế nhạo tất cả các phụ thuộc. Bạn có biết cách trả lại các đối tượng giả nếu chúng ở đó, nhưng dự phòng lấy từ tệp .js thực tế nếu không cung cấp giả? Tôi đã cố gắng tìm hiểu mã yêu cầu để tìm ra nó, nhưng tôi bị lạc một chút.
Glen Hughes

5
Nó chỉ chế giễu sự phụ thuộc mà bạn chuyển đến createContexthàm. Vì vậy, trong trường hợp của bạn nếu bạn chỉ truyền {hurp: 'hurp'}vào chức năng, durptệp sẽ được tải như một phụ thuộc bình thường.
Andreas Köberle

1
Tôi đang sử dụng điều này trong Rails (với jasminerice / ph Phantomjs) và đó là giải pháp tốt nhất tôi tìm thấy để chế giễu với RequireJS.
Ben Anderson

13
+1 Không đẹp, nhưng trong tất cả các giải pháp có thể, đây dường như là giải pháp ít xấu xí / lộn xộn nhất. Vấn đề này đáng được quan tâm hơn.
Chris Salzberg

1
Cập nhật: cho bất cứ ai xem xét giải pháp này, tôi khuyên bạn nên kiểm tra squire.js ( github.com/iammerrick/Squire.js ) được đề cập bên dưới. Đó là một triển khai tốt đẹp của một giải pháp tương tự như giải pháp này, tạo ra bối cảnh mới ở bất cứ nơi nào cần thiết.
Chris Salzberg

44

bạn có thể muốn kiểm tra lib Squire.js mới

từ các tài liệu:

Squire.js là một trình tiêm phụ thuộc cho người dùng Require.js để làm cho việc phụ thuộc dễ dàng!


2
Khuyến khích mạnh mẽ! Tôi đang cập nhật mã của mình để sử dụng squire.js và cho đến nay tôi thích nó rất nhiều. Mã rất rất đơn giản, không có phép thuật tuyệt vời dưới mui xe, nhưng được thực hiện theo cách (tương đối) dễ hiểu.
Chris Salzberg

1
Tôi đã có rất nhiều vấn đề với phía squire ảnh hưởng đến các thử nghiệm khác và không thể đề nghị nó. Tôi muốn giới thiệu npmjs.com/package/requirejs-mock
Jeff

17

Tôi đã tìm thấy ba giải pháp khác nhau cho vấn đề này, không có giải pháp nào dễ chịu cả.

Xác định nội tuyến phụ thuộc

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

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

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

Bỏ trốn. Bạn phải làm lộn xộn các bài kiểm tra của mình với nhiều bản tóm tắt AMD.

Đang tải phụ thuộc giả từ các đường dẫn khác nhau

Điều này liên quan đến việc sử dụng tệp config.js riêng để xác định đường dẫn cho từng phụ thuộc trỏ đến giả thay vì phụ thuộc ban đầu. Điều này cũng xấu, đòi hỏi phải tạo ra hàng tấn tệp thử nghiệm và tệp cấu hình.

Giả mạo trong nút

Đây là giải pháp hiện tại của tôi, nhưng vẫn là một giải pháp tồi tệ.

Bạn tạo definechức năng của riêng mình để cung cấp các mô phỏng của riêng bạn cho mô-đun và đưa các bài kiểm tra của bạn vào cuộc gọi lại. Sau đó, bạn evalmô-đun để chạy thử nghiệm của bạn, như vậy:

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

Đây là giải pháp ưa thích của tôi. Nó có vẻ hơi kỳ diệu, nhưng nó có một vài lợi ích.

  1. Chạy thử nghiệm của bạn trong nút, do đó, không gây rối với tự động hóa trình duyệt.
  2. Ít cần cho nồi hơi AMD lộn xộn trong các thử nghiệm của bạn.
  3. Bạn có thể sử dụng evaltrong sự tức giận và tưởng tượng Crockford bùng nổ với cơn thịnh nộ.

Nó vẫn còn một số nhược điểm, rõ ràng.

  1. Vì bạn đang kiểm tra trong nút, bạn không thể làm bất cứ điều gì với các sự kiện trình duyệt hoặc thao tác DOM. Chỉ tốt để kiểm tra logic.
  2. Vẫn còn một chút vụng về để thiết lập. Bạn cần phải giả định definetrong mọi bài kiểm tra, vì đó là nơi bài kiểm tra của bạn thực sự chạy.

Tôi đang làm việc trên một trình chạy thử nghiệm để đưa ra một cú pháp đẹp hơn cho loại công cụ này, nhưng tôi vẫn không có giải pháp tốt cho vấn đề 1.

Phần kết luận

Mocking deps trong Requjs hút cứng. Tôi đã tìm thấy một cách mà sortof hoạt động, nhưng tôi vẫn không hài lòng lắm với nó. Xin vui lòng cho tôi biết nếu bạn có bất kỳ ý tưởng tốt hơn.


15

Có một config.maptùy chọn http://requirejs.org/docs/api.html#config-map .

Về cách sử dụng nó:

  1. Xác định mô đun bình thường;
  2. Xác định mô đun còn sơ khai;
  3. Cấu hình RequireJS một cách rõ ràng;

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

Trong trường hợp này đối với mã thông thường và mã kiểm tra, bạn có thể sử dụng foomô-đun sẽ là tham chiếu mô-đun thực và sơ khai tương ứng.


Cách tiếp cận này thực sự hiệu quả với tôi. Trong trường hợp của tôi, tôi thêm này vào html của trang kiểm tra Á hậu -> bản đồ: { '*': { 'Common / Modules / usefulModule': '/Tests/Specs/Common/usefulModuleMock.js'}}
Aligned

9

Bạn có thể sử dụng testr.js để giả định phụ thuộc. Bạn có thể đặt testr để tải các phụ thuộc giả thay vì phụ thuộc ban đầu. Dưới đây là một ví dụ sử dụng:

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

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

Kiểm tra điều này là tốt: http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/


2
Tôi thực sự muốn testr.js hoạt động, nhưng nó vẫn chưa hoàn thành nhiệm vụ. Cuối cùng, tôi sẽ sử dụng giải pháp của @Andreas Köberle, sẽ bổ sung các bối cảnh lồng nhau vào các thử nghiệm của tôi (không đẹp) nhưng vẫn hoạt động ổn định. Tôi ước ai đó có thể tập trung giải quyết giải pháp này một cách tao nhã hơn. Tôi sẽ tiếp tục xem testr.js và nếu / khi nó hoạt động, sẽ thực hiện chuyển đổi.
Chris Salzberg

@shioyama hi, cảm ơn đã phản hồi! Tôi muốn xem cách bạn đã định cấu hình testr.js trong ngăn xếp thử nghiệm của mình. Rất vui được giúp bạn khắc phục mọi sự cố bạn có thể gặp phải! Ngoài ra còn có trang Các vấn đề github nếu bạn muốn đăng nhập một cái gì đó ở đó. Cảm ơn,
Matty F

1
@MattyF xin lỗi tôi thậm chí không nhớ ngay bây giờ lý do chính xác là testr.js không hoạt động với tôi, nhưng tôi đã đi đến kết luận rằng việc sử dụng các bối cảnh bổ sung thực sự khá ổn và thực tế là phù hợp với cách notify.js được sử dụng để chế nhạo / khai thác.
Chris Salzberg

2

Câu trả lời này dựa trên câu trả lời của Andreas Köberle .
Tôi không dễ dàng thực hiện và hiểu giải pháp của anh ấy, vì vậy tôi sẽ giải thích chi tiết hơn về cách thức hoạt động của nó và một số cạm bẫy cần tránh, hy vọng rằng nó sẽ giúp ích cho khách truy cập trong tương lai.

Vì vậy, trước hết là thiết lập:
Tôi đang sử dụng Karma làm người chạy thử và MochaJ làm khung thử nghiệm.

Sử dụng một cái gì đó như Squire không hiệu quả với tôi, vì một số lý do, khi tôi sử dụng nó, khung kiểm tra đã đưa ra các lỗi:

LoạiError: Không thể đọc thuộc tính 'cuộc gọi' không xác định

RequireJs có khả năng ánh xạ id mô-đun sang id mô-đun khác. Nó cũng cho phép tạo một requirechức năng sử dụng một cấu hình khác với toàn cầu require.
Những tính năng này rất quan trọng để giải pháp này hoạt động.

Đây là phiên bản mã giả của tôi, bao gồm (rất nhiều) ý kiến ​​(tôi hy vọng nó có thể hiểu được). Tôi bọc nó bên trong một mô-đun, để các bài kiểm tra có thể dễ dàng yêu cầu nó.

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

Các cạm bẫy lớn nhất tôi gặp phải, có nghĩa đen chi phí cho tôi giờ, đã tạo ra các RequireJs config. Tôi đã cố gắng (sâu) sao chép nó và chỉ ghi đè các thuộc tính cần thiết (như bối cảnh hoặc bản đồ). Điều này không hoạt động! Chỉ sao chép baseUrl, điều này hoạt động tốt.

Sử dụng

Để sử dụng nó, yêu cầu nó trong thử nghiệm của bạn, tạo các giả, và sau đó chuyển nó đến createMockRequire. Ví dụ:

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

Và đây là một ví dụ về một tệp thử nghiệm hoàn chỉnh :

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

nếu bạn muốn thực hiện một số thử nghiệm js đơn giản để cô lập một đơn vị, thì bạn chỉ cần sử dụng đoạn mã này:

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;
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.