ES6 모듈의 수입을 조롱하는 방법?


141

다음과 같은 ES6 모듈이 있습니다.

network.js

export function getDataFromServer() {
  return ...
}

widget.js

import { getDataFromServer } from 'network.js';

export class Widget() {
  constructor() {
    getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }

  render() {
    ...
  }
}

의 모의 인스턴스로 위젯을 테스트하는 방법을 찾고 getDataFromServer있습니다. <script>Karma와 같이 ES6 모듈 대신 별도 의 s를 사용하면 다음 과 같이 테스트를 작성할 수 있습니다.

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(window, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

그러나 브라우저 외부에서 개별적으로 ES6 모듈을 테스트하는 경우 (Mocha + babel과 같은) 다음과 같이 작성합니다.

import { Widget } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(?????) // How to mock?
    .andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

좋아, 그러나 지금 getDataFromServer은 사용할 수 window없으며 (아무도 없다 window), 나는 물건을 widget.js자신의 범위에 직접 주입하는 방법을 모른다 .

여기서 어디로 가야합니까?

  1. 의 범위에 액세스 widget.js하거나 가져 오기를 내 코드로 바꾸는 방법이 있습니까?
  2. 그렇지 않다면 어떻게 Widget테스트 할 수 있습니까?

내가 고려한 것들 :

ㅏ. 수동 의존성 주입.

모든 가져 오기를 제거 widget.js하고 발신자가 뎁스를 제공 할 것으로 예상합니다.

export class Widget() {
  constructor(deps) {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}

위젯의 공용 인터페이스를 엉망으로 만들고 구현 세부 정보를 노출하는 것이 매우 불편합니다. 안돼


비. 수입품을 조롱 할 수 있도록 노출하십시오.

다음과 같은 것 :

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

export class Widget() {
  constructor() {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}

그때:

import { Widget, deps } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(deps.getDataFromServer)  // !
      .andReturn("mockData");
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

이것은 덜 침습적이지만 각 모듈에 대해 많은 상용구를 작성해야하며 항상 getDataFromServer대신 사용하는 위험 deps.getDataFromServer이 있습니다. 나는 그것에 대해 불안하지만 그것이 지금까지 나의 최고의 아이디어입니다.


이런 종류의 가져 오기에 대한 기본 모의 지원 이 없다면 ES6 스타일 가져 오기를 사용자 정의 모의 가능한 가져 오기 시스템으로 변환하는 babel 용 자체 변환기를 작성하는 것을 생각할 것입니다. 이것은 확실히 가능한 실패의 또 다른 계층을 추가하고 테스트하려는 코드를 변경합니다 ...
t.niese

지금 테스트 스위트를 설정할 수는 없지만 'network.js'모듈에서 getDataFromServer에 대한 가져온 참조와 함께 jasmin createSpy( github.com/jasmine/jasmine/blob/… ) 함수 를 사용하려고 합니다. 따라서 위젯 테스트 파일에서 getDataFromServer를 가져온 다음let spy = createSpy('getDataFromServer', getDataFromServer)
Microfed

두 번째 추측은 함수가 아닌 'network.js'모듈에서 객체를 반환하는 것입니다. 그런 식으로 모듈 spyOn에서 가져온 해당 객체 를 사용할 수 network.js있습니다. 항상 같은 객체에 대한 참조입니다.
Microfed

실제로, 그것은 이미 내가 볼 수있는 것으로부터 이미 물체입니다 : babeljs.io/repl/…
Microfed

2
의존성 주입이 Widget공용 인터페이스를 어떻게 망칠 지 이해가되지 않습니까? 없이Widget 엉망 입니다. 왜 의존성을 명시 적으로 나타내지 않습니까? deps
thebearingedge

답변:


129

import * as obj테스트 내 에서 스타일을 사용하기 시작했습니다 . 모듈에서 모든 내보내기를 개체의 속성으로 가져온 다음 조롱 할 수 있습니다. 나는 rewire 또는 proxyquire 또는 유사한 기술을 사용하는 것보다 훨씬 깨끗하다는 것을 알았습니다. 예를 들어 Redux 작업을 조롱해야 할 때이 작업을 가장 자주 수행했습니다. 위의 예제에 사용할 수있는 내용은 다음과 같습니다.

import * as network from 'network.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(network, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

함수는 기본 수출 될 일 경우, import * as network from './network'생산 것입니다 {default: getDataFromServer}그리고 당신은 network.default 조롱 수 있습니다.


3
import * as obj테스트 또는 일반 코드에서만 사용 합니까?
Chau Thai

36
@carpeliam 가져 오기가 읽기 전용 인 ES6 모듈 사양에서는 작동하지 않습니다.
ashish

7
Jasmine은 [method_name] is not declared writable or has no setteres6 수입이 일정하기 때문에 이치에 맞습니다. 해결 방법이 있습니까?
lpan

2
@Francisc import(와 달리 require어디든지 갈 수 있음)가 게양되어 기술적으로 여러 번 가져올 수 없습니다. 스파이가 다른 곳에서 불려 오는 것 같습니까? 테스트가 엉망인 상태 (테스트 오염)를 막기 위해 afterEach (예 : sinon.sandbox)에서 스파이를 재설정 할 수 있습니다. Jasmine 나는 이것을 자동으로한다고 믿는다.
carpeliam

10
@ agent47 문제는 ES6 사양이 구체적으로 언급 한대로이 답변이 작동하지 못하게 import하지만 JS 를 작성하는 대부분의 사람들 은 실제로 ES6 모듈을 사용하지 않는다는 것입니다. 웹팩이나 바벨과 같은 뭔가 빌드 시간에 단계 및 코드 (예를 들어 먼 부분을 호출하기위한 자신의 내부 메커니즘에 하나를 변환합니다 __webpack_require__) 또는 사전 ES6 중 하나에 사실상의 표준, CommonJS, AMD 또는 UMD. 그리고 그 변환은 종종 사양을 엄격하게 준수하지 않습니다. 그래서 지금 많은 개발자들 에게이 대답은 잘 작동합니다. 지금은
daemonexmachina

31

@carpeliam은 정확하지만 모듈의 함수를 감시하고 해당 함수를 호출하는 해당 모듈의 다른 함수를 사용하려면 내보내기 네임 스페이스의 일부로 해당 함수를 호출해야합니다. 그렇지 않으면 스파이가 사용되지 않습니다.

잘못된 예 :

// mymodule.js

export function myfunc2() {return 2;}
export function myfunc1() {return myfunc2();}

// tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // out will still be 2
    });
});

