Bagaimana cara mengejek impor modul ES6?

141

Saya memiliki modul ES6 berikut:

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() {
    ...
  }
}

Saya mencari cara untuk menguji Widget dengan contoh tiruan getDataFromServer. Jika saya menggunakan <script>modul terpisah s bukannya ES6, seperti di Karma, saya bisa menulis tes saya seperti:

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

Namun, jika saya menguji modul ES6 secara individual di luar browser (seperti dengan Mocha + babel), saya akan menulis sesuatu seperti:

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

Oke, tapi sekarang getDataFromServertidak tersedia di window(well, tidak ada windowsama sekali), dan saya tidak tahu cara menyuntikkan barang langsung ke dalam widget.jsruang lingkup sendiri.

Jadi kemana saya harus pergi dari sini?

  1. Apakah ada cara untuk mengakses ruang lingkup widget.js, atau setidaknya mengganti impornya dengan kode saya sendiri?
  2. Jika tidak, bagaimana saya bisa membuat Widgetdiuji?

Hal-hal yang saya pertimbangkan:

Sebuah. Injeksi ketergantungan manual.

Hapus semua impor dari widget.jsdan harapkan penelepon memberikan deps.

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

Saya sangat tidak nyaman dengan mengacaukan antarmuka publik Widget seperti ini dan mengekspos detail implementasi. Tidak pergi.


b. Ekspos impor untuk memungkinkan mengejek mereka.

Sesuatu seperti:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

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

kemudian:

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

Ini kurang invasif tetapi mengharuskan saya untuk menulis banyak boilerplate untuk setiap modul, dan masih ada risiko saya menggunakan, getDataFromServerbukan deps.getDataFromServersepanjang waktu. Saya gelisah tentang hal itu, tapi sejauh ini ide terbaik saya.

Kos
sumber
Jika tidak ada dukungan tiruan asli untuk impor jenis ini, saya mungkin akan berpikir untuk menulis transformator sendiri untuk babel mengubah impor gaya ES6 Anda ke sistem impor kustom yang dapat diockock. Ini pasti akan menambah lapisan lain dari kemungkinan kegagalan dan mengubah kode yang ingin Anda uji, ....
t.niese
Saya tidak dapat menetapkan test suite sekarang, tetapi saya akan mencoba menggunakan fungsi jasmin createSpy( github.com/jasmine/jasmine/blob/… ) dengan referensi yang diimpor ke getDataFromServer dari modul 'network.js'. Jadi, dalam file tes widget Anda akan mengimpor getDataFromServer, dan kemudian akanlet spy = createSpy('getDataFromServer', getDataFromServer)
Microfed
Tebakan kedua adalah mengembalikan objek dari modul 'network.js', bukan fungsi. Dengan cara itu, Anda bisa spyOnpada objek itu, diimpor dari network.jsmodul. Itu selalu referensi ke objek yang sama.
Microfed
Sebenarnya, itu sudah menjadi objek, dari apa yang bisa saya lihat: babeljs.io/repl/…
Microfed
2
Saya tidak begitu mengerti bagaimana injeksi ketergantungan mengacaukan Widgetantarmuka publik? Widgetkacau tanpa deps . Mengapa tidak membuat ketergantungan secara eksplisit?
thebearingedge

Jawaban:

129

Saya sudah mulai menggunakan import * as objgaya dalam pengujian saya, yang mengimpor semua ekspor dari modul sebagai properti dari objek yang kemudian dapat diejek. Saya menemukan ini menjadi jauh lebih bersih daripada menggunakan sesuatu seperti rewire atau proxyquire atau teknik serupa lainnya. Saya sudah melakukan ini paling sering ketika perlu mengejek tindakan Redux, misalnya. Inilah yang mungkin saya gunakan untuk contoh Anda di atas:

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

Jika fungsi Anda adalah ekspor default, maka import * as network from './network'akan menghasilkan {default: getDataFromServer}dan Anda dapat mengejek network.default.

carpeliam
sumber
3
Apakah Anda menggunakan import * as objsatu - satunya dalam tes atau juga dalam kode reguler Anda?
Chau Thai
36
@carpeliam Ini tidak akan berfungsi dengan spesifikasi modul ES6 di mana impor dibaca.
ashish
7
Jasmine mengeluh [method_name] is not declared writable or has no setteryang masuk akal karena impor es6 konstan. Apakah ada cara untuk mengatasinya?
lpan
2
@ Francisc import(tidak seperti require, yang dapat pergi ke mana saja) diangkat, sehingga Anda tidak dapat mengimpor secara teknis beberapa kali. Kedengarannya mata-mata Anda dipanggil ke tempat lain? Untuk menjaga agar tes tidak mengacaukan negara (dikenal sebagai polusi uji), Anda dapat mengatur ulang mata-mata Anda di AfterEach (mis. Sinon.sandbox). Jasmine, saya yakin melakukan ini secara otomatis.
carpeliam
10
@ agent47 Masalahnya adalah bahwa sementara spec ES6 secara khusus mencegah jawaban ini bekerja, persis seperti yang Anda sebutkan, kebanyakan orang yang menulis importdalam JS mereka tidak benar-benar menggunakan modul ES6. Sesuatu seperti webpack atau babel akan masuk saat build-time dan mengubahnya menjadi mekanisme internal mereka sendiri untuk memanggil bagian kode yang jauh (misalnya __webpack_require__) atau menjadi salah satu standar de facto pra-ES6 , CommonJS, AMD atau UMD. Dan konversi itu seringkali tidak secara khusus mengikuti spesifikasi. Jadi bagi banyak, banyak pengembang baru saja, jawaban ini berfungsi dengan baik. Untuk sekarang.
daemonexmachina
31

