Diğer modülleri gerektiren bir Node.js modülünü nasıl birim olarak test edebilir ve global gereksinim işlevini nasıl alay edebilirsiniz?


156

Bu benim sorunumun temelini gösteren önemsiz bir örnek:

var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.doComplexStuff();
}

module.exports = underTest;

Bu kod için bir birim testi yazmaya çalışıyorum. İşlevi tamamen innerLibalay etmeden gereksinimi nasıl alay edebilirim require?

Bu yüzden küresel olanı alay etmeye çalışıyorum requireve bunu yapmak için bile işe yaramayacağını buluyorum:

var path = require('path'),
    vm = require('vm'),
    fs = require('fs'),
    indexPath = path.join(__dirname, './underTest');

var globalRequire = require;

require = function(name) {
    console.log('require: ' + name);
    switch(name) {
        case 'connect':
        case indexPath:
            return globalRequire(name);
            break;
    }
};

Sorun, dosya requireiçindeki işlevin underTest.jsaslında alay edilmemiş olmasıdır. Hala küresel requirefonksiyona işaret ediyor . Bu yüzden sadece requirealaycı yaptığım aynı dosya içindeki işlevi alay edebilirim gibi görünüyor . Eğer requireyerel kopyayı geçersiz kıldıktan sonra bile, herhangi bir şeyi dahil etmek için küresel kullanırsam, gerekli dosyalar hala küresel requirebaşvuru.


üzerine yazmak zorundasınız global.require. Değişkenler module, modüller kapsam dahilinde olduğu için varsayılan olarak yazılır .
Raynos

@Raynos Bunu nasıl yaparım? global.require tanımsız mı? Kendi işlevimle değiştirsem bile, diğer işlevler bunu asla kullanmaz mı?
HMR

Yanıtlar:


175

Şimdi yapabilirsin!

Modülünüzü test ederken global gereksinimi geçersiz kılmaya özen gösterecek proxyquire yayınladım .

Bu , gerekli modüller için alay enjekte etmek için kodunuzda herhangi bir değişikliğe ihtiyacınız olmadığı anlamına gelir .

Proxyquire, test etmeye çalıştığınız modülü çözmenizi ve gerekli modüller için alayları / saplamaları basit bir adımda geçirmenizi sağlayan çok basit bir API'ye sahiptir.

@Raynos, bunu başarmak veya aşağıdan yukarıya geliştirme yapmak için geleneksel olarak çok ideal çözümlere başvurmak zorunda kaldığınız konusunda haklı.

Proxyquire oluşturmamın ana nedeni budur - herhangi bir sorun olmadan yukarıdan aşağıya test odaklı geliştirmeye izin vermek.

İhtiyaçlarınıza uygun olup olmadığını ölçmek için belgelere ve örneklere göz atın.


5
Proxyquire kullanıyorum ve yeterince iyi şeyler söyleyemem. Beni kurtardı! Titanyum appcelerator'da geliştirilen bir uygulama için bazı modülleri mutlak yollar ve birçok dairesel bağımlılık olmaya zorlayan yasemin düğümü testleri yazmakla görevlendirildim. proxyquire, bu boşluğu bırakmama ve her test için ihtiyaç duymadığım hammaddeyi alay etmeme izin verin. ( Burada açıklanmıştır ). Çok teşekkür ederim!
Sukima

Proxyquire'ın kodunuzu doğru bir şekilde test etmenize yardımcı olmaktan mutluluk duyuyoruz :)
Thorsten Lorenz

1
very nice @ThorstenLorenz, def edeceğim. kullanıyor proxyquire!
bevacqua

Fantastik! Kabul ettiğim cevabı gördüğümde "yapamazsın" diye düşündüm "Aman Tanrım, cidden mi ?!" ama bu gerçekten kurtardı.
Chadwick

3
Webpack kullananlarınız için proxyquire'ı araştırmak için zaman harcamayın. Webpack'i desteklemez. Bunun yerine inject- loader'a bakıyorum ( github.com/plasticine/inject-loader ).
Artif3x

116

Bu durumda daha iyi bir seçenek, döndürülen modülün yöntemlerini alay etmektir.

Daha iyi veya daha kötü için, node.js modüllerinin çoğu singleton'dur; () aynı modülü gerektiren iki kod parçası, o modüle aynı referansı alır.

Bunu kullanabilir ve gerekli öğeleri alay etmek için sinon gibi bir şey kullanabilirsiniz . mocha testi şu şekildedir:

