Sinon: Ide untuk pencapaian masa depan

Dibuat pada 13 Sep 2017  ·  31Komentar  ·  Sumber: sinonjs/sinon

Latar belakang

sinon.stub / sandbox.stub telah menjadi wastafel dapur
perilaku yang dapat dikonfigurasi dengan masalah yang seringkali sulit ditemukan dan diperbaiki tanpa regresi.

Saya pikir akar penyebab kesulitannya adalah stub memiliki terlalu banyak tanggung jawab.

Lebih lanjut, stub juga memiliki masalah penggunaan yang disebabkan oleh fakta bahwa perilaku diatur setelah dibuat, dan dapat didefinisikan ulang berkali-kali.

var myStub;

beforeEach(function(){
    myStub = sinon.stub().resolves('apple pie :)');
});

// several hundred lines of tests later
myStub = sinon.stub().rejects('no more pie :(');

// several hundred lines of tests later
// what behaviour does myStub currently have? Can you tell without 
// reading the entire file?
// can you safely change the behaviour without affecting tests further 
// down in the file?

Dan kemudian ada skenario yang lebih membingungkan

var myStub = sinon.stub()
                .withArgs(42)
                .onThirdCall()
                .resolves('apple pie')
                .rejects('no more pie')

Apa itu?

Alih-alih terus menambahkan lebih banyak tanggung jawab ke stub , saya mengusulkan agar kami memperkenalkan anggota baru ke sinon , yang cakupannya jauh lebih sempit .

Yang paling penting yang dapat saya pikirkan adalah pendirian yang tidak dapat diubah untuk suatu fungsi.

Kami kemudian dapat mengetahui apa yang akan kami lakukan tentang properti (sebagai anggota tanggung jawab tunggal yang terpisah, baru).

sinon.fake

A fake (nilai kembalian dari panggilan sinon.fake ) adalah Function murni dan tidak dapat diubah . Ia melakukan satu hal, dan hanya satu hal. Ini memiliki perilaku yang sama pada setiap panggilan. Tidak seperti stub , perilakunya tidak dapat didefinisikan ulang. Jika Anda membutuhkan perilaku yang berbeda, buat fake baru.

Tanggung jawab tunggal

Seorang palsu dapat memiliki salah satu dari tanggung jawab ini

  • menyelesaikan Promise menjadi nilai
  • tolak Promise ke Error
  • mengembalikan nilai
  • melempar Error
  • menghasilkan nilai untuk panggilan balik

Jika Anda ingin/membutuhkan efek samping, dan Anda masih menginginkan antarmuka mata-mata, maka gunakan fungsi sebenarnya, gunakan stub atau buat fungsi khusus