올바른 예 :

export function myfunc2() {return 2;}
export function myfunc1() {return exports.myfunc2();}

// tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // out will be 3 which is what you expect
    });
});

4
이 답변에 20 번 더 투표 할 수 있으면 좋겠다. 감사합니다!
sfletche

누군가 이것이 왜 그런지 설명 할 수 있습니까? exports.myfunc2 ()는 직접 참조하지 않고 myfunc2 ()의 사본입니까?
콜린 Whitmarsh

2
@ColinWhitmarsh exports.myfunc2는 스파이 기능에 대한 참조로 대체 myfunc2될 때까지 의 직접 참조 spyOn입니다. spyOn값을 변경하고 exports.myfunc2스파이 개체로 대체하는 반면 myfunc2모듈 범위에서 변경되지 않은 상태로 유지합니다 ( spyOn액세스 할 수 없기 때문에 )
madprog

*객체 를 정지 한 상태에서 가져 오기를하지 않아야 하며 객체 속성을 변경할 수 없습니까?
agent47

1
이와 export function함께 사용하는 권장 사항 exports.myfunc2은 기술적으로 commonjs와 ES6 모듈 구문을 혼합하는 것이며 ES6 모듈 구문 사용법이 전혀 필요하지 않은 최신 버전의 웹팩 (2+)에서는 사용할 수 없습니다. ES6 엄격한 환경에서 작동하는이 답변을 바탕으로 아래 답변을 추가했습니다.
QuarkleMotion

6

명시 적 종속성 주입에 대해 알기 위해 원래 클래스가 필요없이 Typescript 클래스 가져 오기의 런타임 조롱 문제를 해결하려는 라이브러리를 구현했습니다.

라이브러리는 import * as구문을 사용한 다음 원래 내 보낸 객체를 스텁 클래스로 바꿉니다. 유형 안전성을 유지하므로 해당 테스트를 업데이트하지 않고 메소드 이름을 업데이트 한 경우 컴파일 타임에 테스트가 중단됩니다.

이 라이브러리는 여기에서 찾을 수 있습니다 : ts-mock-imports .


1
이 모듈은 더 많은 깃 허브 별이 필요합니다
SD

6