// in your testfile
var innerLib  = require('./path/to/innerLib');
var underTest = require('./path/to/underTest');
var sinon     = require('sinon');

describe("underTest", function() {
  it("does something", function() {
    sinon.stub(innerLib, 'toCrazyCrap').callsFake(function() {
      // whatever you would like innerLib.toCrazyCrap to do under test
    });

    underTest();

    sinon.assert.calledOnce(innerLib.toCrazyCrap); // sinon assertion

    innerLib.toCrazyCrap.restore(); // restore original functionality
  });
});

Sinon, iddialar yapmak için chai ile iyi bir entegrasyona sahip ve daha kolay casus / saplama temizliğine izin vermek için (test kirliliğini önlemek için) sinon'u mocha ile entegre etmek için bir modül yazdım .

UnderTest'in yalnızca bir işlevi döndürdüğü için underTest'in aynı şekilde alay edilemeyeceğini unutmayın.

Başka bir seçenek Jest alayları kullanmaktır. Sayfalarını takip et


1
Ne yazık ki, node.js modüllerinin burada açıklandığı gibi singleton olması garanti EDİLMEZ: justjs.com/posts/…
FrontierPsycho

4
@FrontierPsycho birkaç şey: İlk olarak, test söz konusu olduğunda, makale ilgisizdir. Bağımlılıklarınızı (ve bağımlılık bağımlılıklarını değil) test ettiğiniz sürece, tüm kodlarınız aynı nesneyi geri alırsınız require('some_module'), çünkü tüm kodlarınız aynı node_modules dir değerini paylaşır. İkincisi, makale isim alanını bir çeşit dikey olan tektonlarla karıştırıyor. Üçüncüsü, bu makale oldukça eski (node.js söz konusu olduğunda), bu nedenle gün içinde geçerli olabilecek şey şu anda geçerli olmayabilir.
Elliot Foster

2
Hm. Birimiz aslında bir noktayı kanıtlayan kodu kazmazsa, bağımlılık enjeksiyon çözümünüzle devam ederdim, ya da sadece nesneleri etrafta geçirirseniz, daha güvenli ve geleceğe dönük bir kanıt.
FrontierPsycho

1
Neyin kanıtlanmasını istediğinden emin değilim. Düğüm modüllerinin tekil (önbelleğe alınmış) doğası yaygın olarak anlaşılmaktadır. Bağımlılık enjeksiyonu, iyi bir rota olsa da, daha fazla kazan plakası ve daha fazla kod olabilir. DI, casusları / saplamaları / alayları dinamik olarak kodunuza atmanın daha zor olduğu statik olarak yazılan dillerde daha yaygındır. Son üç yılda yaptığım birden fazla proje yukarıdaki cevabımda açıklanan yöntemi kullanıyor. Her yöntemden en kolayı olsa da, dikkatli bir şekilde kullanıyorum.
Elliot Foster

1
Sinon.js'yi okumanızı öneririm. Ya olur (yukarıdaki örnekte olduğu gibi) Eğer Sinon kullanıyorsanız innerLib.toCrazyCrap.restore()ve restub veya sinon aracılığıyla çağrı sinon.stub(innerLib, 'toCrazyCrap'): nasıl saplama davranacağını değiştirmek olanak sağlayan innerLib.toCrazyCrap.returns(false). Ayrıca, rewire, proxyquireyukarıdaki uzantıyla hemen hemen aynı görünüyor .
Elliot Foster

11

Sahte bir istek kullanıyorum . requireTest edilecek modülden önce alaylarınızı tanımladığınızdan emin olun .


Ayrıca stop (<file>) veya stopAll () yapmak da iyidir, böylece sahte istemediğiniz bir testte önbelleğe alınmış bir dosya elde edemezsiniz.
Justin Kruse

1
Bu bir ton yardımcı oldu.
wallop

2

Alay requireetmek bana kötü bir saldırı gibi geliyor. Şahsen bundan kaçınmaya çalışacağım ve daha test edilebilir hale getirmek için kodu yeniden düzenleyeceğim. Bağımlılıkları ele almak için çeşitli yaklaşımlar vardır.

1) bağımlılıkları bağımsız değişken olarak geçirme

function underTest(innerLib) {
    return innerLib.doComplexStuff();
}

Bu, kodu evrensel olarak test edilebilir hale getirecektir. Dezavantajı, kodun daha karmaşık görünmesini sağlayabilecek bağımlılıkları geçmeniz gerektiğidir.

