Bagaimana cara menguji modul Node.js yang memerlukan modul lain dan bagaimana mengejek fungsi memerlukan global?


156

Ini adalah contoh sepele yang menggambarkan inti permasalahan saya:

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

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

module.exports = underTest;

Saya mencoba menulis unit test untuk kode ini. Bagaimana saya bisa mengejek persyaratan untuk innerLibtanpa mengolok-olok requirefungsi sepenuhnya?

Jadi ini saya mencoba untuk mengejek global requiredan mengetahui bahwa itu tidak akan berhasil bahkan untuk melakukan itu:

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

Masalahnya adalah bahwa requirefungsi di dalam underTest.jsfile sebenarnya belum dipermainkan. Itu masih menunjuk ke requirefungsi global . Jadi sepertinya saya hanya bisa mengejek requirefungsi dalam file yang sama dengan yang saya lakukan mengejek. Jika saya menggunakan global requireuntuk memasukkan apa pun, bahkan setelah saya mengganti salinan lokal, file yang diminta masih memiliki requirereferensi global .


Anda harus menimpa global.require. Variabel menulis modulesecara default karena modul memiliki cakupan modul.
Raynos

@ Raynos Bagaimana saya melakukan itu? global.require tidak terdefinisi? Bahkan jika saya menggantinya dengan fungsi saya sendiri, fungsi lain tidak akan pernah menggunakan itu kan?
HMR

Jawaban:


175

Sekarang kamu bisa!

Saya menerbitkan proxyquire yang akan mengatasi kebutuhan global di dalam modul Anda saat Anda mengujinya.

Ini berarti Anda tidak perlu mengubah kode Anda untuk menyuntikkan ejekan untuk modul yang diperlukan.

Proxyquire memiliki api yang sangat sederhana yang memungkinkan penyelesaian modul yang Anda coba untuk menguji dan meneruskan ejekan / bertopik untuk modul yang diperlukan dalam satu langkah sederhana.

@Raynos benar bahwa secara tradisional Anda harus menggunakan solusi yang tidak terlalu ideal untuk mencapai itu atau melakukan pengembangan bottom-up sebagai gantinya

Yang merupakan alasan utama mengapa saya membuat proxyquire - untuk memungkinkan pengembangan yang didorong oleh pengujian top-down tanpa kesulitan.

Lihat dokumentasi dan contoh untuk mengukur apakah itu sesuai dengan kebutuhan Anda.


5
Saya menggunakan proxyquire dan saya tidak bisa mengatakan hal-hal yang cukup baik. Itu menyelamatkan saya! Saya ditugaskan untuk menulis tes melati-simpul untuk aplikasi yang dikembangkan di appcelerator Titanium yang memaksa beberapa modul menjadi jalur absolut dan banyak ketergantungan melingkar. proxyquire biarkan saya menghentikan celah itu dan mengejek cruft saya tidak perlu untuk setiap tes. (Dijelaskan di sini ). Terima kasih banyak!
Sukima

Senang mendengar bahwa proxyquire membantu Anda menguji kode Anda dengan benar :)
Thorsten Lorenz

1
@ThorstenLorenz sangat bagus, saya akan def. menggunakan proxyquire!
bevacqua

Fantastis! Ketika saya melihat jawaban yang diterima bahwa "Anda tidak bisa" Saya berpikir, "Ya Tuhan, serius ?!" tapi ini benar-benar menyelamatkannya.
Chadwick

3
Bagi Anda yang menggunakan Webpack, jangan habiskan waktu untuk meneliti permintaan proxy. Itu tidak mendukung Webpack. Sebagai gantinya, saya mencari inject-loader ( github.com/plasticine/inject-loader ).
Artif3x

116

Pilihan yang lebih baik dalam hal ini adalah dengan mengejek metode modul yang akan dikembalikan.

Untuk lebih baik atau lebih buruk, sebagian besar modul node.js adalah lajang; dua potong kode yang memerlukan () modul yang sama mendapatkan referensi yang sama ke modul itu.

Anda dapat memanfaatkan ini dan menggunakan sesuatu seperti sinon untuk mengejek item yang diperlukan. Tes mocha berikut:

// 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 memiliki integrasi yang baik dengan chai untuk membuat pernyataan, dan saya menulis modul untuk mengintegrasikan sinon dengan moka untuk memungkinkan pembersihan mata-mata / rintisan yang lebih mudah (untuk menghindari uji polusi.)

Perhatikan bahwa underTest tidak dapat diejek dengan cara yang sama, karena underTest hanya mengembalikan fungsi.

Pilihan lain adalah menggunakan Jest mock. Ikuti di halaman mereka


1
Sayangnya, modul node.js TIDAK dijamin menjadi lajang, seperti yang dijelaskan di sini: justjs.com/posts/…
FrontierPsycho

