다른 모듈이 필요한 Node.js 모듈을 단위 테스트하는 방법과 전역 요구 기능을 조롱하는 방법은 무엇입니까?


156

이것은 내 문제의 요점을 보여주는 사소한 예입니다.

var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.doComplexStuff();
}

module.exports = underTest;

이 코드에 대한 단위 테스트를 작성하려고합니다. 함수를 완전히 innerLib조롱하지 않고 요구 사항을 어떻게 조롱 할 수 require있습니까?

그래서 이것은 전 세계를 조롱하려고 시도 require하고 그렇게 할 수조차 없다는 것을 알게되었습니다.

var path = require('path'),
    vm = require('vm'),
    fs = require('fs'),
    indexPath = path.join(__dirname, './underTest');

var globalRequire = require;

require = function(name) {
    console.log('require: ' + name);
    switch(name) {
        case 'connect':
        case indexPath:
            return globalRequire(name);
            break;
    }
};

문제는 파일 require내부 의 함수 underTest.js가 실제로 조롱되지 않았다는 것입니다. 여전히 전역 require기능을 가리 킵니다 . 따라서 내가 조롱하고있는 require동일한 파일 내 에서만 함수를 조롱 할 수있는 것처럼 보입니다 . require로컬 복사본을 재정의 한 후에도 전역 을 사용하여 아무것도 포함하면 필요한 파일은 여전히 글로벌 require레퍼런스.


덮어 써야 global.require합니다. module모듈은 모듈 범위이므로 변수 는 기본적으로 씁니다 .
Raynos

@Raynos 어떻게해야합니까? global.require가 정의되어 있지 않습니까? 내 함수로 바꾸어도 다른 함수는 절대 사용하지 않습니까?
HMR

답변:


175

당신은 지금 할 수 있습니다!

테스트하는 동안 모듈 내부의 전역 요구를 무시하는 proxyquire 를 게시 했습니다.

이는 필요한 모듈에 대한 모의를 주입하기 위해 코드변경할 필요가 없음을 의미합니다 .

Proxyquire는 매우 간단한 API를 사용하여 테스트하려는 모듈을 해결하고 필요한 모듈에 대한 모의 / 스텁을 따라 한 번의 간단한 단계로 전달할 수 있습니다.

@Raynos는 전통적으로 그 목표를 달성하거나 대신 상향식 개발을하기 위해 이상적인 솔루션이 아닌 솔루션을 사용해야했습니다.

이것이 바로 프록시 요구 사항을 만든 주된 이유입니다. 번거 로움없이 하향식 테스트 중심 개발을 허용합니다.

필요에 맞는지 측정하기 위해 설명서와 예제를 살펴보십시오.


5
나는 proxyquire를 사용하고 충분한 좋은 말을 할 수 없습니다. 그것은 나를 구했다! 나는 일부 모듈을 절대 경로와 많은 순환 의존성으로 만드는 appcelerator Titanium에서 개발 된 앱에 대한 jasmine-node 테스트를 작성해야했습니다. proxyquire를 사용하면 이러한 차이를 막고 각 테스트에 필요하지 않은 틈새를 조롱 할 수 있습니다. ( 여기에 설명되어 있습니다 ). 너무 감사합니다!
Sukima

그 proxyquire을 듣고 행복 : 제대로 코드를 테스트 도움이
토르스텐 로렌츠에게

1
@ThorstenLorenz가 아주 좋았습니다. 사용중 proxyquire!
bevacqua 2015 년

환상적인! "당신은 할 수 없습니다"라는 대답을 보았을 때 나는 "오, 정말?!"라고 생각했습니다. 그러나 이것은 실제로 그것을 저장했습니다.
채드윅

3
Webpack을 사용하는 사람들은 proxyquire를 조사하는 데 시간을 소비하지 마십시오. Webpack을 지원하지 않습니다. 대신 인젝터 로더를 찾고 있습니다 ( github.com/plasticine/inject-loader ).
Artif3x 2016 년

116

이 경우 더 좋은 옵션은 반환되는 모듈의 메서드를 조롱하는 것입니다.

더 좋든 나쁘 든 대부분의 node.js 모듈은 싱글 톤입니다. 동일한 모듈을 필요로하는 두 개의 코드는 해당 모듈에 대한 동일한 참조를 얻습니다.