2) modülü bir sınıf olarak uygulayın, sonra bağımlılıkları elde etmek için sınıf yöntemlerini / özelliklerini kullanın

(Bu, sınıf kullanımının makul olmadığı, ancak fikri ilettiği anlaşılır bir örnektir) (ES6 örneği)

const innerLib = require('./path/to/innerLib')

class underTestClass {
    getInnerLib () {
        return innerLib
    }

    underTestMethod () {
        return this.getInnerLib().doComplexStuff()
    }
}

Şimdi getInnerLibkodunuzu test etmek için kolayca saplama yöntemi yapabilirsiniz . Kod daha ayrıntılı, ancak test edilmesi daha kolay hale gelir.


1
Tahmin ettiğin gibi kibirli olduğunu düşünmüyorum ... bu alaycılığın özü. Gerekli bağımlılıkları alay etmek işleri kolaylaştırır ve kod yapısını değiştirmeden geliştiriciye kontrol sağlar. Yöntemleriniz çok ayrıntılı ve bu nedenle akıl yürütmesi zor. Ben bunun üzerine proxyrequire veya sahte gerektiren seçin; Burada herhangi bir sorun görmüyorum. Kod temiz ve akıl yürütmesi kolaydır ve bunu okuyan çoğu insanın karmaşık hale getirmesini istediğiniz yazılı koda sahip olduğunu hatırlayın. Bu libs hackish ise, o zaman alay ve stubbing de tanımınızla hack'tir ve durdurulmalıdır.
Emmanuel Mahuni

1
1 numaralı yaklaşımla ilgili sorun, dahili uygulama ayrıntısını yığına geçirmenizdir. Birden fazla katmanla modülünüzün tüketicisi olmak çok daha karmaşık hale gelir. Ancak, bağımlılıkların sizin için otomatik olarak enjekte edilmesi için bir IOC kapsayıcısı ile çalışabilir, ancak ithalat bildirimi yoluyla düğüm modüllerine zaten bağımlılıklar enjekte ettiğimizden, o düzeyde onları alay edebilmek mantıklıdır. .
magritte

1) Bu sadece sorunu başka bir dosyaya taşır 2) hala diğer modülü yükler ve bu nedenle performans yükü yükler ve muhtemelen yan etkilere neden olur ( colorsile String.prototype
uğraşan

2

Şimdiye kadar jest kullandıysanız, muhtemelen jest'in sahte özelliğini biliyorsunuzdur.

"Jest.mock (...)" kullanarak, kodunuzdaki bir zorunlu deyimde oluşacak dizeyi bir yerde belirtebilirsiniz ve bu dizeyi kullanarak bir modül gerektiğinde bunun yerine bir sahte nesne döndürülür.

Örneğin

jest.mock("firebase-admin", () => {
    const a = require("mocked-version-of-firebase-admin");
    a.someAdditionalMockedMethod = () => {}
    return a;
})

"firebase-admin" in tüm ithalat / gereksinimlerini, "fabrika" işlevinden döndürdüğünüz nesneyle tamamen değiştirir.

Peki, jest kullanırken bunu yapabilirsiniz, çünkü jest, çalıştığı her modülün etrafında bir çalışma zamanı oluşturur ve modüle bir "çengelli" sürüm enjekte eder, ancak bunu jest olmadan yapamazsınız.

Ben bunu sahte-ihtiyaç ile elde etmeye çalıştım ama benim için kaynağımda iç içe düzeyleri için işe yaramadı. Github ile ilgili şu konuya bir göz atın: mock-gerektiren her zaman Mocha ile çağrılmaz .

Bunu ele almak için istediğinizi elde etmek için kullanabileceğiniz iki npm modülü oluşturdum.

Bir babel eklentisine ve bir modül dolandırıcısına ihtiyacınız var.

.Babelrc dosyasında aşağıdaki seçeneklerle babel-plugin-mock-required eklentisini kullanın:

...
"plugins": [
        ["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
        ...
]
...

ve test dosyanızda jestlike-mock modülünü aşağıdaki gibi kullanın:

import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
            const firebase = new (require("firebase-mock").MockFirebaseSdk)();
            ...
            return firebase;
});
...

jestlike-mockModül hala çok dumura uğramış ve belgelerin bir sürü yok ama çok kod Orada da yok. Daha eksiksiz bir özellik kümesi için PR'ları takdir ediyorum. Amaç "jest.mock" özelliğinin tamamını yeniden oluşturmak olacaktır.

