RequireJS에서 단위 테스트에 대한 종속성을 어떻게 조롱 할 수 있습니까?


127

테스트하려는 AMD 모듈이 있지만 실제 종속성을로드하는 대신 해당 종속성을 모방하려고합니다. requirejs를 사용하고 있으며 모듈 코드는 다음과 같습니다.

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

어떻게 밖으로 조롱 수 hurpdurp내가 단위 테스트를 효과적으로 할 수 있도록?


node.js에서 미친 평가 자료를 사용하여 define함수 를 조롱하고 있습니다. 그래도 몇 가지 다른 옵션이 있습니다. 도움이되기를 바랍니다.
jergason

1
Jasmine을 사용한 단위 테스트의 경우 Jasq를 빠르게 살펴볼 수도 있습니다 . [면책 조항 : 나는 lib를 유지하고있다]
biril

1
노드 환경에서 테스트하는 경우 require-mock 패키지를 사용할 수 있습니다 . 그것은 당신이 쉽게 당신의 의존성을 조롱하고, 모듈 등을 대체 할 수있게합니다. 비동기 모듈로드를 가진 브라우저 env가 필요하다면-당신은 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은 약간 더러울 수 있지만 작동합니다. 많은 테스트를 해야하는 경우 모의 재사용을 방지하거나 실제 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 파일에서 검색하는 방법으로 대체됩니까? 요구 코드를 통해 알아 내려고 노력했지만 조금 길을 잃었습니다.
Glen Hughes

5
createContext함수에 전달하는 종속성 만 조롱합니다 . 따라서 귀하의 경우 {hurp: 'hurp'}함수 에만 전달 하면 durp파일이 정상적인 종속성으로로드됩니다.
Andreas Köberle

1
나는 이것을 Jasminerice / phantomjs와 함께 Rails에서 사용하고 있으며 RequireJS로 조롱하기 위해 찾은 최고의 솔루션이었습니다.
벤 앤더슨

13
+1 예쁘지는 않지만 가능한 모든 해결책 중 가장 추악하고 지저분한 것 같습니다. 이 문제는 더주의를 기울여야합니다.
Chris Salzberg

1
업데이트 :이 솔루션을 고려하는 사람에게는 아래 언급 된 squire.js ( github.com/iammerrick/Squire.js )를 확인하는 것이 좋습니다 . 이 솔루션과 비슷한 솔루션을 훌륭하게 구현하여 스텁이 필요한 곳마다 새로운 컨텍스트를 만듭니다.
Chris Salzberg

44

새로운 Squire.js 라이브러리 를 확인하고 싶을 수도 있습니다.

문서에서 :

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

못 생겼어 많은 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에서 조롱하는 사람들은 열심히 짜증납니다. 나는 일종의 작동 방식을 찾았지만 여전히 그것에 만족하지 않습니다. 더 좋은 아이디어가 있으면 알려주세요.


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이 추가 ->지도 : { '*': { '공통 / 모듈 / 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 이슈 페이지도 있습니다. 감사합니다,
매티 F

1
@MattyF 죄송합니다. testr.js가 저에게 효과가 없었던 정확한 이유를 지금까지도 기억하지 못하지만 추가 컨텍스트를 사용하는 것이 실제로 아주 옳고 사실이라는 결론에 도달했습니다. require.js가 조롱 / 스터 빙에 사용되는 방법.
Chris Salzberg

2

이 답변은 Andreas Köberle의 답변을 기반으로 합니다.
그의 솔루션을 구현하고 이해하는 것은 쉽지 않았으므로 앞으로의 방문자에게 도움이되기를 바라면서 어떻게 작동하는지, 피해야 할 함정을 좀 더 자세히 설명하겠습니다.

그래서, 우선 설정 :
내가 사용 카르마를 테스트 러너와 같은 MochaJs 테스트 프레임 워크로.

Squire 와 같은 것을 사용하면 나에게 효과가 없었습니다. 어떤 이유로 그것을 사용했을 때 테스트 프레임 워크에서 오류가 발생했습니다.

TypeError : 정의되지 않은 'call'속성을 읽을 수 없습니다

RequireJs 는 모듈 ID를 다른 모듈 ID 에 맵핑 할 수 있습니다 . 또한 전역 과 다른 구성 을 사용하는 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제대로 작동합니다.

용법

사용하려면 테스트에서 요구하고 모형을 만든 다음에 전달하십시오 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.