sinon.replace(myObject, myMethod, sandbox.spy(function(args) {
    someFunctionWithSideEffects(args);
});

Melempar kesalahan dengan murah hati

Akan bermurah hati dengan melempar kesalahan ketika pengguna mencoba membuat/menggunakannya dengan cara yang tidak didukung.

// will throw TypeError when `config` argument has more than one property
const fake = sinon.fake({
    resolves: true, 
    returns: true
});

Menggunakan API mata-mata

Kecuali .withArgs , karena itu melanggar kekekalan

Ide penggunaan

// will return a Promise that resolves to 'apple pie'
var fake = sinon.fake({resolves: 'apple pie'})

// will return a Promise that rejects with the provided Error, or 
// creates a generic Error using the input as message
var fake = sinon.fake({rejects: new TypeError('no more pie')});
var fake = sinon.fake({rejects: 'no more pie'});

// returns the value passed
var fake = sinon.fake({returns: 'apple pie'});

// throws the provided Error, or creates a generic Error using the 
// input as message
var fake = sinon.fake({throws: new RangeError('no more pie')});
var fake = sinon.fake({throws: 'no more pie'});

// replace a method with a fake
var fake = sinon.replace(myObject, 'methodName', sandbox.fake({
    returns: 'apple pie'
}))
// .. or use the helper method, which will use `sandbox.replace` and `
// sinon.fake`
var fake = sinon.setFake(global, 'methodName', {
    returns: 'apple pie'
});

// create an async fake
var asyncFake = sinon.asyncFake({
    returns: 'apple pie'
});

Sinonim

Saya tidak tahu apakah fake adalah kata benda terbaik untuk digunakan di sini, tapi saya pikir kita harus mencoba untuk tetap berpegang pada konvensi menggunakan kata benda, dan tidak menyimpang ke kata sifat atau kata kerja.

Perubahan API yang diusulkan

Gunakan kotak pasir default

Ini adalah sesuatu yang telah saya pertimbangkan untuk sementara waktu, mengapa kita tidak membuat kotak pasir default? Jika orang membutuhkan kotak pasir terpisah, mereka masih dapat membuatnya.

Kita harus membuat kotak pasir default yang digunakan untuk semua metode yang diekspos melalui sinon.* .
Ini berarti sinon.stub akan menjadi sama dengan sandbox.stub , yang akan menghilangkan batasan kemampuan untuk mematikan properti menggunakan sinon.stub .

sandbox.replace

Buat sandbox.replace dan gunakan itu untuk semua operasi yang menggantikan apa pun di mana saja. Paparkan ini sebagai sinon.replace dan gunakan kotak pasir default saat digunakan dengan cara ini.

Ini mungkin harus memiliki beberapa validasi input yang serius, jadi itu hanya akan menggantikan fungsi dengan fungsi, pengakses dengan pengakses, dll.

Feature Request Improvement Needs investigation pinned

Komentar yang paling membantu

Oke, saya pikir kami memiliki pemahaman yang sama 👍

Hanya untuk mengulangi, jika kami melewatkan sesuatu dan agar kontributor lain memiliki pemahaman yang sama.

TL;DR

  • semua penggantian akan dilakukan oleh utilitas baru: sandbox.replace (saat ini tinggal di stub )
  • sinon akan memiliki kotak pasir default, memungkinkan untuk sinon.reset dan sinon.restore (haruskah kita menggabungkannya saja?)
  • sinon.fake — pengganti yang tidak dapat diubah dan dapat diprogram untuk fungsi yang merekam semua panggilan
  • sinon.spy
  • sinon.stub
// effectively a spy that has no target
const fake = sinon.fake()

// spy on a function
const fake = sinon.fake(console.log);
const fake = sinon.fake(function() { return 'apple pie'; });

// a shorthand construction of fake with behaviour
const fake = sinon.fake({
    returns: 'apple pie'
});

// replacing an existing function with a fake
var fakeLog = sinon.fake();
sandbox.replace(console, 'log', fakeLog);

Pada keadaan ini, proposal hanya berhubungan dengan Function . Kita perlu mempertimbangkan apa yang harus dilakukan tentang properti dan pengakses non-fungsi. Paling tidak, kita harus melihat apakah kita dapat membatasi sandbox.replace untuk hanya mengizinkan penggantian waras.

Semua 31 komentar

Ping ke @sinonjs/core

Saran yang bagus, Morgan. Terima kasih telah mengangkat ini. Saya juga berpikir bahwa stub API membingungkan dan saya menyukai semua saran Anda. Berikut adalah beberapa pemikiran:

sinon.fake

Saya setuju bahwa kekekalan adalah kuncinya di sini. Kami dapat mengizinkan beberapa kasus penggunaan "waras" yang saat ini dimungkinkan dengan stub.

Misalnya, ini bisa menjadi kasus penggunaan yang valid untuk menghasilkan dan mengembalikan:

sinon.fake({
  yields: [null, 42],
  returns: true
})

Kita bisa mengecek mana yang masuk akal dan mana yang tidak.

Juga, jika kami mendukung callsThrough: true sebagai konfigurasi (yang tidak valid dalam kombinasi dengan properti perilaku mana pun), pemalsuan baru juga dapat digunakan sebagai ganti API "mata-mata". Ini akan lebih menjelaskan diri sendiri daripada mempelajari apa arti "mata-mata" dan "rintisan" dalam bahasa Sinon

Gunakan kotak pasir default

Meskipun saya menyukai ide ini, itu berarti bahwa memanggil sinon.restore() setelah pengujian dapat mengembalikan beberapa sisa dari pengujian lain dan menghasilkan hasil yang mengejutkan - atau kegagalan pengujian yang sebelumnya berhasil. Hal brilian yang memungkinkan ini adalah mengatur ulang kotak pasir global di beforeEach untuk meningkatkan isolasi pengujian. 👍

kotak pasir.ganti

Saya sangat menyukai ini. Saya memahami ini sebagai utilitas "biarkan saya menempelkan benda ini di sana", bukan?

Selain itu, jika kami mendukung callThrough: true sebagai konfigurasi (yang tidak valid jika digabungkan dengan properti perilaku mana pun), fakes baru juga dapat digunakan sebagai ganti API "mata-mata". Ini akan lebih menjelaskan diri sendiri daripada mempelajari apa arti "mata-mata" dan "rintisan" dalam bahasa Sinon

Apakah itu berarti kita tidak membutuhkan spy atau stub sama sekali?

kotak pasir.ganti

Saya sangat menyukai ini. Saya memahami ini sebagai utilitas "biarkan saya menempelkan benda ini di sana", bukan?

Ya, itu idenya. Alih-alih membebani metode yang sama ( sinon.stub ) untuk melakukan banyak, banyak hal, miliki metode eksplisit yang hanya melakukan satu hal

Seperti yang Anda soroti, fake API mungkin tidak akan mendukung semua yang saat ini dimungkinkan dengan spies dan stub. Tapi ya, menurut saya fake API adalah kesempatan untuk menyatukan fungsionalitas stub dan spy .

Meskipun saya menyukai ide ini, itu berarti bahwa memanggil sinon.restore() setelah pengujian dapat mengembalikan beberapa sisa dari pengujian lain dan menghasilkan hasil yang mengejutkan - atau kegagalan pengujian yang sebelumnya berhasil. Hal brilian yang memungkinkan ini adalah mengatur ulang kotak pasir global di beforeEach untuk meningkatkan isolasi pengujian. 👍

Ini tentu saja merupakan perubahan besar, dan tidak boleh dianggap enteng.

Saat membuat fake , jika Anda tidak memberikannya konfigurasi perilaku, itu akan setara dengan spy .

// ~spy, records all calls, has no behaviour
const fake = sinon.fake();

// ~stub, records all calls, returns 'apple pie'
const fake = sinon.fake({
    returns: 'apple pie'
});

Bagaimana Anda membuat rintisan yang tidak melakukan apa-apa?

Bagaimana Anda membuat rintisan yang tidak melakukan apa-apa?

Saya tidak yakin saya sepenuhnya memahami pertanyaan Anda ... tapi begini

// a fake that has no behaviour
const fake = sinon.fake();

// put it in place of an existing method
sandbox.replace(myObject, 'someMethod', fake);

Ah, saya rasa saya mengerti apa yang Anda maksud sekarang: A fake selalu stub . Ketika Anda mengatakan ~spy, records all calls Saya mengerti "memanggil ke fungsi asli". Namun, fake tidak memiliki pengetahuan tentang fungsi yang diganti – itulah yang dilakukan sandbox.replace .

Jadi dengan mengingat hal itu, berikut adalah proposal lain bagaimana kita dapat melipat fungsi spy saat ini (seperti dalam memanggil) menjadi palsu baru:

const fake = sinon.fake(function () {
   // Any custom function
});

Fungsi yang diberikan akan dipanggil oleh yang palsu. API ini tidak memungkinkan untuk mencampurnya dengan perilaku lain. Faktanya, objek konfigurasi akan membuat fungsi yang mengimplementasikan perilaku yang ditentukan dan kemudian meneruskannya ke yang palsu.

Implementasi sandbox.spy(object, method) kemudian bisa menjadi ini:

const original = object[method];
const fake = sinon.fake(original);
sandbox.replace(object, method, fake);

Pada dasarnya satu kalimat

Ya. Setelah Anda menyederhanakan berbagai hal, Anda dapat mulai mencampur ulang untuk bersenang-senang dan untung

Namun, jika kita ingin lebih memilih menggunakan fake dan tidak lagi menggunakan spy dan stub , maka sebaiknya kita biarkan saja keduanya.

Saya sedang memikirkan tentang API "berikutnya" di sini. Anda akan membutuhkan sandbox.spy untuk memiliki logika penggantian di suatu tempat. Seperti yang saya pahami, itu harus kompatibel ke belakang. Implementasi stub kemudian dapat dihentikan.

Anda akan membutuhkan sandbox.spy untuk memiliki logika pengganti di suatu tempat. Seperti yang saya pahami, itu harus kompatibel ke belakang. Implementasi rintisan kemudian dapat dihentikan.

Saya tidak yakin saya mengikuti. Bisakah Anda menguraikannya?

Tentu. Seperti yang saya pahami tentang proposal Anda, Anda menginginkan pengganti untuk stub API yang terlalu rumit. Cara penerapan stub saat ini adalah dengan membuat spy dengan fungsi yang mengimplementasikan perilaku tersebut. Apa yang saya sarankan adalah melakukan hal yang sama dengan fake API dan secara internal membuat spy , tetapi kami tidak akan mengembalikan perilaku lagi, karena kami ingin menyingkirkan rantai . Kami baru saja mengembalikan mata-mata. Ini menjadikan implementasi fake sebagai alternatif dari stub dengan fungsi yang dikembalikan kompatibel dengan semua API Sinon saat ini. Apakah itu masuk akal atau saya melewatkan sesuatu?

Oke, saya pikir kami memiliki pemahaman yang sama 👍

Hanya untuk mengulangi, jika kami melewatkan sesuatu dan agar kontributor lain memiliki pemahaman yang sama.

TL;DR

  • semua penggantian akan dilakukan oleh utilitas baru: sandbox.replace (saat ini tinggal di stub )
  • sinon akan memiliki kotak pasir default, memungkinkan untuk sinon.reset dan sinon.restore (haruskah kita menggabungkannya saja?)
  • sinon.fake — pengganti yang tidak dapat diubah dan dapat diprogram untuk fungsi yang merekam semua panggilan
  • sinon.spy
  • sinon.stub
// effectively a spy that has no target
const fake = sinon.fake()

// spy on a function
const fake = sinon.fake(console.log);
const fake = sinon.fake(function() { return 'apple pie'; });

// a shorthand construction of fake with behaviour
const fake = sinon.fake({
    returns: 'apple pie'
});

// replacing an existing function with a fake
var fakeLog = sinon.fake();
sandbox.replace(console, 'log', fakeLog);

Pada keadaan ini, proposal hanya berhubungan dengan Function . Kita perlu mempertimbangkan apa yang harus dilakukan tentang properti dan pengakses non-fungsi. Paling tidak, kita harus melihat apakah kita dapat membatasi sandbox.replace untuk hanya mengizinkan penggantian waras.

Apakah ini berarti sinon.stub() dan sinon.spy() keduanya akan ditinggalkan di masa mendatang demi sinon.fake() , atau hanya dikerjakan ulang secara internal? Jika demikian, maka kita pada dasarnya bergerak menuju pemikiran TestDouble . Tidak selalu merupakan hal yang buruk, IMHO, tetapi mungkin perlu dipertimbangkan bahwa jika banyak orang merasa bahwa mereka perlu mengganti semua panggilan api Sinon mereka untuk sinon.fake() , mereka mungkin juga hanya menggunakan perpustakaan lain (walaupun itu berarti mereka akan kehilangan semua pengetahuan mereka tentang API Sinon).

Apakah ini berarti sinon.stub() dan sinon.spy() keduanya akan ditinggalkan di masa mendatang demi sinon.fake(), atau hanya dibuat ulang secara internal? Jika demikian, maka kita pada dasarnya bergerak menuju pemikiran TestDouble.

Saya kira itu agak tumpang tindih. Motivasi utama saya untuk proposal ini adalah memiliki fungsi palsu dengan perilaku yang tidak dapat diubah.

Tidak selalu merupakan hal yang buruk, IMHO, tetapi mungkin perlu dipertimbangkan bahwa jika banyak orang merasa bahwa mereka perlu mengganti semua panggilan api Sinon mereka untuk sinon.fake(), mereka mungkin juga hanya menggunakan perpustakaan lain (walaupun itu berarti mereka akan kehilangan semua pengetahuan mereka tentang API Sinon).

Jika orang menemukan bahwa perpustakaan lain melayani kebutuhan mereka dengan lebih baik, maka saya senang kami membantu mereka mempelajarinya :)

