unit testing fungsi pribadi dengan mocha dan node.js

131

Saya menggunakan mocha untuk menguji unit aplikasi yang ditulis untuk node.js

Saya ingin tahu apakah mungkin untuk menguji fungsi unit yang belum diekspor dalam sebuah modul.

Contoh:

Saya memiliki banyak fungsi yang didefinisikan seperti ini di foobar.js

function private_foobar1(){
    ...
}

function private_foobar2(){
    ...
}

dan beberapa fungsi yang diekspor sebagai publik:

exports.public_foobar3 = function(){
    ...
}

Test case disusun sebagai berikut:

describe("private_foobar1", function() {
    it("should do stuff", function(done) {
        var stuff = foobar.private_foobar1(filter);
        should(stuff).be.ok;
        should(stuff).....

Jelas ini tidak berfungsi, karena private_foobar1tidak diekspor.

Apa cara yang benar untuk menguji metode pribadi unit? Apakah moka memiliki beberapa metode bawaan untuk melakukan itu?

fstab
sumber

Jawaban:

64

Jika fungsi tidak diekspor oleh modul, itu tidak dapat dipanggil dengan kode uji di luar modul. Itu karena cara kerja JavaScript, dan Mocha tidak dapat dengan sendirinya mengelak dari ini.

Dalam beberapa kasus di mana saya menentukan bahwa pengujian fungsi pribadi adalah hal yang benar untuk dilakukan, apa yang telah saya lakukan adalah mengatur beberapa variabel lingkungan yang diperiksa modul saya untuk menentukan apakah itu berjalan dalam pengaturan pengujian atau tidak. Jika itu berjalan dalam pengaturan pengujian, maka itu mengekspor fungsi tambahan yang kemudian dapat saya panggil selama pengujian.

Kata "lingkungan" digunakan secara longgar di sini. Ini mungkin berarti memeriksa process.envatau sesuatu yang lain yang dapat berkomunikasi dengan modul "Anda sedang diuji sekarang". Contoh di mana saya harus melakukan ini berada dalam lingkungan RequireJS, dan saya telah menggunakan module.configuntuk tujuan ini.

Louis
sumber
2
Nilai ekspor kondisional tampaknya tidak kompatibel dengan modul ES6. Saya mendapatkanSyntaxError: 'import' and 'export' may only appear at the top level
aij
1
@aij ya karena ES6 ekspor statis yang tidak dapat Anda gunakan import, exportdi dalam blok. Akhirnya Anda akan dapat mencapai hal semacam ini di ES6 dengan System loader. Salah satu cara untuk menyiasatinya sekarang adalah dengan menggunakan module.exports = process.env.NODE_ENV === 'production' ? require('prod.js') : require('dev.js')dan menyimpan perbedaan kode ES6 Anda di file masing-masing.
cchamberlain
2
Saya kira jika Anda memiliki cakupan penuh, maka Anda sedang menguji semua fungsi pribadi Anda, apakah Anda telah mengeksposnya atau tidak.
Ziggy
1
@aij Anda dapat mengekspor secara kondisional ... lihat jawaban ini: stackoverflow.com/questions/39583958/…
RayLoveless
187

Lihat modul rewire . Ini memungkinkan Anda untuk mendapatkan (dan memanipulasi) variabel dan fungsi pribadi dalam suatu modul.

Jadi dalam kasus Anda penggunaannya akan seperti:

var rewire = require('rewire'),
    foobar = rewire('./foobar'); // Bring your module in with rewire

describe("private_foobar1", function() {

    // Use the special '__get__' accessor to get your private function.
    var private_foobar1 = foobar.__get__('private_foobar1');

    it("should do stuff", function(done) {
        var stuff = private_foobar1(filter);
        should(stuff).be.ok;
        should(stuff).....
barwin
sumber
3
@ Jaro Sebagian besar kode saya baik dalam bentuk modul AMD, yang rewire tidak dapat menangani (karena modul AMD berfungsi tetapi rewire tidak dapat menangani "variabel di dalam fungsi"). Atau diubah, skenario lain yang tidak bisa ditangani oleh rewire. Sebenarnya, orang-orang yang akan melihat rewire sebaiknya membaca terlebih dahulu batasan (ditautkan sebelumnya) sebelum mereka mencoba menggunakannya. Saya tidak memiliki satu aplikasi yang a) perlu mengekspor barang-barang "pribadi" dan b) tidak mengalami batasan rewire.
Louis
1
Hanya sebagian kecil, cakupan kode mungkin gagal untuk mengambil tes yang ditulis seperti ini. Setidaknya itulah yang saya lihat menggunakan alat cakupan bawaan Jest.
Mike Stead
Rewire juga tidak cocok dengan alat auto-mocking jest. Saya masih mencari cara untuk memanfaatkan manfaat lelucon dan mengakses beberapa vars pribadi.
btburton42
Jadi saya mencoba membuat ini bekerja tetapi saya menggunakan naskah, yang saya duga menyebabkan masalah ini. Pada dasarnya saya mendapatkan error berikut: Cannot find module '../../package' from 'node.js'. Adakah yang akrab dengan ini?
clu
rewire bekerja dengan baik .ts, typescriptsaya menjalankan menggunakan ts-node @clu
muthukumar selvaraj
24

Berikut ini adalah alur kerja yang sangat bagus untuk menguji metode pribadi Anda yang dijelaskan oleh Philip Walton, seorang insinyur Google di blog-nya.

Prinsip

  • Tulis kode Anda secara normal
  • Bind metode pribadi Anda ke objek dalam blok kode terpisah, tandai dengan sebagai _contoh
  • Kelilingi blok kode itu dengan memulai dan mengakhiri komentar

Kemudian gunakan tugas build atau sistem build Anda sendiri (untuk contoh grunt-strip-code) untuk menghapus blok ini untuk build produksi.

Build pengujian Anda memiliki akses ke api pribadi Anda, dan build produksi Anda belum.

Potongan

Tulis kode Anda seperti ini:

var myModule = (function() {

  function foo() {
    // private function `foo` inside closure
    return "foo"
  }

  var api = {
    bar: function() {
      // public function `bar` returned from closure
      return "bar"
    }
  }

  /* test-code */
  api._foo = foo
  /* end-test-code */

  return api
}())

Dan tugas kasarmu seperti itu

grunt.registerTask("test", [
  "concat",
  "jshint",
  "jasmine"
])
grunt.registerTask("deploy", [
  "concat",
  "strip-code",
  "jshint",
  "uglify"
])

Lebih dalam

Dalam artikel selanjutnya , ini menjelaskan "mengapa" dari "pengujian metode pribadi"

Rémi Becheras
sumber
1
Ditemukan juga plugin webkit yang sepertinya dapat mendukung alur kerja yang serupa: webpack-strip-block
JRulle
21

Jika Anda lebih suka membuatnya sederhana, cukup ekspor anggota pribadi juga, tetapi jelas dipisahkan dari API publik dengan beberapa konvensi, misalnya awali mereka dengan _atau sarang mereka di bawah satu objek pribadi .

var privateWorker = function() {
    return 1
}

var doSomething = function() {
    return privateWorker()
}

module.exports = {
    doSomething: doSomething,
    _privateWorker: privateWorker
}
gin terkenal
sumber
7
Saya sudah melakukan ini dalam kasus di mana seluruh modul benar-benar dimaksudkan untuk menjadi pribadi dan bukan untuk konsumsi umum. Tetapi untuk modul untuk keperluan umum, saya lebih suka mengekspos apa yang saya butuhkan untuk pengujian hanya ketika kode sedang diuji. Memang benar bahwa pada akhirnya tidak ada yang akan mencegah seseorang untuk sampai ke masalah pribadi dengan memalsukan lingkungan pengujian tetapi ketika seseorang melakukan debugging pada aplikasi mereka sendiri, saya lebih suka mereka tidak melihat simbol yang tidak perlu bagian dari API publik. Dengan cara ini tidak ada godaan langsung untuk menyalahgunakan API untuk tujuan yang tidak dirancang untuknya.
Louis
2
Anda juga dapat menggunakan sintaks bersarang {... privat : {pekerja: pekerja}}
Jason
2
Jika modul semua fungsi murni, maka saya tidak melihat kelemahan untuk melakukan ini. Jika Anda menjaga dan bermutasi, maka waspadalah ...
Ziggy
5

Saya membuat paket npm untuk tujuan ini yang mungkin berguna bagi Anda: memerlukan-dari

Pada dasarnya Anda mengekspos metode non-publik dengan:

module.testExports = {
    private_foobar1: private_foobar1,
    private_foobar2: private_foobar2,
    ...
}

catatan: testExports dapat berupa nama valid yang Anda inginkan, kecuali exportstentu saja.

Dan dari modul lain:

var requireFrom = require('require-from');
var private_foobar1 = requireFrom('testExports', './path-to-module').private_foobar1;
DEADB17
sumber
1
Saya tidak melihat manfaat praktis dari metode ini. Itu tidak membuat simbol "pribadi" lebih pribadi. (Siapa saja dapat memanggil requireFromdengan parameter yang tepat.) Juga, jika modul dengan textExportsdimuat oleh requirepanggilan sebelum requireFrom memuatnya, requireFromakan kembali undefined. (Saya baru saja mengujinya.) Meskipun seringkali dimungkinkan untuk mengontrol urutan muatan modul, itu tidak selalu praktis. (Sebagaimana dibuktikan oleh beberapa pertanyaan Mocha pada SO.) Solusi ini juga umumnya tidak akan bekerja dengan modul tipe AMD. (Saya memuat modul AMD di Node setiap hari untuk pengujian.)
Louis
Seharusnya tidak bekerja dengan modul AMD! Node.js menggunakan common.js dan jika Anda mengubahnya untuk menggunakan AMD, maka Anda melakukannya di luar norma.
jemiloii
@JemiloII Ratusan pengembang menggunakan Node.js setiap hari untuk menguji modul AMD. Tidak ada yang "di luar norma" dalam melakukan itu. Yang paling bisa Anda katakan adalah bahwa Node.js tidak datang dengan AMD loader tetapi ini tidak banyak bicara, mengingat Node memberikan kait eksplisit untuk memperluas loadernya untuk memuat format pengembang apa pun yang ingin dikembangkan.
Louis
Itu di luar norma. Jika Anda harus memasukkan loader amd secara manual, itu bukan norma untuk node.js. Saya jarang melihat AMD untuk kode node.js. Saya akan melihatnya untuk browser, tetapi simpul. Tidak. Saya tidak mengatakan itu tidak dilakukan, hanya pertanyaan dan jawaban yang kami komentari, tidak mengatakan apa-apa tentang modul amd. Jadi tanpa ada yang menyatakan bahwa mereka menggunakan loader amd, ekspor node, seharusnya tidak bekerja dengan amd. Meskipun saya ingin mencatat, commonjs mungkin sedang dalam perjalanan keluar dengan ekspor es6. Saya hanya berharap suatu hari kita semua bisa menggunakan satu metode ekspor saja.
jemiloii
4

Saya telah menambahkan fungsi tambahan yang saya beri nama Internal () dan mengembalikan semua fungsi pribadi dari sana. Ini internal () function kemudian diekspor. Contoh:

function Internal () {
  return { Private_Function1, Private_Function2, Private_Function2}
}

// Exports --------------------------
module.exports = { PublicFunction1, PublicFunction2, Internal }

Anda dapat memanggil fungsi internal seperti ini:

let test = require('.....')
test.Internal().Private_Function1()

Saya suka solusi ini karena:

  • hanya satu fungsi Internal () yang selalu diekspor. Ini internal () fungsi selalu digunakan untuk menguji fungsi-fungsi pribadi.
  • Sederhana untuk diterapkan
  • Dampak rendah pada kode produksi (hanya satu fungsi tambahan)
Perez Lamed van Niekerk
sumber
2

Saya mengikuti jawaban @barwin dan memeriksa bagaimana tes unit dapat dilakukan dengan modul rewire . Saya dapat mengkonfirmasi bahwa solusi ini hanya berfungsi.

Modul harus diminta dalam dua bagian - bagian publik dan bagian pribadi. Untuk fungsi publik, Anda dapat melakukannya dengan cara standar:

const { public_foobar3 } = require('./foobar');

Untuk ruang lingkup pribadi:

const privateFoobar = require('rewire')('./foobar');
const private_foobar1 = privateFoobar .__get__('private_foobar1');
const private_foobar2 = privateFoobar .__get__('private_foobar2');

Untuk mengetahui lebih banyak tentang subjek, saya membuat contoh kerja dengan pengujian modul lengkap, pengujian mencakup ruang lingkup pribadi dan publik.

Untuk informasi lebih lanjut, saya mendorong Anda untuk memeriksa artikel ( https://medium.com/@macsikora/how-to-test-private-functions-of-es6-module-fb8c1345b25f ) yang sepenuhnya menggambarkan subjek, termasuk contoh kode.

Maciej Sikora
sumber
2

Saya tahu bahwa ini belum tentu jawaban yang Anda cari, tetapi apa yang saya temukan adalah bahwa sebagian besar waktu jika fungsi pribadi layak diuji, ada baiknya berada di file sendiri.

Misalnya alih-alih memiliki metode pribadi dalam file yang sama dengan yang publik, seperti ini ...

src / hal / PublicInterface.js


function helper1 (x) {
    return 2 * x;
}

function helper2 (x) {
    return 3 * x;
}

export function publicMethod1(x) {
    return helper1(x);
}

export function publicMethod2(x) {
    return helper1(x) + helper2(x);
}

... Anda membaginya seperti ini:

src / hal / PublicInterface.js

import {helper1} from './internal/helper1.js';
import {helper2} from './internal/helper2.js';

export function publicMethod1(x) {
    return helper1(x);
}

export function publicMethod2(x) {
    return helper1(x) + helper2(x);
}

src / hal / internal / helper1.js

export function helper1 (x) {
    return 2 * x;
}

src / hal / internal / helper2.js

export function helper2 (x) {
    return 3 * x;
}

Dengan begitu, Anda dapat dengan mudah menguji helper1dan helper2apa adanya, tanpa menggunakan Rewire dan "sihir" lainnya (yang, saya temukan, memiliki poin rasa sakitnya sendiri saat debugging, atau ketika Anda mencoba untuk bergerak menuju TypeScript, belum lagi lebih miskin dimengerti untuk kolega baru). Dan mereka yang berada di sub-folder bernama internal, atau sesuatu seperti itu, akan membantu menghindari penggunaan mereka secara tidak sengaja di tempat yang tidak diinginkan.


PS: Masalah umum lainnya dengan metode "pribadi" adalah bahwa jika Anda ingin menguji publicMethod1dan publicMethod2dan mengejek pembantu, sekali lagi, Anda biasanya perlu sesuatu seperti Rewire untuk melakukan itu. Namun, jika mereka berada di file terpisah, Anda dapat menggunakan Proxyquire untuk melakukannya, yang, tidak seperti Rewire, tidak memerlukan perubahan apa pun pada proses pembuatan Anda, mudah dibaca dan di-debug, dan berfungsi dengan baik bahkan dengan TypeScript.

Dániel Kis-Nagy
sumber
1

Untuk membuat metode pribadi tersedia untuk pengujian, saya melakukan ini:

const _myPrivateMethod: () => {};

const methods = {
    myPublicMethod1: () => {},
    myPublicMethod2: () => {},
}

if (process.env.NODE_ENV === 'test') {
    methods._myPrivateMethod = _myPrivateMethod;
}

module.exports = methods;
MFB
sumber