@ vdloo의 대답으로 올바른 방향으로 향했지만 공통 파일 "exports"와 ES6 모듈 "export"키워드를 같은 파일에 함께 사용하면 나에게 효과가 없었습니다 (webpack v2 이상이 불평합니다). 대신, 이름이 지정된 개별 모듈 내보내기를 모두 래핑하고 테스트 파일에서 기본 내보내기를 가져 오는 기본 (이름이 지정된 변수) 내보내기를 사용하고 있습니다. mocha / sinon과 함께 다음 내보내기 설정을 사용하고 있으며 스터 빙은 다시 와이어 링 등이 필요하지 않고 잘 작동합니다.

// MyModule.js
let MyModule;

export function myfunc2() { return 2; }
export function myfunc1() { return MyModule.myfunc2(); }

export default MyModule = {
  myfunc1,
  myfunc2
}

// tests.js
import MyModule from './MyModule'

describe('MyModule', () => {
  const sandbox = sinon.sandbox.create();
  beforeEach(() => {
    sandbox.stub(MyModule, 'myfunc2').returns(4);
  });
  afterEach(() => {
    sandbox.restore();
  });
  it('myfunc1 is a proxy for myfunc2', () => {
    expect(MyModule.myfunc1()).to.eql(4);
  });
});

유용한 답변, 감사합니다. let MyModule기본 내보내기를 사용할 필요가 없다고 언급하고 싶었습니다 (원시 객체 일 수 있음). 또한이 방법은 myfunc1()을 호출 myfunc2()할 필요가 없으며 직접 스파이하기 만합니다.
Mark Edington

@QuarkleMotion : 실수로 기본 계정과 다른 계정으로 편집 한 것 같습니다. 그것에서처럼은 보이지 않았다 - 그것은 당신의 편집 수동 승인을 통과했다 왜 당신 이 의도적 있다면, 당신은해야한다, 나는 이것이 단지 사고하지만, 가정 꼭두각시 당신 때문에 계정 양말에 공식 정책을 읽어 실수로 규칙을 위반하지 마십시오 .
눈에 띄는 컴파일러

1
@ConspicuousCompiler 감사합니다-이것은 실수였습니다. 내 이메일에 연결된 SO 계정 으로이 답변을 수정하려고하지 않았습니다.
QuarkleMotion 3

이것은 다른 질문에 대한 답변 인 것 같습니다! widget.js와 network.js는 어디에 있습니까? 이 답변에는 전이 의존성이없는 것처럼 보이므로 원래 질문을 어렵게 만들었습니다.
Bennett McElwee

3

이 구문이 작동하는 것을 발견했습니다.

내 모듈 :

// mymod.js
import shortid from 'shortid';

const myfunc = () => shortid();
export default myfunc;

내 모듈의 테스트 코드 :

// mymod.test.js
import myfunc from './mymod';
import shortid from 'shortid';

jest.mock('shortid');

describe('mocks shortid', () => {
  it('works', () => {
    shortid.mockImplementation(() => 1);
    expect(myfunc()).toEqual(1);
  });
});

문서를 참조하십시오 .


+1 및 몇 가지 추가 지침 : package.json에있는 노드 모듈에서만 작동하는 것 같습니다. 그리고 더 중요한 것은 Jest 문서에 언급되지 않은 것이 전달 된 문자열 jest.mock()이 상수 이름 대신 import / packge.json에 사용 된 이름과 일치해야한다는 것입니다. 문서에서 둘 다 동일하지만 코드와 같이 import jwt from 'jsonwebtoken'모의를 설정해야합니다.jest.mock('jsonwebtoken')
kaskelotti

0

나는 그것을 직접 시도하지는 않았지만 조롱 이 효과가 있다고 생각 합니다. 실제 모듈을 제공 한 모의로 대체 할 수 있습니다. 아래는 작동 방식에 대한 아이디어를 제공하는 예입니다.

mockery.enable();
var networkMock = {
    getDataFromServer: function () { /* your mock code */ }
};
mockery.registerMock('network.js', networkMock);

import { Widget } from 'widget.js';
// This widget will have imported the `networkMock` instead of the real 'network.js'

mockery.deregisterMock('network.js');
mockery.disable();

mockery더 이상 유지되지 않는 것 같고 Node.js에서만 작동한다고 생각하지만 덜 조롱하지 않으면 모의하기 어려운 모듈을 조롱하는 깔끔한 솔루션입니다.

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