이것을 활용하고 sinon 과 같은 것을 사용 하여 필요한 항목을 조롱 할 수 있습니다. 모카 테스트는 다음과 같습니다.

// in your testfile
var innerLib  = require('./path/to/innerLib');
var underTest = require('./path/to/underTest');
var sinon     = require('sinon');

describe("underTest", function() {
  it("does something", function() {
    sinon.stub(innerLib, 'toCrazyCrap').callsFake(function() {
      // whatever you would like innerLib.toCrazyCrap to do under test
    });

    underTest();

    sinon.assert.calledOnce(innerLib.toCrazyCrap); // sinon assertion

    innerLib.toCrazyCrap.restore(); // restore original functionality
  });
});

Sinon은 어설 션을 만들기 위해 chai와통합되어 있으며 , 간첩과 스텁을보다 쉽게 청소할 수 있도록 sinon과 mocha통합 하는 모듈을 작성했습니다 (테스트 오염 방지).

underTest는 함수 만 반환하므로 동일한 방법으로 underTest를 조롱 할 수 없습니다.

또 다른 옵션은 Jest mock을 사용하는 것입니다. 그들의 페이지 에 후속


1
불행히도, node.js 모듈은 여기에 설명 된 것처럼 싱글 톤임을 보장하지 않습니다 : justjs.com/posts/…
FrontierPsycho

4
@FrontierPsycho 몇 가지 사항 : 첫째, 테스트와 관련하여 기사는 관련이 없습니다. 모든 코드가 require('some_module')동일한 node_modules 디렉토리를 공유하기 때문에 종속성 (종속성의 종속성이 아닌)을 테스트하는 한 모든 코드는 동일한 객체를 다시 가져옵니다 . 둘째,이 기사는 직교하는 싱글 톤으로 네임 스페이스를 병합합니다. 셋째, 그 기사는 꽤 오래되어서 (node.js에 관한 한) 당시에 유효했던 것이 현재 유효하지 않을 수 있습니다.
Elliot Foster

2
흠. 우리 중 한 사람이 실제로 어떤 점을 입증하는 코드를 파기하지 않는 한 의존성 주입 솔루션을 사용하거나 단순히 객체를 전달하는 것이 더 안전하고 미래의 증거입니다.
FrontierPsycho

1
무엇을 증명해야할지 모르겠습니다. 노드 모듈의 단일 (캐시 된) 특성은 일반적으로 이해됩니다. 의존성 주입은 좋은 경로이지만, 더 많은 보일러 플레이트와 더 많은 코드가 될 수 있습니다. DI는 정적으로 형식이 지정된 언어에서 더 일반적으로 스파이 / 스텁 / 모의 코드를 코드에 오리 펀치하기가 더 어렵습니다. 지난 3 년간 수행 한 여러 프로젝트는 위의 답변에 설명 된 방법을 사용합니다. 조금씩 사용하지만 모든 방법 중 가장 쉬운 방법입니다.
Elliot Foster

1
sinon.js를 읽으십시오. sinon을 사용하는 경우 (위의 예에서와 같이) 스텁의 동작 방식을 변경할 수있는 innerLib.toCrazyCrap.restore()sinon을 호출하고 호출 합니다. 또한, rewire는 위 의 확장 과 거의 같은 것으로 보입니다 . sinon.stub(innerLib, 'toCrazyCrap')innerLib.toCrazyCrap.returns(false)proxyquire
Elliot Foster

11

mock-require 사용 합니다 . require테스트 할 모듈 전에 모의 객체를 정의해야합니다 .


또한 stop (<file>) 또는 stopAll ()을 수행하여 모의를 원하지 않는 테스트에서 캐시 된 파일을 얻지 않는 것이 좋습니다.
Justin Kruse

1
이것은 톤을 도왔다.
wallop

2

조롱 require은 나에게 심한 해킹처럼 느껴집니다. 나는 개인적으로 그것을 피하고 코드를 리팩터링하여 더 테스트 가능하게 만들려고합니다. 종속성을 처리하는 다양한 방법이 있습니다.

1) 의존성을 인수로 전달

function underTest(innerLib) {
    return innerLib.doComplexStuff();
}

이렇게하면 코드를 보편적으로 테스트 할 수 있습니다. 단점은 종속성을 전달해야하므로 코드가 더 복잡해 보일 수 있다는 것입니다.

2) 모듈을 클래스로 구현 한 다음 클래스 메소드 / 속성을 사용하여 종속성을 얻습니다.

