Bir ES6 modülünün ithalatı nasıl taklit edilir?


141

Aşağıdaki ES6 modüllerine sahibim:

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

Widget'ı sahte bir örnekle test etmenin bir yolunu arıyorum getDataFromServer. <script>Karma gibi ES6 modülleri yerine ayrı s kullandıysam, testimi şöyle yazabilirdim:

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

Ancak, ES6 modüllerini tek tek bir tarayıcının dışında test edersem (Mocha + babel gibi), şöyle bir şey yazardım:

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

Tamam, ama şu anda getDataFromServermevcut değil window(iyi, hiç yok window) ve bir şeyleri doğrudan widget.jskendi kapsamına enjekte etmenin bir yolunu bilmiyorum .

Peki buradan nereye gideceğim?

  1. İçe aktarma kapsamına erişmenin widget.jsveya en azından ithalatını kendi kodumla değiştirmenin bir yolu var mı ?
  2. Değilse, nasıl Widgettest edilebilir yapabilirim ?

Düşündüğüm şeyler:

a. Manuel bağımlılık enjeksiyonu.

Tüm ithalatları kaldırın widget.jsve arayanın depsi sağlamasını bekleyin.

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

Widget'ın genel arayüzünü bu şekilde karıştırmak ve uygulama ayrıntılarını ortaya çıkarmaktan çok rahatsızım. Gitme.


b. İthalatları alay konusu yapmak için açıkta bırakın

Gibi bir şey:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

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

sonra:

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

Bu daha az invazivdir, ancak her modül için çok sayıda kaynatma plakası yazmamı gerektirir ve hala her zaman getDataFromServeryerine kullanma riskim var deps.getDataFromServer. Bu konuda huzursuzum ama şimdiye kadarki en iyi fikrim bu.


Bu tür bir ithalat için yerel alay desteği yoksa, muhtemelen ES6 tarzı ithalatınızı özel bir mockable ithalat sistemine dönüştüren babel için kendi transformatörünü yazmayı düşünürüm. Bu kesinlikle başka bir olası hata katmanı ekler ve test etmek istediğiniz kodu değiştirir, ....
t.niese

Şu anda bir test paketi ayarlayamıyorum, ancak jasmin'increateSpy ( github.com/jasmine/jasmine/blob/… ) işlevini 'network.js' modülünden DataFromServer almak için içe aktarılmış bir referansla kullanmaya çalışacağım . Böylece, widget'ın test dosyasında getDataFromServer'ı içe aktarırsınız ve sonralet spy = createSpy('getDataFromServer', getDataFromServer)
Microfed

İkinci tahmin bir nesneyi 'network.js' modülünden döndürmektir, bir işlevi değil. Bu şekilde, modülden spyOniçe aktarılan bu nesne üzerinde olabilirsiniz network.js. Her zaman aynı nesneye referanstır.
Microfed

Aslında, zaten görebildiğim kadarıyla
Microfed

2
Bağımlılık enjeksiyonunun Widgetgenel arayüzünü nasıl bozduğunu gerçekten anlamıyorum ? olmadanWidget berbat . Bağımlılığı neden açık hale getirmiyorsun? deps
thebearingedge

Yanıtlar:


129

import * as objTestlerimdeki stili kullanmaya başladım , bu da daha sonra alay edilebilecek bir nesnenin özellikleri olarak bir modülden tüm ihracatı içeri aktarıyor. Bunu rewire veya proxyquire veya benzer bir teknik kullanmaktan çok daha temiz buluyorum. Bunu en çok Redux eylemleriyle alay etmek gerektiğinde yaptım. Yukarıdaki örnek için ne kullanabilirsiniz:

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

İşleviniz varsayılan bir dışa aktarma olursa, o import * as network from './network'zaman üretecektir {default: getDataFromServer}ve network.default ile alay edebilirsiniz.


3
Kullanıyor musunuz import * as objsadece testinde ya da normal kodunda?
Chau Thai

36
@carpeliam Bu, içe aktarma işlemlerinin salt okunur olduğu ES6 modülü spesifikasyonu ile çalışmaz.
ashish

7
Yasemin, [method_name] is not declared writable or has no setteres6 ithalatı sabit olduğundan mantıklı. Geçici çözümün bir yolu var mı?
lpan

2
@Francisc import(aksine require, herhangi bir yere gidebilir) kaldırılır, böylece teknik olarak birçok kez içe aktaramazsınız. Casusun başka bir yere çağırılıyor gibi mi geliyor? Testlerin karışıklık durumunu (test kirliliği olarak bilinir) korumak için casuslarınızı afterEach (sıfır sinon.sandbox) içinde sıfırlayabilirsiniz. Yasemin inanıyorum bunu otomatik olarak yapıyor.
carpeliam

10
@ agent47 Sorun, ES6 spesifikasyonunun bu cevabın özellikle bahsettiğiniz şekilde çalışmasını engellemesine rağmen, JS'lerinde yazan çoğu insanın importgerçekten ES6 modülleri kullanmamasıdır. Webpack veya babel gibi bir şey, derleme sırasında devreye girecek ve kodun uzak kısımlarını çağırmak için kendi iç mekanizmasına (örneğin __webpack_require__) veya ES6 öncesi facto standartlarından biri olan CommonJS, AMD veya UMD'ye dönüştürecektir. Ve bu dönüşüm genellikle spesifikasyona kesinlikle uymuyor. Yani şimdi, birçok geliştirici için, bu cevap iyi çalışıyor. Şimdilik.
daemonexmachina

31