Jest'in nasıl uygulandığını görmek için "jest-runtime" paketindeki kodu arayabilir. Örneğin bkz. Https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734 , burada bir modülün "otomobilini" oluştururlar.

Umarım yardımcı olur ;)


1

Yapamazsın. Birim test takımınızı, en düşük modüllerin önce test edilmesi ve modül gerektiren yüksek seviye modüllerin daha sonra test edilmesi için oluşturmanız gerekir.

Ayrıca, herhangi bir 3. taraf kodunun ve node.js'nin kendisinin de test edildiğini varsaymalısınız.

Sanırım alaycı çerçevelerin üzerine yakın gelecekte geleceğini göreceksiniz global.require

Gerçekten bir sahte enjekte etmeniz gerekiyorsa, modüler kapsamı ortaya çıkarmak için kodunuzu değiştirebilirsiniz.

// underTest.js
var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.toCrazyCrap();
}

module.exports = underTest;
module.exports.__module = module;

// test.js
function test() {
    var underTest = require("underTest");
    underTest.__module.innerLib = {
        toCrazyCrap: function() { return true; }
    };
    assert.ok(underTest());
}

Bu, API'nize maruz kaldığı .__moduleve herhangi bir kodun kendi tehlikeleriyle modüler kapsama erişebileceği konusunda uyarılmalıdır .


2
Üçüncü taraf kodunun iyi test edildiğini varsaymak, IMO'yu çalıştırmanın harika bir yolu değildir.
henry.oswald

5
@beck çalışmak için harika bir yol. Sizi sadece yüksek kaliteli üçüncü taraf
kodlarıyla

Tamam, kodunuz ve üçüncü taraf kodu arasında entegrasyon testleri yapmamanız gerektiğini düşündüm. Kabul.
henry.oswald

1
Bir "birim test paketi" sadece birim testlerin bir toplamıdır, ancak birim testleri birbirinden bağımsız olmalıdır, bu yüzden birim testindeki birim. Kullanılabilir olması için, birim testleri hızlı ve bağımsız olmalıdır, böylece bir birim testi başarısız olduğunda kodun nerede kırıldığını açıkça görebilirsiniz.
Andreas Berheim Brudin

Bu benim için işe yaramadı. Modül nesnesi "var innerLib ..." vb.
Göstermez

1

Alay kitaplığı kullanabilirsiniz :

describe 'UnderTest', ->
  before ->
    mockery.enable( warnOnUnregistered: false )
    mockery.registerMock('./path/to/innerLib', { doComplexStuff: -> 'Complex result' })
    @underTest = require('./path/to/underTest')

  it 'should compute complex value', ->
    expect(@underTest()).to.eq 'Complex result'

1

Meraklılar için modülleri taklit etmek için basit kod

Gizli sos olduğu için , require.cacheve not require.resolveyöntemini manipüle ettiğiniz parçalara dikkat edin .

class MockModules {  
  constructor() {
    this._resolvedPaths = {} 
  }
  add({ path, mock }) {
    const resolvedPath = require.resolve(path)
    this._resolvedPaths[resolvedPath] = true
    require.cache[resolvedPath] = {
      id: resolvedPath,
      file: resolvedPath,
      loaded: true,
      exports: mock
    }
  }
  clear(path) {
    const resolvedPath = require.resolve(path)
    delete this._resolvedPaths[resolvedPath]
    delete require.cache[resolvedPath]
  }
  clearAll() {
    Object.keys(this._resolvedPaths).forEach(resolvedPath =>
      delete require.cache[resolvedPath]
    )
    this._resolvedPaths = {}
  }
}

Gibi kullanın :

describe('#someModuleUsingTheThing', () => {
  const mockModules = new MockModules()
  beforeAll(() => {
    mockModules.add({
      // use the same require path as you normally would
      path: '../theThing',
      // mock return an object with "theThingMethod"
      mock: {
        theThingMethod: () => true
      }
    })
  })
  afterAll(() => {
    mockModules.clearAll()
  })
  it('should do the thing', async () => {
    const someModuleUsingTheThing = require('./someModuleUsingTheThing')
    expect(someModuleUsingTheThing.theThingMethod()).to.equal(true)
  })
})

AMA ... proxyquire oldukça harika ve bunu kullanmalısınız. Gereksinimlerinizi geçersiz kılma işlemlerini yalnızca testlere yerelleştirilmiş olarak tutar ve kesinlikle tavsiye ederim.

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.