@carpeliam benar tetapi perhatikan bahwa jika Anda ingin memata-matai suatu fungsi dalam modul dan menggunakan fungsi lain dalam modul yang memanggil fungsi itu, Anda perlu memanggil fungsi itu sebagai bagian dari ekspor namespace jika mata-mata tidak akan digunakan.

Contoh yang salah:

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

Contoh yang tepat:

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
    });
});
vdloo
sumber
4
Saya berharap saya dapat memilih jawaban ini 20 kali lebih banyak! Terima kasih!
sfletche
Adakah yang bisa menjelaskan mengapa ini masalahnya? Apakah exports.myfunc2 () salinan myfunc2 () tanpa menjadi rujukan langsung?
Colin Whitmarsh
2
@ColinWhitmarsh exports.myfunc2adalah referensi langsung myfunc2hingga spyOnmenggantinya dengan referensi ke fungsi mata-mata. spyOnakan mengubah nilai exports.myfunc2dan menggantinya dengan objek mata-mata, sedangkan myfunc2tetap tidak tersentuh dalam lingkup modul (karena spyOntidak memiliki akses ke sana)
madprog
tidakkah mengimpor dengan *membekukan objek dan atribut objek tidak dapat diubah?
agen47
1
Hanya catatan bahwa rekomendasi ini menggunakan export functionbersama dengan exports.myfunc2secara teknis pencampuran commonjs dan sintaks modul ES6 dan ini tidak diperbolehkan dalam versi yang lebih baru dari Webpack (2+) yang membutuhkan semua-atau-tidak penggunaan ES6 sintaks modul. Saya menambahkan jawaban di bawah ini berdasarkan yang ini akan bekerja di lingkungan ketat ES6.
QuarkleMotion
6

Saya menerapkan pustaka yang mencoba untuk memecahkan masalah ejekan run-time dari impor kelas typescript tanpa perlu kelas asli untuk mengetahui tentang injeksi ketergantungan secara eksplisit.

Perpustakaan menggunakan import * as sintaks dan kemudian menggantikan objek yang diekspor asli dengan kelas rintisan. Mempertahankan keamanan jenis sehingga pengujian Anda akan rusak pada waktu kompilasi jika nama metode telah diperbarui tanpa memperbarui tes yang sesuai.

Perpustakaan ini dapat ditemukan di sini: ts-mock-import .

EmandM
sumber
1
Modul ini membutuhkan lebih banyak bintang github
SD
6

@ vdloo jawaban membuat saya menuju ke arah yang benar, tetapi menggunakan kata kunci commonjs "ekspor" dan ES6 "ekspor" bersama-sama dalam file yang sama tidak bekerja untuk saya (webpack v2 atau yang lebih baru mengeluh). Sebagai gantinya, saya menggunakan ekspor default (bernama variabel) yang membungkus semua individu bernama modul ekspor dan kemudian mengimpor ekspor default di file tes saya. Saya menggunakan pengaturan ekspor berikut dengan mocha / sinon dan stubbing berfungsi dengan baik tanpa perlu rewire, dll.:

// 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);
  });
});
QuarkleMotion
sumber
Jawaban yang bermanfaat, terima kasih. Hanya ingin menyebutkan bahwa let MyModuletidak diperlukan untuk menggunakan ekspor default (ini bisa menjadi objek mentah). Juga, metode ini tidak perlu myfunc1()memanggil myfunc2(), ia berfungsi hanya memata-matai secara langsung.
Mark Edington
@QuarkleMotion: Tampaknya Anda mengedit ini dengan akun yang berbeda dari akun utama Anda secara tidak sengaja. Itu sebabnya hasil edit Anda harus melalui persetujuan manual - sepertinya bukan dari Anda. Saya berasumsi ini hanya kecelakaan, tetapi, jika disengaja, Anda harus membaca kebijakan resmi tentang akun boneka kaus kaki sehingga Anda jangan sengaja melanggar aturan .
Compiler yang mencolok
1
@ConspicuousCompiler terimakasih untuk kepala - ini adalah kesalahan, saya tidak bermaksud untuk memodifikasi jawaban ini dengan akun SO saya yang terhubung dengan email kantor.
QuarkleMotion
Ini sepertinya menjadi jawaban untuk pertanyaan yang berbeda! Di mana widget.js dan network.js? Jawaban ini tampaknya tidak memiliki ketergantungan transitif, yang membuat pertanyaan awal sulit.
Bennett McElwee
3

Saya menemukan sintaks ini berfungsi:

Modul saya:

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

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

Kode tes modul saya:

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

Lihat dokumen .

ahli nujum
sumber
+1 dan dengan beberapa instruksi tambahan: Tampaknya hanya berfungsi dengan modul simpul, yaitu hal-hal yang Anda miliki di package.json. Dan yang lebih penting adalah, sesuatu yang tidak disebutkan dalam dokumen Jest, string yang diteruskan jest.mock()harus cocok dengan nama yang digunakan dalam impor / packge.json bukan nama konstanta. Dalam dokumen, keduanya sama, tetapi dengan kode seperti import jwt from 'jsonwebtoken'Anda perlu mengatur tiruannyajest.mock('jsonwebtoken')
kaskelotti
0

Saya belum mencobanya sendiri, tetapi saya pikir ejekan mungkin berhasil. Ini memungkinkan Anda untuk mengganti modul nyata dengan tiruan yang telah Anda berikan. Di bawah ini adalah contoh untuk memberi Anda gambaran tentang cara kerjanya:

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

Sepertinya mockerytidak terawat lagi dan saya pikir itu hanya bekerja dengan Node.js, tetapi tidak kurang, itu adalah solusi yang rapi untuk mengejek modul yang dinyatakan sulit untuk diejek.

Erik B
sumber