4
@FrontierPsycho beberapa hal: Pertama, sejauh pengujian yang bersangkutan, artikel itu tidak relevan. Selama Anda menguji dependensi Anda (dan bukan dependensi dependensi) semua kode Anda akan mendapatkan objek yang sama kembali ketika Anda require('some_module'), karena semua kode Anda membagikan dir node_modules yang sama. Kedua, artikel ini menggabungkan namespace dengan lajang, yang semacam ortogonal. Ketiga, artikel itu sudah sangat tua (sejauh node.js yang bersangkutan) sehingga apa yang mungkin telah valid kembali pada hari itu mungkin tidak valid sekarang.
Elliot Foster

2
Hm Kecuali salah satu dari kita benar-benar menggali kode yang membuktikan satu titik atau yang lain, saya akan pergi dengan solusi Anda injeksi ketergantungan, atau hanya sekadar melewati objek di sekitar, itu bukti lebih aman dan lebih di masa depan.
FrontierPsycho

1
Saya tidak yakin apa yang Anda minta untuk dibuktikan. Sifat singleton (cache) dari modul simpul umumnya dipahami. Ketergantungan injeksi, sementara rute yang baik, bisa menjadi jumlah yang adil lebih banyak plat ketel dan lebih banyak kode. DI lebih umum dalam bahasa yang diketik secara statis, di mana lebih sulit untuk menusuk mata-mata / bertopik / mengolok-olok kode Anda secara dinamis. Beberapa proyek yang telah saya lakukan selama tiga tahun terakhir menggunakan metode yang dijelaskan dalam jawaban saya di atas. Ini yang termudah dari semua metode, meskipun saya menggunakannya hemat.
Elliot Foster

1
Saya sarankan Anda membaca sinon.js. Jika Anda menggunakan sinon (seperti dalam contoh di atas) Anda akan baik innerLib.toCrazyCrap.restore()dan restub, atau hubungi sinon via sinon.stub(innerLib, 'toCrazyCrap')yang memungkinkan Anda untuk mengubah bagaimana berperilaku rintisan: innerLib.toCrazyCrap.returns(false). Selain itu, rewire tampaknya sangat mirip dengan proxyquireekstensi di atas.
Elliot Foster

11

Saya menggunakan mock-butuhkan . Pastikan Anda mendefinisikan tiruan Anda sebelum Anda requiremenguji modul.


Juga bagus untuk melakukan stop (<file>) atau stopAll () segera sehingga Anda tidak mendapatkan file cache dalam tes di mana Anda tidak ingin mock.
Justin Kruse

1
Ini membantu satu ton.
pukulan keras

2

Mengejek requireterasa seperti hack jahat bagi saya. Saya pribadi akan mencoba menghindarinya dan memperbaiki kode untuk membuatnya lebih dapat diuji. Ada berbagai pendekatan untuk menangani ketergantungan.

1) lulus dependensi sebagai argumen

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

Ini akan membuat kode dapat diuji secara universal. Kelemahannya adalah Anda harus melewati dependensi, yang dapat membuat kode terlihat lebih rumit.

2) mengimplementasikan modul sebagai kelas, kemudian menggunakan metode / properti kelas untuk mendapatkan dependensi

(Ini adalah contoh yang dibuat-buat, di mana penggunaan kelas tidak masuk akal, tetapi menyampaikan gagasan itu) (contoh ES6)

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

class underTestClass {
    getInnerLib () {
        return innerLib
    }

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

Sekarang Anda dapat dengan mudah mematikan getInnerLibmetode untuk menguji kode Anda. Kode menjadi lebih verbose, tetapi juga lebih mudah untuk diuji.


1
Saya tidak berpikir itu hacky karena Anda menganggap ... ini adalah inti dari cemoohan. Mengejek dependensi yang diperlukan membuat segalanya menjadi sangat sederhana sehingga memberikan kontrol kepada pengembang tanpa mengubah struktur kode. Metode Anda terlalu bertele-tele dan karenanya sulit dipikirkan. Saya memilih proxyrequire atau mock-butuhkan dari ini; saya tidak melihat masalah di sini. Kode bersih dan mudah dipikirkan dan diingat sebagian besar orang yang membaca ini sudah memiliki kode tertulis yang Anda ingin mereka rumit. Jika lib ini adalah peretasan, maka mengejek dan mematikan juga peretasan menurut definisi Anda & harus dihentikan.
Emmanuel Mahuni

1
Masalah dengan pendekatan no.1 adalah bahwa Anda meneruskan detail implementasi internal ke stack. Dengan banyak lapisan maka menjadi lebih rumit untuk menjadi konsumen modul Anda. Ini dapat bekerja dengan wadah seperti pendekatan IOC sehingga sehingga dependensi secara otomatis disuntikkan untuk Anda, namun rasanya karena kita sudah memiliki dependensi yang disuntikkan dalam modul simpul melalui pernyataan impor, maka masuk akal untuk dapat mengejek mereka di tingkat itu .
magritte

1) Ini hanya memindahkan masalah ke file lain 2) masih memuat modul lain dan dengan demikian membebani kinerja overhead, dan mungkin menyebabkan efek samping (seperti colorsmodul populer yang mengacaukan String.prototype)
ThomasR

