development

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

big-blog 2020. 6. 14. 09:39
반응형

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


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

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레퍼런스.


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

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

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

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

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

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

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


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

더 좋든 나쁘 든 대부분의 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을 사용하는 것입니다. 그들의 페이지 에 후속


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


조롱 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코드를 테스트하는 방법을 쉽게 스텁 할 수 있습니다 . 코드가 더 장황 해지지 만 테스트하기도 더 쉽습니다.


You can't. You have to build up your unit test suite so that the lowest modules are tested first and that the higher level modules that require modules are tested afterwards.

You also have to assume that any 3rd party code and node.js itself is well tested.

I presume you'll see mocking frameworks arrive in the near future that overwrite global.require

If you really must inject a mock you can change your code to expose modular scope.

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

Be warned this exposes .__module into your API and any code can access modular scope at their own danger.


You can use mockery library:

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'

Simple code to mock modules for the curious

Notice the parts where you manipulate the require.cache and note require.resolve method as this is the secret sauce.

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 = {}
  }
}

Use like:

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

BUT... proxyquire is pretty awesome and you should use that. It keeps your require overrides localized to tests only and I highly recommend it.


If you've ever used jest, then you're probably familiar with jest's mock feature.

Using "jest.mock(...)" you can simply specify the string that would occur in a require-statement in your code somewhere and whenever a module is required using that string a mock-object would be returned instead.

For example

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

would completely replace all imports/requires of "firebase-admin" with the object you returned from that "factory"-function.

Well, you can do that when using jest because jest creates a runtime around every module it runs and injects a "hooked" version of require into the module, but you wouldn't be able to do this without jest.

I have tried to achieve this with mock-require but for me it didn't work for nested levels in my source. Have a look at the following issue on github: mock-require not always called with Mocha.

To address this I have created two npm-modules you can use to achieve what you want.

You need one babel-plugin and a module mocker.

In your .babelrc use the babel-plugin-mock-require plugin with following options:

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

and in your test file use the jestlike-mock module like so:

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

The jestlike-mock module is still very rudimental and does not have a lot of documentation but there's not much code either. I appreciate any PRs for a more complete feature set. The goal would be to recreate the whole "jest.mock" feature.

In order to see how jest implements that one can look up the code in the "jest-runtime" package. See https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734 for example, here they generate an "automock" of a module.

Hope that helps ;)

참고URL : https://stackoverflow.com/questions/5747035/how-to-unit-test-a-node-js-module-that-requires-other-modules-and-how-to-mock-th

반응형