@carpeliam doğrudur, ancak bir modüldeki bir işlev üzerinde casusluk yapmak ve o modülde bu işlevi çağıran başka bir işlev kullanmak istiyorsanız, bu işlevi dışa aktarma ad alanının bir parçası olarak çağırmanız gerekir, aksi takdirde casus kullanılmaz.

Yanlış örnek:

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

Doğru örnek:

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
Keşke bu cevabı 20 kere daha oylayabilseydim! Teşekkür ederim!
sfletche

Birisi bunun neden böyle olduğunu açıklayabilir mi? Export.myfunc2 (), doğrudan başvuru olmadan myfunc2 () 'nin bir kopyası mı?
Colin Whitmarsh

2
@ColinWhitmarsh bir casus işlevine gönderme ile değiştirilene kadar exports.myfunc2doğrudan bir başvurudır. değerini değiştirecek ve bir casus nesneyle değiştirecek, ancak modülün kapsamına dokunulmaz (çünkü ona erişimi yoktur)myfunc2spyOnspyOnexports.myfunc2myfunc2spyOn
madprog

*nesneyi dondurmayla içe aktarmamalısınız ve nesne öznitelikleri değiştirilemez mi?
agent47

1
Bununla export functionbirlikte kullanmanın bu tavsiyesinin exports.myfunc2teknik olarak commonjs ve ES6 modülü sözdizimini karıştırdığı ve buna tamamen ya da hiç ES6 modülü sözdizimi kullanımı gerektiren web paketinin (2+) daha yeni sürümlerinde izin verilmediğine dikkat edin. Aşağıda ES6 katı ortamlarında çalışacak bir cevap ekledim.
QuarkleMotion

6

Herhangi bir açık bağımlılık enjeksiyonunu bilmek için orijinal sınıfa ihtiyaç duymadan, Typecript sınıf ithalatının çalışma zamanı alay sorununu çözmeye çalışan bir kütüphane uyguladım.

Kütüphane import * assözdizimini kullanır ve ardından dışa aktarılan orijinal nesneyi bir saplama sınıfıyla değiştirir. Tip güvenliğini korur, böylece ilgili test güncellenmeden bir yöntem adı güncellenirse testleriniz derleme zamanında kırılır.

Bu kütüphane burada bulunabilir: ts-mock-import .


1
Bu modül daha fazla github yıldızına ihtiyaç duyuyor
SD

6

@ vdloo'nun cevabı beni doğru yöne yönlendirdi, ancak aynı dosyada hem ortak "" dışa aktarma "hem de ES6 modülü" dışa aktarma "anahtar kelimelerini kullanmak benim için işe yaramadı (webpack v2 veya daha sonra şikayet ediyor). Bunun yerine, varsayılan (adlandırılmış değişken) dışa aktarma, adlandırılmış modül dışa aktarma işlemlerini tek tek tamamlayıp test dosyamda varsayılan dışa aktarmayı kullanıyorum. Mocha / sinon ile aşağıdaki ihracat kurulumunu kullanıyorum ve stubbing, rewire, vs'ye ihtiyaç duymadan iyi çalışıyor:

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

Yararlı cevap, teşekkürler. Sadece let MyModulevarsayılan dışa aktarmayı kullanmak için gerekli olmadığını belirtmek istedim (ham bir nesne olabilir). Ayrıca, bu yöntemin myfunc1()çağrılmasını gerektirmez myfunc2(), sadece doğrudan casusluk yapmak için çalışır.
Mark Edington

@QuarkleMotion: Bunu yanlışlıkla ana hesabınızdan farklı bir hesapla düzenlediğiniz anlaşılıyor. Bu nedenle düzenlemenizin manuel bir onaydan geçmesi gerekiyordu - bu sizden gelmiş gibi görünmüyordu, bunun sadece bir kaza olduğunu varsayıyorum, ancak kasıtlıysa, çorap kukla hesaplarındaki resmi politikayı okumalısınız, böylece kuralları yanlışlıkla ihlal etmeyin .
Dikkat çekici Derleyici

1
@ConspicuousCompiler dikkat çektiği için teşekkürler - bu bir hataydı, bu cevabı iş e-posta bağlantılı SO hesabımla değiştirmek istemedim.
QuarkleMotion

Bu farklı bir sorunun cevabı gibi görünüyor! Widget.js ve network.js nerede? Bu cevabın geçişli bağımlılığı yok gibi görünüyor, bu da orijinal soruyu zorlaştıran şeydi.
Bennett McElwee

3

Bu sözdiziminin çalıştığını gördüm:

Modülüm:

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

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

Modülümün test kodu:

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

Dokümana bakınız .


+1 ve bazı ek talimatlarla: Yalnızca düğüm modülleriyle, yani package.json'da bulunanlarla çalışmak gibi görünüyor. Ve daha da önemlisi, Jest belgelerinde belirtilmeyen bir şey, geçirilen dize jest.mock(), sabit adı yerine import / packge.json içinde kullanılan adla eşleşmelidir. Dokümanlarda ikisi de aynı, ancak import jwt from 'jsonwebtoken'sizin gibi kod ile alay olarak kurulum gerekirjest.mock('jsonwebtoken')
kaskelotti

0

Kendim denemedim, ama alaycılığın işe yarayabileceğini düşünüyorum . Gerçek modülü, sağladığınız bir alayla değiştirmenizi sağlar. Aşağıda size nasıl çalıştığı hakkında bir fikir veren bir örnek verilmiştir:

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

Artık korunmuyor gibi görünüyor mockeryve sadece Node.js ile çalıştığını düşünüyorum, ancak daha azı, aksi takdirde alay etmek zor modülleri taklit etmek için temiz bir çözüm.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.