วิธีการจำลองการนำเข้าโมดูล 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>s แยกต่างหากจากโมดูล ES6 เช่นเดียวกับใน Karma ฉันสามารถเขียนการทดสอบของฉันเช่น:

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

ฉันรู้สึกไม่สบายใจมากที่ได้ทำอินเทอร์เฟซสาธารณะของ Widget แบบนี้และเปิดเผยรายละเอียดการใช้งาน ไม่ไป.


ข เปิดเผยการนำเข้าเพื่ออนุญาตการเยาะเย้ยพวกเขา

สิ่งที่ต้องการ:

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ตลอดเวลา ฉันไม่สบายใจเกี่ยวกับเรื่องนี้ แต่นั่นเป็นความคิดที่ดีที่สุดของฉันจนถึงตอนนี้


หากไม่มีการสนับสนุนการเยาะเย้ยของพื้นเมืองสำหรับการนำเข้าชนิดนี้ฉันอาจคิดว่าการเขียนหม้อแปลงของตัวเองเพื่อให้ Babel แปลงการนำเข้าสไตล์ ES6 ของคุณเป็นระบบนำเข้าที่กำหนดเองได้ สิ่งนี้จะช่วยเพิ่มความล้มเหลวอีกชั้นหนึ่งและเปลี่ยนรหัสที่คุณต้องการทดสอบ ...
t.niese

ฉันไม่สามารถตั้งค่าชุดทดสอบได้ในขณะนี้ แต่ฉันพยายามใช้ฟังก์ชั่นของ jasmin createSpy( github.com/jasmine/jasmine/blob/… ) พร้อมการอ้างอิงที่นำเข้าสู่ getDataFromServer จากโมดูล 'network.js' ดังนั้นในไฟล์ทดสอบของวิดเจ็ตที่คุณจะต้องนำเข้า getDataFromServer และจากนั้นก็จะlet spy = createSpy('getDataFromServer', getDataFromServer)
Microfed

การคาดเดาครั้งที่สองคือการส่งคืนวัตถุจากโมดูล 'network.js' ไม่ใช่ฟังก์ชัน ด้วยวิธีนี้คุณสามารถทำได้spyOnบนวัตถุนั้นซึ่งนำเข้าจากnetwork.jsโมดูล มันเป็นการอ้างอิงถึงวัตถุเดียวกันเสมอ
Microfed

ที่จริงแล้วมันเป็นวัตถุจากสิ่งที่ฉันเห็น: babeljs.io/repl/ ......
Microfed

2
ฉันไม่เข้าใจว่าการฉีดการพึ่งพาทำให้ยุ่งเหยิงWidgetอินเตอร์เฟซสาธารณะได้อย่างไร Widgetเป็น messed up โดยไม่ต้อง 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}และคุณสามารถจำลองเครือข่ายได้


3
คุณใช้import * as objเฉพาะในการทดสอบหรือในรหัสปกติของคุณหรือไม่
Chau Thai

36
@carpeliam นี้จะไม่ทำงานกับข้อมูลจำเพาะโมดูล ES6 ที่นำเข้าจะอ่านได้อย่างเดียว
Ashish

7
จัสมินบ่น[method_name] is not declared writable or has no setterซึ่งเหมาะสมแล้วเนื่องจากการนำเข้า es6 คงที่ มีวิธีแก้ปัญหาหรือไม่?
lpan

2
@ Francisc import(ซึ่งแตกต่างจากrequireที่สามารถไปได้ทุกที่) ได้รับการยกเพื่อให้คุณไม่สามารถนำเข้าทางเทคนิคได้หลายครั้ง เสียงเหมือนสายลับของคุณกำลังถูกเรียกที่อื่น? เพื่อป้องกันไม่ให้สถานะการทดสอบยุ่งเหยิง (เรียกว่าการทดสอบมลพิษ) คุณสามารถรีเซ็ตสายลับของคุณใน afterEach (เช่น sinon.sandbox) จัสมินฉันเชื่อว่าทำสิ่งนี้โดยอัตโนมัติ
carpeliam

