ES6 모듈의 수입을 조롱하려면 어떻게 해야 합니까?
다음과 같은 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>카르마와 같이 ES6 모듈 대신 다음과 같이 테스트를 작성할 수 있습니다.
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자신의 범위
그럼 여기서 어디로 가야하죠?
- 다음의 범위에 접근할 수 있는 방법이 있습니까?
widget.js, 아니면 적어도 수입품을 내 코드로 대체하는 것? - 만약 그렇지 않다면, 어떻게 만들 수 있습니까?
Widget테스트 가능한?
고려한 항목:
a. 수동 종속성 주입.
다음에서 가져오기를 모두 제거합니다.widget.js그리고 전화 건 사람이 dep를 제공할 것으로 예상합니다.
export class Widget() {
constructor(deps) {
deps.getDataFromServer("dataForWidget")
.then(data => this.render(data));
}
}
위젯의 공개 인터페이스를 이렇게 엉망으로 만들고 구현 세부 정보를 노출하는 것이 매우 불편합니다.안 돼요.
b. 그들을 조롱할 수 있도록 수입품을 노출시킵니다.
다음과 같은 경우:
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시종저는 그것이 불안하지만, 까지 제가한 최선의 방법입니다.저는 그것이 불안하지만, 지금까지 제가 생각한 최선의 방법입니다.
저는 그들을 고용하기 시작했습니다.import * as obj내 테스트 내 스타일, 모듈에서 모든 내보내기를 개체의 속성으로 가져온 다음 조롱할 수 있습니다.저는 이것이 재배선이나 프록시콰이어 같은 것이나 비슷한 기술을 사용하는 것보다 훨씬 더 깔끔하다고 생각합니다.예를 들어, 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를 모의할 수 있습니다.
참고: ES 사양은 모듈을 읽기 전용으로 정의하며, 많은 ES 트랜스필러가 이를 존중하기 시작했으며, 이는 이러한 스파이 방식을 깨뜨릴 수 있습니다.이는 테스트 프레임워크뿐만 아니라 트랜스필러에 크게 의존합니다.예를 들어, 재스민은 적어도 현재는 그렇지 않지만, 제스트는 이 작품을 만들기 위해 마법을 부린다고 생각합니다.YMMV.
carpeliam이 맞지만 모듈의 함수를 스파이로 감시하고 해당 모듈에서 해당 함수를 호출하는 다른 함수를 사용하려면 해당 함수를 내보내기 네임스페이스의 일부로 호출해야 합니다. 그렇지 않으면 스파이가 사용되지 않습니다.
잘못된 예:
// File mymodule.js
export function myfunc2() {return 2;}
export function myfunc1() {return myfunc2();}
// File 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();}
// File 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
});
});
vdloo의 답변은 제가 올바른 방향으로 향하도록 만들었지만, CommonJS "exports"와 ES6 module "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);
});
});
저는 원래 클래스가 명시적인 종속성 주입에 대해 알 필요 없이 TypeScript 클래스 가져오기의 런타임 조롱 문제를 해결하려고 시도하는 라이브러리를 구현했습니다.
도서관은 사용합니다.import * assyntax를 선택한 다음 원래 내보낸 개체를 stub 클래스로 바꿉니다.형식 안전성을 유지하므로 해당 테스트를 업데이트하지 않고 메서드 이름을 업데이트한 경우 컴파일 시 테스트가 중단됩니다.
이 라이브러리는 여기에서 찾을 수 있습니다: ts-mock-imports.
이 구문이 작동하는 것을 발견했습니다.
내 모듈:
// File mymod.js
import shortid from 'shortid';
const myfunc = () => shortid();
export default myfunc;
내 모듈의 테스트 코드:
// File 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);
});
});
설명서 참조.
제가 직접 해본 적은 없지만, 조롱이 효과가 있을지도 모른다고 생각합니다.제공한 모의실험으로 실제 모듈을 대체할 수 있습니다.아래 예시는 작동 방식에 대한 아이디어를 제공하는 것입니다.
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에서만 작동한다고 생각하지만, 그럼에도 불구하고 조롱하기 어려운 모듈을 조롱할 수 있는 깔끔한 솔루션입니다.
저는 최근에 이 문제를 깔끔하게 처리하는 babel-plugin-mockable-imports를 발견했습니다, IMHO.이미 바벨을 사용하고 있다면 살펴볼 가치가 있습니다.
다음에서 반환된 결과를 모의 실험하고 싶다고 가정해 보십시오.isDevMode()특정 상황에서 코드가 어떻게 작동하는지 확인하기 위해 기능합니다.
다음 예제는 다음 설정에 대해 테스트됩니다.
"@angular/core": "~9.1.3",
"karma": "~5.1.0",
"karma-jasmine": "~3.3.1",
다음은 간단한 테스트 케이스 시나리오의 예입니다.
import * as coreLobrary from '@angular/core';
import { urlBuilder } from '@app/util';
const isDevMode = jasmine.createSpy().and.returnValue(true);
Object.defineProperty(coreLibrary, 'isDevMode', {
value: isDevMode
});
describe('url builder', () => {
it('should build url for prod', () => {
isDevMode.and.returnValue(false);
expect(urlBuilder.build('/api/users').toBe('https://api.acme.enterprise.com/users');
});
it('should build url for dev', () => {
isDevMode.and.returnValue(true);
expect(urlBuilder.build('/api/users').toBe('localhost:3000/api/users');
});
});
의 예시적인 내용src/app/util/url-builder.ts
import { isDevMode } from '@angular/core';
import { environment } from '@root/environments';
export function urlBuilder(urlPath: string): string {
const base = isDevMode() ? environment.API_PROD_URI ? environment.API_LOCAL_URI;
return new URL(urlPath, base).toJSON();
}
이를 위해 출력 기반 라이브러리 모의 가져오기를 사용할 수 있습니다.
테스트하고 싶은 코드가 있다고 가정해 보겠습니다.cat.js:
import {readFile} from 'fs/promises';
export default function cat() {
const readme = await readFile('./README.md', 'utf8');
return readme;
};
그리고 이름을 가진 탭 기반 테스트test.js다음과 같이 표시됩니다.
import {test, stub} from 'supertape';
import {createImport} from 'mock-import';
const {mockImport, reImport, stopAll} = createMockImport(import.meta.url);
// check that stub called
test('cat: should call readFile', async (t) => {
const readFile = stub();
mockImport('fs/promises', {
readFile,
});
const cat = await reImport('./cat.js');
await cat();
stopAll();
t.calledWith(readFile, ['./README.md', 'utf8']);
t.end();
});
// mock result of a stub
test('cat: should return readFile result', async (t) => {
const readFile = stub().returns('hello');
mockImport('fs/promises', {
readFile,
});
const cat = await reImport('./cat.js');
const result = await cat();
stopAll();
t.equal(result, 'hello');
t.end();
});
테스트를 실행하려면 --loader 매개 변수를 추가해야 합니다.
node --loader mock-import test.js
또는 NODE_OPTIONS:
NODE_OPTIONS="--loader mock-import" node test.js
밑단에mock-importfly all에서 대체하는 transformSourcehook을 사용합니다.imports다음과 같은 형태로 상수를 선언합니다.
const {readFile} = global.__mockImportCache.get('fs/promises');
그렇게mockImport새 항목을 추가합니다.Map그리고.stopAll테스트가 겹치지 않도록 모든 모의실험을 지웁니다.
이 모든 것이 필요한 이유는 ESM이 자체 별도의 캐시를 가지고 있고 사용자 환경 코드가 직접 액세스할 수 없기 때문입니다.
이 lib이 getter로 아무것도 할 수 없기 때문에 클래스에 추가한 다음 덮어쓰기로 해결했습니다 :'(
import { libFn } from 'lib';
class MyComponent {
libFn = libFn;
inUse() {
this.libFn();
}
}
it('', () => {
component.libFn = jasmine.createSpy().and.returnValue(5);
});
그만큼 쉬운 일입니다.그게 최선의 방법은 아닐 거라고 확신해요. 왜냐면 libFn은 내 반에 있어서는 안 되기 때문이에요.재스민은 립스를 조롱하면서 재스민에게서 배워야 합니다.
가져온 함수를 조롱하는 예가 있습니다.
파일 network.js
export function exportedFunc(data) {
//..
}
파일 widget.js
import { exportedFunc } from 'network.js';
export class Widget() {
constructor() {
exportedFunc("data")
}
}
테스트파일
import { Widget } from 'widget.js';
import { exportedFunc } from 'network'
jest.mock('network', () => ({
exportedFunc: jest.fn(),
}))
describe("widget", function() {
it("should do stuff", function() {
let widget = new Widget();
expect(exportedFunc).toHaveBeenCalled();
});
});
아직 시도해 보지는 못했지만 (라이브 데모는 codesandbox.io/s/adoring-orla-wqs3zl?file=/index.js)
이론적으로 브라우저 기반 테스트 런너가 있는 경우, 무시할 ES6 모듈에 대한 요청을 차단하고 대체 구현으로 대체할 수 있는 Service Worker를 포함할 수 있어야 합니다(Mock Service Worker의 접근 방식과 유사하거나 동일).
그래서 당신의 서비스 직원들은 이와 같은 것입니다.
self.addEventListener('fetch', (event) => {
if (event.request.url.includes("canvas-confetti")) {
event.respondWith(
new Response('const confetti=function() {}; export default confetti;', {
headers: { 'Content-Type': 'text/javascript' }
})
);
}
});
소스 코드가 이와 같은 ES6 모듈을 끌어들이는 경우
import confetti from 'https://cdn.skypack.dev/canvas-confetti';
confetti();
언급URL : https://stackoverflow.com/questions/35240469/how-can-i-mock-the-imports-of-an-es6-module
'programing' 카테고리의 다른 글
| 테마 기능에서 도우미 기능을 정의하고 사용하는 방법.php? (0) | 2023.10.17 |
|---|---|
| 여러 개의 빈 열을 팬더 DataFrame에 추가 (0) | 2023.10.17 |
| jQuery에서 PHP 함수를 호출하시겠습니까? (0) | 2023.10.17 |
| 장고와 MySQL에 문제가 있습니다.NotSupportedError(django.db.utils).지원되지 않는 오류: MariaDB 10.3 이상이 필요합니다(5.5.65 발견). (0) | 2023.10.12 |
| "제출" 콜백을 사용할 때 여러 번 제출한 양식과 $.ajax를 게시합니다. (0) | 2023.10.12 |