Tetapi apakah kami akan mempertahankan metode mata-mata dan rintisan atau tidak menggunakannya, dengan beberapa kemungkinan pengurangan fungsionalitas? Itu tidak jelas bagi saya.

Tetapi apakah kami akan mempertahankan metode mata-mata dan rintisan atau tidak menggunakannya, dengan beberapa kemungkinan pengurangan fungsionalitas?

Setelah fake terlihat stabil, maka saya akan menghentikan spy dan stub , dan kemudian memberikannya seperti setahun untuk memberi waktu kepada orang-orang untuk meningkatkan.

Saya pikir kita harus mencoba yang terbaik untuk menyediakan codemod dan dokumentasi yang bagus, untuk membantu orang memindahkan kode mereka

Saya sedang mengerjakan cabang untuk bagian pertama ini (kotak pasir default). Saya telah memfaktorkan ulang kode sehingga sandbox dan collection sekarang menjadi satu. Saya membuat kotak pasir default berfungsi.

Saya akan merapikan komit selama beberapa hari ke depan, dan kemudian membuat cabang di repositori ini untuk API yang diperbarui.

Ini adalah ide bagus, btw ditulis dengan sangat baik.

Saya juga akan menambahkan pemberitahuan penghentian ke bertopik dan mata-mata.