10
@ agent47 ปัญหาคือว่าในขณะที่ข้อมูลจำเพาะ ES6 ป้องกันไม่ให้คำตอบนี้ทำงานในลักษณะที่คุณกล่าวถึงคนส่วนใหญ่ที่เขียนimportใน JS ของพวกเขาไม่ได้ใช้โมดูล ES6 บางอย่างเช่น webpack หรือ babel จะก้าวเข้าสู่ build-time และแปลงให้เป็นกลไกภายในของตนเองสำหรับการเรียกส่วนต่าง ๆ ของรหัส (เช่น__webpack_require__) หรือเป็นหนึ่งในมาตรฐาน pre-ES6 de facto , CommonJS, AMD หรือ UMD และการแปลงนั้นมักจะไม่ยึดติดกับข้อกำหนดอย่างเคร่งครัด ดังนั้นสำหรับหลาย ๆ devs ตอนนี้คำตอบนี้ใช้ได้ดี สำหรับตอนนี้.
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 และสิ่งนี้ไม่ได้รับอนุญาตใน webpack (2+) เวอร์ชันใหม่กว่าที่ต้องการการใช้งานไวยากรณ์ของโมดูล ES6 ทั้งหมดหรือไม่มีอะไรเลย ฉันได้เพิ่มคำตอบด้านล่างตามคำแนะนำนี้ซึ่งจะทำงานในสภาพแวดล้อมที่เข้มงวด ES6
QuarkleMotion

6

ฉันใช้ไลบรารีที่พยายามแก้ปัญหาการเยาะเย้ยรันไทม์ของการอิมพอร์ตคลาส typescript โดยไม่ต้องการคลาสดั้งเดิมเพื่อทราบเกี่ยวกับการฉีดพึ่งพาใด ๆ อย่างชัดเจน

ไลบรารีใช้import * asไวยากรณ์แล้วแทนที่อ็อบเจ็กต์ที่เอ็กซ์พอร์ตดั้งเดิมด้วยคลาส stub มันยังคงความปลอดภัยของประเภทเพื่อการทดสอบของคุณจะแตกสลายในเวลารวบรวมถ้าชื่อวิธีการได้รับการปรับปรุงโดยไม่ต้องปรับปรุงการทดสอบที่สอดคล้องกัน

ห้องสมุดนี้สามารถพบได้ที่นี่: TS-จำลองการนำเข้า


1
โมดูลนี้ต้องการดาว GitHub มากขึ้น
SD

6

@ คำตอบของ vdloo ทำให้ฉันมุ่งหน้าไปในทิศทางที่ถูกต้อง แต่การใช้คำหลัก "การส่งออก" และ "การส่งออก" โมดูล Commonjs ร่วมกันในไฟล์เดียวกันไม่ได้ผลสำหรับฉัน (webpack v2 หรือใหม่กว่าบ่น) แต่ฉันใช้การส่งออกเริ่มต้น (ตัวแปรที่มีชื่อ) การห่อของแต่ละโมดูลที่มีชื่อการส่งออกแล้วนำเข้าการส่งออกเริ่มต้นในไฟล์ทดสอบของฉัน ฉันใช้การตั้งค่าการส่งออกต่อไปนี้ด้วยมอคค่า / ไซดอนและการขัดถูทำงานได้ดีโดยไม่จำเป็นต้องใช้ rewire ฯลฯ :

// 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: ดูเหมือนว่าคุณจะแก้ไขด้วยบัญชีอื่นที่ไม่ใช่บัญชีหลักของคุณโดยไม่ได้ตั้งใจ นั่นเป็นสาเหตุที่การแก้ไขของคุณต้องผ่านการอนุมัติด้วยตนเอง - มันดูไม่เหมือนว่ามันมาจากคุณ ฉันคิดว่านี่เป็นเพียงอุบัติเหตุ แต่ถ้ามันเป็นความตั้งใจคุณควรอ่านนโยบายอย่างเป็นทางการเกี่ยวกับบัญชีถุงเท้าหุ่นเพื่อให้คุณ ไม่ได้ตั้งใจละเมิดกฎ
Compiler ที่เห็นได้ชัดเจน

1
@ConspicuousCompiler ขอบคุณสำหรับหัวขึ้น - นี่เป็นข้อผิดพลาดฉันไม่ได้ตั้งใจที่จะแก้ไขคำตอบนี้ด้วยบัญชี SO ที่เชื่อมโยงกับอีเมลในที่ทำงานของฉัน
QuarkleMotion

นี่ดูเหมือนจะเป็นคำตอบสำหรับคำถามอื่น! 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 docs สตริงที่ส่งผ่านจะ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.