2

Jika Anda pernah menggunakan jest, maka Anda mungkin akrab dengan fitur tiruan jest.

Dengan menggunakan "jest.mock (...)" Anda cukup menentukan string yang akan muncul dalam pernyataan-kebutuhan dalam kode Anda di suatu tempat dan setiap kali modul diperlukan menggunakan string itu, objek-mock akan dikembalikan sebagai gantinya.

Sebagai contoh

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

akan sepenuhnya mengganti semua impor / persyaratan "firebase-admin" dengan objek yang Anda kembalikan dari "pabrik" -fungsi.

Nah, Anda bisa melakukannya saat menggunakan jest karena jest menciptakan runtime di sekitar setiap modul yang dijalankannya dan menyuntikkan versi "doyan" dari kebutuhan ke dalam modul, tetapi Anda tidak akan bisa melakukan ini tanpa bercanda.

Saya telah mencoba untuk mencapai ini dengan mock-butuhkan tetapi bagi saya itu tidak berhasil untuk tingkat bersarang di sumber saya. Lihat masalah berikut di github: mock-need tidak selalu dipanggil dengan Mocha .

Untuk mengatasi ini, saya telah membuat dua modul npm yang dapat Anda gunakan untuk mencapai apa yang Anda inginkan.

Anda memerlukan satu babel-plugin dan pengejek modul.

Dalam .babelrc Anda gunakan plugin babel-plugin-mock-require dengan opsi berikut:

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

dan dalam file pengujian Anda gunakan modul jestlike-mock seperti:

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

The jestlike-mockmodul ini masih sangat belum sempurna dan tidak memiliki banyak dokumentasi tapi tidak ada banyak kode baik. Saya menghargai setiap PR untuk set fitur yang lebih lengkap. Tujuannya adalah untuk membuat ulang seluruh fitur "jest.mock".

Untuk melihat bagaimana jest mengimplementasikan seseorang dapat mencari kode dalam paket "jest-runtime". Lihat https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734 misalnya, di sini mereka menghasilkan "automock" modul.

Semoga itu bisa membantu;)


1

Kamu tidak bisa Anda harus membangun unit test unit Anda sehingga modul terendah diuji terlebih dahulu dan modul tingkat lebih tinggi yang membutuhkan modul diuji setelahnya.

Anda juga harus berasumsi bahwa kode pihak ketiga dan node.js sendiri sudah teruji dengan baik.

Saya kira Anda akan melihat kerangka mengejek tiba dalam waktu dekat yang menimpa global.require

Jika Anda benar-benar harus menyuntikkan mock Anda dapat mengubah kode Anda untuk mengekspos ruang lingkup modular.

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

Berhati-hatilah karena ini akan memaparkan .__moduleAPI Anda dan kode apa pun dapat mengakses lingkup modular dengan bahaya sendiri.


2
Dengan asumsi bahwa kode pihak ketiga diuji dengan baik bukan cara yang bagus untuk bekerja IMO.
henry.oswald

5
@beck itu cara yang bagus untuk bekerja. Ini memaksa Anda untuk hanya bekerja dengan kode pihak ketiga berkualitas tinggi atau menulis semua bagian dari kode Anda sehingga setiap ketergantungan diuji dengan baik
Raynos

Oke, saya pikir Anda merujuk untuk tidak melakukan tes integrasi antara kode Anda dan kode pihak ketiga. Sepakat.
henry.oswald

1
"Unit test suite" hanya kumpulan tes unit, tetapi unit test harus independen satu sama lain, karenanya unit dalam unit test. Agar dapat digunakan, tes unit harus cepat dan independen, sehingga Anda dapat dengan jelas melihat di mana kode rusak ketika tes unit gagal.
Andreas Berheim Brudin

Ini tidak berhasil untuk saya. Objek modul tidak memaparkan "var innerLib ..." dll.
AnitKryst

1

Anda dapat menggunakan perpustakaan tiruan :

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

Kode sederhana untuk mengejek modul untuk yang penasaran

Perhatikan bagian di mana Anda memanipulasi require.cachedan perhatikan require.resolvemetode karena ini adalah saus rahasia.

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 = {}
  }
}

Gunakan seperti :

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

TAPI ... proxyquire sangat luar biasa dan Anda harus menggunakannya. Itu membuat Anda memerlukan override terlokalisasi untuk tes saja dan saya sangat merekomendasikannya.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.