Saya juga berpikir tentang mungkin mengubah melewatkan objek dengan kunci dengan melewatkan fungsi.

Ini akan menambahkan manfaat berikut:

  • Ini akan memungkinkan kita untuk menambahkan type ke fungsi tersebut untuk pengguna yang ingin menggunakan typescript atau jenis pemeriksa statis lainnya
  • Pengguna akan mendapatkan kesalahan saat mencoba menjalankan fungsi untuk perilaku yang tidak ada
  • Kami dapat mendokumentasikan fungsi-fungsi tersebut secara terpisah dan membuat dokumen menjadi lebih baik
  • Kami dapat memberikan kesalahan yang berguna ketika memberikan argumen yang tidak masuk akal untuk perilaku tersebut dan memungkinkan mereka untuk memiliki argumen opsional/lebih dari satu
  • Itu akan membuat hal-hal lebih dapat dikomposisi juga (meskipun saya tidak melihat banyak kasus untuk ini dalam kasus ini) dan memungkinkan orang untuk menggunakan kembali perilaku yang dibuat
  • IMO ini juga akan lebih sederhana daripada memiliki objek dengan perilaku

Oleh karena itu, API akan terlihat seperti ini:

// It would be cool to allow users to import these using destructuring to make code more concise
import { resolves, rejects, returns } from 'sinon/behaviors'; 