(이것은 클래스 사용이 합리적이지 않지만 아이디어를 전달하는 고안된 예입니다.) (ES6 예)

const innerLib = require('./path/to/innerLib')

class underTestClass {
    getInnerLib () {
        return innerLib
    }

    underTestMethod () {
        return this.getInnerLib().doComplexStuff()
    }
}

이제 getInnerLib코드를 테스트하는 방법을 쉽게 스텁 할 수 있습니다 . 코드가 더 장황 해지지 만 테스트하기도 더 쉽습니다.


1
나는 당신이 추정하는 것처럼 해키라고 생각하지 않습니다 ... 이것은 조롱의 본질입니다. 필요한 종속성을 조롱하면 코드 구조를 변경하지 않고도 개발자에게 제어권을 부여 할 수 있습니다. 메소드가 너무 장황하므로 추론하기가 어렵습니다. 나는 이것보다 proxyrequire 또는 mock-require를 선택한다. 나는 여기에 어떤 문제도 보지 못했다. 코드는 깨끗하고 추론하기 쉽고 이미 이것을 읽는 대부분의 사람들은 복잡한 코드를 작성했습니다. 이 라이브러리가 해킹 된 경우 조롱 및 스터 빙도 정의에 따라 해킹되므로 중지해야합니다.
Emmanuel Mahuni

1
접근법 1의 문제점은 내부 구현 세부 사항을 스택으로 전달한다는 것입니다. 다중 레이어를 사용하면 모듈 소비자가되기가 훨씬 더 복잡해집니다. 접근 방식과 같은 IOC 컨테이너와 함께 작동 할 수 있으므로 종속성이 자동으로 주입되지만 imports 문을 통해 노드 모듈에 종속성이 주입되어 있으므로 해당 수준에서이를 모의 할 수 있습니다. .
magritte

1) 이것은 단순히 문제를 다른 파일로 옮기는 것입니다. 2) 여전히 다른 모듈을로드하여 성능 오버 헤드를 발생시키고 부작용을 일으킬 수 있습니다 (예 : 인기있는 colors모듈 과 같은 String.prototype)
ThomasR

2

jest를 사용해 본 적이 있다면 아마도 jest의 모의 기능에 익숙 할 것입니다.

"jest.mock (...)"을 사용하면 코드의 require 문에서 발생할 문자열을 지정하고 해당 문자열을 사용하여 모듈이 필요할 때마다 mock-object가 반환됩니다.

예를 들어

jest.mock("firebase-admin", () => {
    const a = require("mocked-version-of-firebase-admin");
    a.someAdditionalMockedMethod = () => {}
    return a;
})

"firebase-admin"의 모든 가져 오기 / 요구 사항을 "공장"기능에서 반환 한 객체로 완전히 대체합니다.

jest를 사용하면 jest가 실행하는 모든 모듈 주위에 런타임을 생성하고 모듈에 "hooked"버전의 require를 주입하기 때문에 jest를 사용할 때 그렇게 할 수 있지만 jest 없이는이를 수행 할 수 없습니다.

나는 mock-require로 이것을 달성하려고 노력했지만 내 소스의 중첩 된 레벨에서는 작동하지 않았다. github에서 다음 문제를 살펴보십시오. mock-require가 항상 Mocha와 함께 호출되는 것은 아닙니다 .

이 문제를 해결하기 위해 원하는 것을 달성하는 데 사용할 수있는 두 개의 npm 모듈을 만들었습니다.

하나의 babel-plugin과 모듈 mocker가 필요합니다.

.babelrc에서 다음 옵션과 함께 babel-plugin-mock-require 플러그인을 사용하십시오.

...
"plugins": [
        ["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
        ...
]
...

테스트 파일에서 다음과 같이 jestlike-mock 모듈을 사용하십시오.

import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
            const firebase = new (require("firebase-mock").MockFirebaseSdk)();
            ...
            return firebase;
});
...

jestlike-mock모듈은 여전히 ​​초보적이며 많은 문서가 없지만 코드는 많지 않습니다. 보다 완전한 기능 세트에 대한 모든 PR에 감사드립니다. 목표는 전체 "jest.mock"기능을 재생성하는 것입니다.

jest가이를 구현하는 방법을보기 위해 "jest-runtime"패키지에서 코드를 찾을 수 있습니다. 예를 들어 https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734 를 참조 하십시오 . 여기에서 모듈의 "자동"을 생성합니다.

