ฉันจะจำลองการอ้างอิงสำหรับการทดสอบหน่วยใน RequireJS ได้อย่างไร


127

ฉันมีโมดูล AMD ที่ฉันต้องการทดสอบ แต่ฉันต้องการจำลองการอ้างอิงแทนการโหลดการอ้างอิงจริง ฉันใช้ requirejs และรหัสสำหรับโมดูลของฉันมีลักษณะดังนี้:

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

ฉันจะเยาะเย้ยออกhurpและdurpเพื่อให้ฉันได้อย่างมีประสิทธิภาพการทดสอบหน่วย?


ฉันแค่ทำสิ่งที่บ้าคลั่งใน node.js เพื่อล้อเลียนdefineฟังก์ชัน มีตัวเลือกที่แตกต่างกันเล็กน้อย ฉันจะโพสต์คำตอบโดยหวังว่าจะเป็นประโยชน์
jergason

1
สำหรับหน่วยทดสอบกับจัสมินคุณยังอาจต้องการที่จะดูอย่างรวดเร็วที่Jasq [Disclaimer: ฉันกำลังดูแล lib]
biril

1
หากคุณกำลังทดสอบใน node env คุณสามารถใช้need-mock package ช่วยให้คุณสามารถจำลองการอ้างอิงของคุณได้อย่างง่ายดายเปลี่ยนโมดูล ฯลฯ หากคุณต้องการเบราว์เซอร์ env พร้อมโหลดโมดูล async คุณสามารถลองใช้Squire.js
ValeriiVasin

คำตอบ:


64

ดังนั้นหลังจากอ่านโพสต์นี้ฉันได้พบกับโซลูชันที่ใช้ฟังก์ชันกำหนดค่า 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/'
  });
}

ดังนั้นจึงสร้างบริบทใหม่ที่คำจำกัดความสำหรับHurpและDurpจะถูกกำหนดโดยวัตถุที่คุณส่งผ่านไปยังฟังก์ชัน Math.random สำหรับชื่ออาจจะสกปรกไปหน่อย แต่ก็ใช้งานได้ ทำให้หากคุณมีการทดสอบจำนวนมากคุณจำเป็นต้องสร้างบริบทใหม่สำหรับทุกชุดเพื่อป้องกันการนำล้อเลียนของคุณกลับมาใช้ใหม่หรือเพื่อโหลดล้อเลียนเมื่อคุณต้องการโมดูลความต้องการที่แท้จริง

ในกรณีของคุณจะมีลักษณะดังนี้:

(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 จริงหากไม่มีการจำลอง ฉันพยายามขุดคุ้ยรหัสที่ต้องการเพื่อหามัน แต่ฉันหลงทางนิดหน่อย
Glen Hughes

5
เพียงแค่ล้อเลียนการพึ่งพาที่คุณส่งผ่านไปยังcreateContextฟังก์ชันเท่านั้น ดังนั้นในกรณีของคุณหากคุณส่งผ่าน{hurp: 'hurp'}ไปยังฟังก์ชันเท่านั้นdurpไฟล์จะถูกโหลดเป็นการอ้างอิงตามปกติ
Andreas Köberle

1
ฉันใช้สิ่งนี้ใน Rails (กับ jasminerice / phantomjs) และเป็นทางออกที่ดีที่สุดที่ฉันพบในการล้อเลียนกับ RequireJS
Ben Anderson

13
+1 ไม่สวย แต่จากวิธีแก้ปัญหาที่เป็นไปได้ทั้งหมดนี้ดูเหมือนจะเป็นวิธีที่น่าเกลียด / ยุ่งน้อยที่สุด ปัญหานี้สมควรได้รับความสนใจมากขึ้น
Chris Salzberg

1
อัปเดต: สำหรับทุกคนที่พิจารณาวิธีแก้ปัญหานี้ฉันขอแนะนำให้ตรวจสอบ squire.js ( github.com/iammerrick/Squire.js ) ที่กล่าวถึงด้านล่าง เป็นการใช้งานโซลูชันที่คล้ายกับโซลูชันนี้ได้ดีโดยสร้างบริบทใหม่ทุกที่ที่ต้องการต้นขั้ว
Chris Salzberg

44

คุณอาจต้องการตรวจสอบSquire.js libใหม่

จากเอกสาร:

Squire.js เป็นตัวฉีดการพึ่งพาสำหรับผู้ใช้ Require.js เพื่อให้การอ้างอิงเยาะเย้ยเป็นเรื่องง่าย!


2
ขอแนะนำ! ฉันกำลังอัปเดตโค้ดเพื่อใช้ squire.js และจนถึงตอนนี้ฉันก็ชอบมันมาก รหัสที่ง่ายมากไม่มีเวทมนตร์ที่ยอดเยี่ยมภายใต้ประทุน แต่ทำในแบบที่ (ค่อนข้าง) เข้าใจง่าย
Chris Salzberg

1
ฉันมีปัญหามากมายเกี่ยวกับผลข้างเคียงของ squire ที่มีผลต่อการทดสอบอื่น ๆ และไม่สามารถแนะนำได้ ฉันอยากจะแนะนำnpmjs.com/package/requirejs-mock
Jeff Whiting

17

ฉันพบวิธีแก้ปัญหาที่แตกต่างกันสามวิธีสำหรับปัญหานี้ แต่ไม่มีวิธีใดที่น่าพอใจ

การกำหนดค่าอ้างอิงแบบอินไลน์

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

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

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

fugly คุณต้องยุ่งกับการทดสอบของคุณด้วยแผ่นสำเร็จรูปของ AMD จำนวนมาก

กำลังโหลดการอ้างอิงจำลองจากเส้นทางที่แตกต่างกัน

สิ่งนี้เกี่ยวข้องกับการใช้ไฟล์ config.js แยกต่างหากเพื่อกำหนดพา ธ สำหรับการอ้างอิงแต่ละรายการที่ชี้ไปที่ mocks แทนการอ้างอิงดั้งเดิม สิ่งนี้น่าเกลียดเช่นกันที่ต้องสร้างไฟล์ทดสอบและไฟล์คอนฟิกูเรชันจำนวนมาก

ปลอมในโหนด

นี่เป็นวิธีแก้ปัญหาปัจจุบันของฉัน แต่ก็ยังแย่มาก

คุณสร้าง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ความโกรธและลองนึกภาพว่า Crockford ระเบิดด้วยความโกรธ

ยังคงมีข้อบกพร่องบางประการอย่างเห็นได้ชัด

  1. เนื่องจากคุณกำลังทดสอบในโหนดคุณจึงไม่สามารถทำอะไรกับเหตุการณ์ของเบราว์เซอร์หรือการจัดการ DOM ได้ ใช้ได้ดีสำหรับการทดสอบตรรกะเท่านั้น
  2. ยังคงเป็นเรื่องยุ่งยากเล็กน้อยในการตั้งค่า คุณต้องเยาะเย้ยdefineในการทดสอบทุกครั้งเพราะนั่นคือจุดที่การทดสอบของคุณดำเนินไปอย่างแท้จริง

ฉันกำลังทำงานกับนักวิ่งทดสอบเพื่อให้ไวยากรณ์ที่ดีกว่าสำหรับสิ่งประเภทนี้ แต่ฉันยังไม่มีทางออกที่ดีสำหรับปัญหา 1

ข้อสรุป

Mocking deps ใน requirejs ดูดยาก ฉันพบวิธีที่การเรียงลำดับได้ผล แต่ฉันก็ยังไม่พอใจกับมันมากนัก โปรดแจ้งให้เราทราบหากคุณมีแนวคิดที่ดีกว่านี้


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 ของหน้านักวิ่งทดสอบ -> map: {'*': {'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 ทำงานจริงๆ แต่มันยังไม่รู้สึกถึงงาน ในท้ายที่สุดฉันจะใช้โซลูชันของ @Andreas Köberleซึ่งจะเพิ่มบริบทที่ซ้อนกันให้กับการทดสอบของฉัน (ไม่สวย) แต่ก็ใช้ได้ผลอย่างสม่ำเสมอ ฉันหวังว่าใครบางคนสามารถมุ่งเน้นไปที่การแก้ไขโซลูชันนี้ด้วยวิธีที่หรูหรากว่านี้ ฉันจะคอยดู testr.js และถ้า / เมื่อมันใช้งานได้จะทำการเปลี่ยน
Chris Salzberg

@shioyama สวัสดีขอบคุณสำหรับคำติชม! ฉันชอบที่จะดูวิธีที่คุณกำหนดค่า testr.js ภายในกองทดสอบของคุณ ยินดีที่จะช่วยคุณแก้ไขปัญหาที่คุณอาจพบ! นอกจากนี้ยังมีหน้าปัญหา github หากคุณต้องการบันทึกบางสิ่งที่นั่น ขอบคุณ
Matty F

1
@MattyF ขอโทษตอนนี้ฉันจำไม่ได้แล้วว่าเหตุผลที่แท้จริงคืออะไร testr.js ไม่ได้ผลสำหรับฉัน แต่ฉันได้ข้อสรุปว่าการใช้บริบทเพิ่มเติมนั้นค่อนข้างดีและในความเป็นจริง ด้วยวิธีการที่ require.js ถูกนำมาใช้เพื่อการเยาะเย้ย / การลอกแบบ
Chris Salzberg

2

คำตอบนี้จะขึ้นอยู่กับคำตอบของแอนเดรีKöberle
มันไม่ใช่เรื่องง่ายสำหรับฉันที่จะปรับใช้และทำความเข้าใจโซลูชันของเขาดังนั้นฉันจะอธิบายรายละเอียดเพิ่มเติมเล็กน้อยว่ามันทำงานอย่างไรและข้อผิดพลาดบางอย่างที่ควรหลีกเลี่ยงหวังว่ามันจะช่วยผู้เยี่ยมชมในอนาคตได้

ก่อนอื่นการตั้งค่า:
ฉันใช้Karmaเป็นนักวิ่งทดสอบและMochaJsเป็นกรอบการทดสอบ

การใช้บางอย่างเช่นSquireไม่ได้ผลสำหรับฉันด้วยเหตุผลบางประการเมื่อฉันใช้มันกรอบการทดสอบทำให้เกิดข้อผิดพลาด:

TypeError: ไม่สามารถอ่านคุณสมบัติ 'call' ของ undefined

RequireJsมีความเป็นไปได้ในการแมปรหัสโมดูลกับรหัสโมดูลอื่น ๆ นอกจากนี้ยังช่วยในการสร้าง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ซึ่งใช้งานได้ดี

การใช้

ที่จะใช้มันจำเป็นต้องใช้ในการทดสอบของคุณสร้าง mocks 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

หากคุณต้องการทำการทดสอบ 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.