Haruskah savePeople () diuji unit
Ya, seharusnya begitu. Tetapi cobalah untuk menulis kondisi pengujian Anda dengan cara yang independen dari implementasi. Misalnya, mengubah contoh penggunaan Anda menjadi unit test:
function testSavePeople() {
myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);
assert(myDataStore.containsPerson('Joe'));
assert(myDataStore.containsPerson('Maggie'));
assert(myDataStore.containsPerson('John'));
}
Tes ini melakukan banyak hal:
- itu memverifikasi kontrak fungsi
savePeople()
- tidak peduli dengan implementasi
savePeople()
- itu mendokumentasikan contoh penggunaan
savePeople()
Perhatikan bahwa Anda masih dapat mengejek / mematikan / memalsukan penyimpanan data. Dalam hal ini saya tidak akan memeriksa panggilan fungsi eksplisit, tetapi untuk hasil operasi. Dengan cara ini pengujian saya disiapkan untuk perubahan / refaktor di masa depan.
Misalnya, implementasi penyimpanan data Anda mungkin menyediakan saveBulkPerson()
metode di masa mendatang - sekarang perubahan pada penerapan savePeople()
untuk menggunakan saveBulkPerson()
tidak akan merusak pengujian unit selama saveBulkPerson()
pekerjaan seperti yang diharapkan. Dan jika saveBulkPerson()
entah bagaimana tidak berfungsi seperti yang diharapkan, unit test Anda akan menangkapnya.
atau apakah tes semacam itu berarti menguji konstruksi bahasa forEach bawaan?
Seperti yang dikatakan, cobalah untuk menguji hasil yang diharapkan dan antarmuka fungsi, bukan untuk implementasi (kecuali jika Anda melakukan tes integrasi - maka menangkap panggilan fungsi tertentu mungkin berguna). Jika ada beberapa cara untuk mengimplementasikan suatu fungsi, semuanya harus bekerja dengan unit test Anda.
Mengenai pembaruan pertanyaan Anda:
Uji perubahan negara! Misal beberapa adonan akan digunakan. Menurut implementasi Anda, nyatakan bahwa jumlah yang digunakan dough
sesuai pan
atau menyatakan bahwa dough
habis. Menyatakan bahwa pan
cookie berisi setelah fungsi panggilan. Menyatakan bahwa oven
kosong / dalam keadaan yang sama seperti sebelumnya.
Untuk tes tambahan, verifikasi kasus tepi: Apa yang terjadi jika oven
tidak kosong sebelum panggilan? Apa yang terjadi jika tidak cukup dough
? Jika pan
sudah penuh?
Anda harus dapat menyimpulkan semua data yang diperlukan untuk pengujian ini dari objek adonan, penggorengan dan oven itu sendiri. Tidak perlu menangkap panggilan fungsi. Perlakukan fungsi seolah-olah implementasinya tidak akan tersedia untuk Anda!
Bahkan, sebagian besar pengguna TDD menulis tes mereka sebelum mereka menulis fungsi sehingga mereka tidak bergantung pada implementasi yang sebenarnya.
Untuk tambahan terbaru Anda:
Ketika seorang pengguna membuat akun baru, sejumlah hal perlu terjadi: 1) catatan pengguna baru perlu dibuat dalam database 2) email selamat datang perlu dikirim 3) alamat IP pengguna perlu direkam untuk penipuan tujuan.
Jadi kami ingin membuat metode yang mengikat semua langkah "pengguna baru":
function createNewUser(validatedUserData, emailService, dataStore) {
userId = dataStore.insertUserRecord(validateduserData);
emailService.sendWelcomeEmail(validatedUserData);
dataStore.recordIpAddress(userId, validatedUserData.ip);
}
Untuk fungsi seperti ini saya akan mengejek / rintisan / palsu (apa pun yang tampak lebih umum) dataStore
dan emailService
parameter. Fungsi ini tidak melakukan transisi status apa pun pada parameter apa pun, ia mendelegasikannya ke metode beberapa di antaranya. Saya akan mencoba memverifikasi bahwa panggilan ke fungsi melakukan 4 hal:
- itu memasukkan pengguna ke dalam penyimpanan data
- mengirim (atau paling tidak disebut metode yang sesuai) email selamat datang
- itu mencatat IP pengguna ke dalam penyimpanan data
- itu mendelegasikan pengecualian / kesalahan yang ditemui (jika ada)
3 pemeriksaan pertama dapat dilakukan dengan mengejek, bertopik atau palsu dataStore
dan emailService
(Anda benar-benar tidak ingin mengirim email saat pengujian). Karena saya harus mencari ini untuk beberapa komentar, inilah perbedaannya:
- Palsu adalah objek yang berperilaku sama seperti aslinya dan sampai batas tertentu tidak bisa dibedakan. Kode ini biasanya dapat digunakan kembali di seluruh tes. Misalnya, ini bisa menjadi basis data dalam memori sederhana untuk pembungkus basis data.
- Sebuah rintisan hanya mengimplementasikan sebanyak yang diperlukan untuk memenuhi operasi yang diperlukan dari tes ini. Dalam kebanyakan kasus, sebuah rintisan adalah spesifik untuk tes atau sekelompok tes yang hanya membutuhkan satu set kecil metode asli. Dalam contoh ini, bisa jadi
dataStore
itu hanya mengimplementasikan versi insertUserRecord()
dan recordIpAddress()
.
- Mock adalah objek yang memungkinkan Anda memverifikasi cara penggunaannya (paling sering dengan membiarkan Anda mengevaluasi panggilan ke metodenya). Saya akan mencoba menggunakannya dengan hemat dalam unit test karena dengan menggunakannya Anda benar-benar mencoba untuk menguji implementasi fungsi dan bukan kepatuhan terhadap antarmuka, tetapi mereka masih memiliki kegunaannya. Banyak kerangka kerja tiruan ada untuk membantu Anda membuat hanya tiruan yang Anda butuhkan.
Perhatikan bahwa jika salah satu dari metode ini menghasilkan kesalahan, kami ingin kesalahan tersebut muncul hingga ke kode panggilan, sehingga dapat menangani kesalahan sesuai keinginan. Jika dipanggil oleh kode API, itu dapat menerjemahkan kesalahan menjadi kode respons HTTP yang sesuai. Jika itu dipanggil oleh antarmuka web, itu dapat menerjemahkan kesalahan menjadi pesan yang sesuai untuk ditampilkan kepada pengguna, dan sebagainya. Intinya adalah fungsi ini tidak tahu bagaimana menangani kesalahan yang mungkin terjadi.
Pengecualian / kesalahan yang diharapkan adalah kasus uji yang valid: Anda mengonfirmasi, bahwa, dalam hal peristiwa seperti itu terjadi, fungsi berperilaku seperti yang Anda harapkan. Ini dapat dicapai dengan membiarkan benda tiruan / tiruan / rintisan yang sesuai melempar saat diinginkan.
Inti dari kebingungan saya adalah bahwa untuk menguji unit fungsi seperti itu tampaknya perlu mengulangi implementasi yang tepat dalam tes itu sendiri (dengan menentukan bahwa metode dipanggil pada tiruan dalam urutan tertentu) dan yang tampaknya salah.
Kadang-kadang ini harus dilakukan (meskipun Anda lebih peduli tentang ini dalam tes integrasi). Lebih sering, ada cara lain untuk memverifikasi efek samping yang diharapkan / perubahan keadaan.
Memverifikasi panggilan fungsi yang tepat menghasilkan tes unit yang agak rapuh: Hanya perubahan kecil pada fungsi asli yang menyebabkannya gagal. Ini bisa diinginkan atau tidak, tetapi itu memerlukan perubahan pada unit test yang sesuai setiap kali Anda mengubah suatu fungsi (baik itu refactoring, optimalisasi, perbaikan bug, ...).
Sayangnya, dalam kasus itu, unit test kehilangan beberapa kredibilitasnya: karena itu diubah, itu tidak mengkonfirmasi fungsi setelah perubahan berperilaku dengan cara yang sama seperti sebelumnya.
Sebagai contoh, pertimbangkan seseorang menambahkan panggilan ke oven.preheat()
(pengoptimalan!) Dalam contoh pembuatan cookie Anda:
- Jika Anda mengolok-olok objek oven, itu tidak akan mengharapkan panggilan itu dan gagal tes, meskipun perilaku yang dapat diamati dari metode ini tidak berubah (Anda masih memiliki panci kue, mudah-mudahan).
- Rintisan A mungkin atau mungkin tidak gagal, tergantung pada apakah Anda hanya menambahkan metode yang akan diuji atau seluruh antarmuka dengan beberapa metode dummy.
- Palsu tidak boleh gagal, karena harus mengimplementasikan metode (sesuai dengan antarmuka)
Dalam pengujian unit saya, saya mencoba untuk menjadi se-umum mungkin: Jika implementasi berubah, tetapi perilaku yang terlihat (dari perspektif pemanggil) masih sama, tes saya harus lulus. Idealnya, satu-satunya kasus saya perlu mengubah unit test yang ada harus berupa perbaikan bug (dari pengujian, bukan fungsi yang sedang diuji).