var fake = sinon.fake(resolves('apple pie'))

var fake = sinon.fake(rejects(new TypeError('no more pie')));
var fake = sinon.fake(rejects('no more pie'));

var fake = sinon.fake(returns('apple pie'));

var fake = sinon.fake(throws(new RangeError('no more pie'));
var fake = sinon.fake(throws('no more pie'));

Ketika datang untuk mengimplementasikan ini, itu hanya masalah mengembalikan objek yang sangat sederhana seperti yang Anda usulkan. Kemudian, jika kita memiliki lebih dari satu perilaku, kita bisa menggabungkannya.

Juga, ketika datang untuk mencampur hal-hal seperti onThirdCall dan withArgs Saya pikir apa yang terjadi dalam kasus tersebut harus didokumentasikan.

Maaf untuk meninjau ini sangat terlambat. Beberapa bulan terakhir ini sangat sibuk.

@lucasfcosta lihat PR #1586

Masalah ini secara otomatis ditandai sebagai basi karena tidak ada aktivitas terbaru. Ini akan ditutup jika tidak ada aktivitas lebih lanjut yang terjadi. Terima kasih atas kontribusi Anda.

Versi 5.0.0 sebelumnya menyebabkan masalah dengan versi prarilis 5.0.0-next.* yang lebih baru di package.json karena 5.0.0 lebih besar daripada versi prarilis mana pun.

Karena 5.0.0 ada di luar sana, saya pikir nomor pra-rilis next mungkin perlu diubah menjadi 5.0.1-next.1 ?

Saya perhatikan ini karena paket lain yang saya gunakan mendapatkan pesan usang dan package.json-nya bergantung pada "sinon": "^5.0.0-next.4"

npm WARN deprecated [email protected]: this version has been deprecated

Saya tidak yakin apakah ini layak untuk membuka edisi baru untuk masalah pra-rilis, jadi komentar di sini tampaknya paling aman.

Solusi lain adalah merilis versi utama berikutnya. Bagaimana menurut Anda @sinonjs/core?

@mroderick Saya tidak tahu lagi apa semua perubahan untuk v5. Dari tes terakhir saya, itu berfungsi dengan baik dan saya menantikan untuk menggunakan palsu baru. Ini jurusan baru, jadi, kirimkan

Hanya ada satu lagi PR #1764 yang ingin saya gabungkan, sebelum kami merilis versi utama berikutnya.

Saya telah menerbitkan [email protected] , semoga itu akan membuat hidup lebih mudah bagi orang-orang sementara itu.

Terima kasih, saya telah menguji (selalu baik untuk memeriksa ulang) dependensi di package.json dan "sinon": "^5.0.1" memberikan kesalahan sebagaimana mestinya karena tidak ada kecocokan yang ditemukan (belum ada rilis), dan "sinon": "^5.0.1-next.1" berfungsi dengan baik untuk mendapatkan versi itu.

Ini bukan masalah besar, saya hanya berpikir itu layak untuk membuat Anda sadar, terutama ketika saya melihat bahwa v5 telah dikembangkan untuk sementara waktu jadi saya tidak yakin berapa lama sampai dirilis. Saya pikir merilis dalam waktu dekat terdengar seperti ide yang bagus.

fake telah diperkenalkan dengan #1768, yang menjadi [email protected]

Apakah halaman ini membantu?
0 / 5 - 0 peringkat