희망이 도움이됩니다;)


1

당신은 할 수 없습니다. 가장 낮은 모듈을 먼저 테스트하고 모듈이 필요한 높은 수준의 모듈을 나중에 테스트 할 수 있도록 장치 테스트 스위트를 구축해야합니다.

또한 타사 코드 및 node.js 자체가 잘 테스트되었다고 가정해야합니다.

나는 당신이 조롱 프레임 워크가 가까운 미래에 도착하는 것을 보게 될 것이라고 가정합니다. global.require

실제로 모형을 주입해야하는 경우 모듈 식 범위를 노출하도록 코드를 변경할 수 있습니다.

// underTest.js
var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.toCrazyCrap();
}

module.exports = underTest;
module.exports.__module = module;

// test.js
function test() {
    var underTest = require("underTest");
    underTest.__module.innerLib = {
        toCrazyCrap: function() { return true; }
    };
    assert.ok(underTest());
}

이것은 .__moduleAPI에 노출 되며 모든 코드는 위험에 따라 모듈 식 범위에 액세스 할 수 있습니다.


2
타사 코드가 제대로 테스트되었다고 가정하는 것이 IMO를 작동시키는 좋은 방법이 아닙니다.
henry.oswald

5
@beck 그것은 일하기 좋은 방법입니다. 그것은 높은 품질의 타사 코드 만 일 당신을 강제로 또는 모든 의존성이 아니라 테스트 그래서 코드의 모든 조각 쓰기
Raynos

Ok 귀하의 코드와 타사 코드 사이에 통합 테스트를 수행하지 않는다고 언급했다고 생각했습니다. 동의했다.
henry.oswald

1
"단위 테스트 스위트"는 단위 테스트 모음 일 뿐이지 만 단위 테스트는 서로 독립적이어야하므로 단위 테스트 단위입니다. 사용하기 위해서는 단위 테스트가 빠르고 독립적이어야하므로 단위 테스트가 실패 할 때 코드가 어디에 있는지 명확하게 알 수 있습니다.
Andreas Berheim Brudin

이것은 나를 위해 작동하지 않았습니다. 모듈 객체는 "var innerLib ..."등을 노출시키지 않습니다.
AnitKryst

1

조롱 라이브러리 를 사용할 수 있습니다 :

describe 'UnderTest', ->
  before ->
    mockery.enable( warnOnUnregistered: false )
    mockery.registerMock('./path/to/innerLib', { doComplexStuff: -> 'Complex result' })
    @underTest = require('./path/to/underTest')

  it 'should compute complex value', ->
    expect(@underTest()).to.eq 'Complex result'

1

호기심을 위해 모듈을 조롱하는 간단한 코드

비밀 소스이므로 조작하는 부분 require.cache과 노트 require.resolve방법을 확인하십시오.

class MockModules {  
  constructor() {
    this._resolvedPaths = {} 
  }
  add({ path, mock }) {
    const resolvedPath = require.resolve(path)
    this._resolvedPaths[resolvedPath] = true
    require.cache[resolvedPath] = {
      id: resolvedPath,
      file: resolvedPath,
      loaded: true,
      exports: mock
    }
  }
  clear(path) {
    const resolvedPath = require.resolve(path)
    delete this._resolvedPaths[resolvedPath]
    delete require.cache[resolvedPath]
  }
  clearAll() {
    Object.keys(this._resolvedPaths).forEach(resolvedPath =>
      delete require.cache[resolvedPath]
    )
    this._resolvedPaths = {}
  }
}

다음과 같이 사용하십시오 .

describe('#someModuleUsingTheThing', () => {
  const mockModules = new MockModules()
  beforeAll(() => {
    mockModules.add({
      // use the same require path as you normally would
      path: '../theThing',
      // mock return an object with "theThingMethod"
      mock: {
        theThingMethod: () => true
      }
    })
  })
  afterAll(() => {
    mockModules.clearAll()
  })
  it('should do the thing', async () => {
    const someModuleUsingTheThing = require('./someModuleUsingTheThing')
    expect(someModuleUsingTheThing.theThingMethod()).to.equal(true)
  })
})

그러나 ... proxyquire는 정말 대단합니다. 테스트에만 국한된 필수 재정의를 유지하며 강력히 권장합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.