Di mana garis antara logika aplikasi pengujian unit dan konstruksi bahasa yang tidak percaya?


87

Pertimbangkan fungsi seperti ini:

function savePeople(dataStore, people) {
    people.forEach(person => dataStore.savePerson(person));
}

Ini dapat digunakan seperti ini:

myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);

Mari kita asumsikan bahwa Storememiliki unit test sendiri, atau disediakan vendor. Bagaimanapun, kami percaya Store. Dan mari kita asumsikan lebih jauh bahwa penanganan kesalahan - misalnya, kesalahan pemutusan basis data - bukan tanggung jawab savePeople. Memang, mari kita asumsikan bahwa toko itu sendiri adalah basis data magis yang tidak mungkin kesalahan dengan cara apa pun. Dengan asumsi-asumsi ini, pertanyaannya adalah:

Harus savePeople()diuji unit, atau apakah tes semacam itu untuk menguji forEachkonstruksi bahasa built-in ?

Kita bisa, tentu saja, berpura-pura dataStoredan menegaskan bahwa dataStore.savePerson()itu dipanggil satu kali untuk setiap orang. Anda tentu bisa membuat argumen bahwa tes semacam itu memberikan keamanan terhadap perubahan implementasi: misalnya, jika kami memutuskan untuk mengganti forEachdengan forloop tradisional , atau metode iterasi lainnya. Jadi tes ini tidak sepenuhnya sepele. Namun sepertinya sangat dekat ...


Inilah contoh lain yang mungkin lebih bermanfaat. Pertimbangkan fungsi yang tidak melakukan apa pun selain mengoordinasikan objek atau fungsi lain. Sebagai contoh:

function bakeCookies(dough, pan, oven) {
    panWithRawCookies = pan.add(dough);
    oven.addPan(panWithRawCookies);
    oven.bakeCookies();
    oven.removePan();
}

Bagaimana seharusnya fungsi seperti ini diuji unit, dengan asumsi Anda pikir seharusnya demikian? Sulit bagi saya untuk membayangkan jenis unit tes yang tidak hanya mengejek dough, pandan oven, dan kemudian menegaskan bahwa metode disebut pada mereka. Tetapi tes semacam itu tidak lebih dari menduplikasi implementasi fungsi yang tepat.

Apakah ketidakmampuan untuk menguji fungsi dengan cara kotak hitam yang berarti menunjukkan cacat desain dengan fungsi itu sendiri? Jika demikian, bagaimana ini bisa diperbaiki?


Untuk memberikan kejelasan lebih lanjut pada pertanyaan yang memotivasi bakeCookiescontoh, saya akan menambahkan skenario yang lebih realistis, yang saya temui ketika mencoba menambahkan tes ke dan refactor kode warisan.

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

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.

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.


44
Setelah itu berjalan. Apakah Anda punya kue
Ewan

6
tentang pembaruan Anda: mengapa Anda ingin mengejek panci? atau adonan? ini terdengar seperti objek dalam-memori sederhana yang seharusnya sepele untuk dibuat, jadi tidak ada alasan mengapa Anda tidak harus mengujinya sebagai satu unit. ingat, "unit" dalam "unit testing" tidak berarti "kelas tunggal". itu berarti "unit kode sekecil mungkin yang digunakan untuk menyelesaikan sesuatu". panci mungkin tidak lebih dari sebuah wadah untuk benda-benda adonan, jadi itu akan dibuat untuk mengujinya dalam isolasi, bukan hanya menguji mengemudi metode bakeCookies dari luar masuk.
sara

11
Pada akhirnya, prinsip dasar yang bekerja di sini adalah bahwa Anda menulis cukup tes untuk meyakinkan diri sendiri bahwa kode itu berfungsi, dan itu adalah "kenari di tambang batu bara" yang memadai ketika seseorang mengubah sesuatu. Itu dia. Tidak ada mantra magis, anggapan formulaik atau pernyataan dogmatis, itulah sebabnya cakupan kode 85% hingga 90% (bukan 100%) secara luas dianggap sangat baik.
Robert Harvey

5
@RobertHarvey sayangnya kata-kata basi formulaic dan suara TDD, sementara pasti memberi Anda anggukan antusiasme, tidak membantu memecahkan masalah dunia nyata. untuk itu Anda perlu membuat tangan Anda kotor dan berisiko menjawab pertanyaan aktual
Jonah

4
Uji unit dalam rangka mengurangi kompleksitas siklomatik. Percayalah, Anda akan kehabisan waktu sebelum Anda mendapatkan fungsi ini
Neil McGuigan

Jawaban:


118

Haruskah savePeople()unit diuji? Iya. Anda tidak menguji yang dataStore.savePersonberfungsi, atau bahwa koneksi db berfungsi, atau bahkan itu foreachberhasil. Anda menguji yang savePeoplememenuhi janji yang dibuatnya melalui kontraknya.

Bayangkan skenario ini: seseorang melakukan refactor besar terhadap basis kode, dan secara tidak sengaja menghapus forEachbagian dari implementasi sehingga selalu hanya menyimpan item pertama. Tidakkah Anda ingin tes unit menangkap itu?


20
@RobertHarvey: Ada banyak area abu-abu, dan membuat perbedaan, IMO, tidak penting. Anda benar - tidak benar-benar penting untuk menguji bahwa "itu memanggil fungsi yang tepat" melainkan "itu melakukan hal yang benar" terlepas dari bagaimana melakukannya. Yang penting adalah mengujinya, dengan serangkaian input spesifik pada fungsi, Anda mendapatkan output tertentu. Namun, saya bisa melihat bagaimana kalimat terakhir itu membingungkan, jadi saya menghapusnya.
Bryan Oakley

64
"Kamu sedang menguji bahwa savePeople memenuhi janji yang dibuatnya melalui kontraknya." Ini. Begitu banyak ini.
Lovis

2
Kecuali jika Anda memiliki tes sistem "ujung ke ujung" yang mencakupnya.
Ian

6
@Ian Tes ujung ke ujung tidak menggantikan unit test, semuanya gratis. Hanya karena Anda mungkin memiliki tes ujung ke ujung yang memastikan Anda menyimpan daftar orang tidak berarti Anda tidak harus memiliki unit test untuk juga menutupinya.
Vincent Savard

4
@VincentSavard, tetapi biaya / manfaat dari tes unit berkurang jika risiko dikendalikan dengan cara lain.
Ian

36

Biasanya pertanyaan seperti ini muncul ketika orang melakukan pengembangan "test-after". Pendekatan masalah ini dari sudut pandang TDD, di mana tes datang sebelum implementasi, dan tanyakan pada diri sendiri pertanyaan ini lagi sebagai latihan.

Setidaknya dalam aplikasi TDD saya, yang biasanya di luar, saya tidak akan mengimplementasikan fungsi seperti savePeoplesetelah diimplementasikan savePerson. Fungsi savePeopledan savePersonakan dimulai sebagai satu dan didorong oleh tes dari unit test yang sama; pemisahan antara keduanya akan timbul setelah beberapa tes, pada langkah refactoring. Mode kerja ini juga akan menimbulkan pertanyaan di mana fungsi savePeopleseharusnya - apakah itu fungsi bebas atau bagian dari dataStore.

Pada akhirnya, tes tidak akan hanya memeriksa apakah Anda benar dapat menyimpan Persondalam Store, tetapi juga banyak orang. Ini juga akan membuat saya mempertanyakan apakah pemeriksaan lain diperlukan, misalnya, "Apakah saya perlu memastikan bahwa savePeoplefungsinya adalah atomik, baik menyelamatkan semua atau tidak sama sekali?", "Bisakah itu entah bagaimana mengembalikan kesalahan untuk orang-orang yang tidak bisa t diselamatkan? Seperti apa kesalahan itu? ", dan seterusnya. Semua ini jumlahnya jauh lebih dari sekadar memeriksa penggunaan suatu forEachatau bentuk iterasi lainnya.

Padahal, jika persyaratan untuk menyimpan lebih dari satu orang sekaligus datang hanya setelah savePersonsudah dikirimkan, maka saya akan memperbarui tes yang ada savePersonuntuk menjalankan melalui fungsi baru savePeople, memastikan itu masih dapat menyelamatkan satu orang dengan hanya mendelegasikan pada awalnya, kemudian uji coba perilaku tersebut untuk lebih dari satu orang melalui tes baru, pikirkan apakah perlu untuk membuat perilaku itu atomik atau tidak.


4
Pada dasarnya menguji antarmuka, bukan implementasinya.
Mengintai

8
Poin yang adil dan berwawasan luas. Namun saya merasa entah bagaimana pertanyaan saya yang sebenarnya sedang dihindari :) Jawaban Anda mengatakan, "Di dunia nyata, dalam sistem yang dirancang dengan baik, saya tidak berpikir versi sederhana masalah Anda ini akan ada." Sekali lagi, adil, tetapi saya secara khusus membuat versi yang disederhanakan ini untuk menyoroti esensi dari masalah yang lebih umum. Jika Anda tidak bisa melewati sifat buatan contoh, Anda mungkin bisa membayangkan contoh lain di mana Anda memang memiliki alasan yang baik untuk fungsi serupa, yang hanya melakukan iterasi dan delegasi. Atau mungkin Anda berpikir itu tidak mungkin?
Jonah

@Jonah diperbarui. Saya harap ini menjawab pertanyaan Anda sedikit lebih baik. Ini semua sangat berdasarkan pendapat dan mungkin bertentangan dengan tujuan untuk situs ini, tetapi tentu saja diskusi yang sangat menarik untuk dilakukan. Ngomong-ngomong, saya mencoba menjawab dari sudut pandang pekerjaan profesional, di mana kita harus berusaha untuk meninggalkan tes unit untuk semua perilaku aplikasi, terlepas dari seberapa sepele implementasi mungkin, karena kita memiliki tugas untuk membangun yang teruji dengan baik dan sistem terdokumentasi untuk pengelola baru jika kita pergi. Untuk proyek-proyek pribadi atau, katakanlah, non-kritis (uang juga kritis), saya memiliki pendapat yang sangat berbeda.
MichelHenrich

Terima kasih atas pembaruannya. Bagaimana tepatnya Anda menguji savePeople? Seperti yang saya jelaskan dalam paragraf terakhir OP atau cara lain?
Jonah

1
Maaf, saya tidak memperjelas bagian "tidak mengolok-olok". Maksud saya, saya tidak akan menggunakan tiruan untuk savePersonfungsi seperti yang Anda sarankan, sebagai gantinya saya akan mengujinya secara lebih umum savePeople. Tes unit untuk Storeakan diubah untuk dijalankan savePeopledaripada langsung menelepon savePerson, jadi untuk ini, tidak ada tiruan yang digunakan. Tetapi database tentu saja tidak boleh ada, karena kami ingin mengisolasi masalah pengkodean dari berbagai masalah integrasi yang terjadi dengan database aktual, jadi di sini kami masih memiliki tiruan.
MichelHenrich

21

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 doughsesuai panatau menyatakan bahwa doughhabis. Menyatakan bahwa pancookie berisi setelah fungsi panggilan. Menyatakan bahwa ovenkosong / dalam keadaan yang sama seperti sebelumnya.

Untuk tes tambahan, verifikasi kasus tepi: Apa yang terjadi jika oventidak kosong sebelum panggilan? Apa yang terjadi jika tidak cukup dough? Jika pansudah 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) dataStoredan emailServiceparameter. 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 dataStoredan 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 dataStoreitu 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).


1
Masalahnya adalah bahwa segera setelah Anda menulis myDataStore.containsPerson('Joe')Anda mengasumsikan adanya database uji fungsional. Setelah Anda melakukannya, Anda menulis tes integrasi dan bukan tes unit.
Jonah

Saya berasumsi bahwa saya dapat mengandalkan memiliki penyimpanan data uji (saya tidak peduli apakah itu nyata atau tiruan) dan semuanya berfungsi sebagaimana diatur (karena saya harus sudah memiliki unit test untuk kasus-kasus tersebut). Satu-satunya tes yang ingin diuji adalah yang savePeople()benar - benar menambahkan orang-orang ke penyimpanan data apa pun yang Anda berikan selama penyimpanan data mengimplementasikan antarmuka yang diharapkan. Sebagai contoh, tes integrasi akan memeriksa apakah pembungkus basis data saya benar-benar melakukan pemanggilan basis data yang tepat untuk pemanggilan metode.
hoffmale

Untuk memperjelas, jika Anda menggunakan tiruan, yang bisa Anda lakukan adalah menegaskan bahwa metode pada tiruan itu dipanggil , mungkin dengan beberapa parameter tertentu. Anda tidak dapat menegaskan status tiruan setelahnya. Jadi jika Anda ingin membuat pernyataan tentang status database setelah memanggil metode yang diuji, seperti dalam myDataStore.containsPerson('Joe'), Anda harus menggunakan db fungsional dari beberapa jenis. Setelah Anda mengambil langkah itu bukan lagi tes unit.
Jonah

1
itu tidak harus menjadi database nyata - hanya sebuah objek yang mengimplementasikan antarmuka yang sama dengan penyimpanan data nyata (baca: itu melewati tes unit yang relevan untuk antarmuka penyimpanan data). Saya masih menganggap ini sebagai tiruan. Biarkan tiruan menyimpan semua yang ditambahkan oleh metode apa pun untuk melakukannya dalam sebuah array dan periksa apakah data uji (elemen myPeople) ada di dalam array. IMHO mock masih harus memiliki perilaku yang dapat diamati sama yang diharapkan dari objek nyata, jika tidak Anda menguji kepatuhan dengan antarmuka tiruan, bukan antarmuka nyata.
hoffmale

"Biarkan tiruan menyimpan semua yang ditambahkan dengan metode apa pun untuk melakukannya dalam sebuah array dan periksa apakah data uji (elemen myPeople) ada dalam array" - itu masih merupakan database "nyata", hanya sebuah ad-hoc, dalam memori yang telah Anda buat. "IMHO, mock masih harus memiliki perilaku yang dapat diamati sama yang diharapkan dari objek nyata" - Saya kira Anda dapat mengadvokasi itu, tapi bukan itu yang berarti "mock" dalam literatur pengujian atau di salah satu perpustakaan populer. sudah terlihat. Sebuah tiruan hanya memverifikasi bahwa metode yang diharapkan dipanggil dengan parameter yang diharapkan.
Jonah

13

Nilai utama yang disediakan oleh tes tersebut adalah bahwa hal itu membuat implementasi Anda menjadi reaktif.

Saya biasa melakukan banyak optimasi kinerja dalam karir saya dan sering menemukan masalah dengan pola yang tepat yang Anda tunjukkan: untuk menyimpan entitas N ke dalam database, melakukan insert N. Biasanya lebih efisien untuk melakukan penyisipan massal menggunakan pernyataan tunggal.

Di sisi lain, kami juga tidak ingin mengoptimalkan secara prematur. Jika Anda biasanya hanya menghemat 1 - 3 orang sekaligus, maka menulis batch yang dioptimalkan mungkin berlebihan.

Dengan unit test yang tepat, Anda dapat menuliskannya dengan cara Anda menerapkannya di atas, dan jika Anda perlu mengoptimalkannya, Anda bebas melakukannya dengan jaring pengaman dari tes otomatis untuk menangkap kesalahan. Secara alami, ini bervariasi berdasarkan kualitas tes, jadi uji secara bebas dan uji dengan baik.

Keuntungan sekunder dari unit yang menguji perilaku ini adalah untuk berfungsi sebagai dokumentasi untuk apa tujuannya. Contoh sepele ini mungkin jelas, tetapi mengingat poin berikutnya di bawah ini, itu bisa sangat penting.

Keuntungan ketiga, yang telah ditunjukkan oleh orang lain, adalah Anda dapat menguji rincian di bawah-penutup yang sangat sulit untuk diuji dengan tes integrasi atau penerimaan. Sebagai contoh, jika ada persyaratan bahwa semua pengguna disimpan secara atom, maka Anda dapat menulis sebuah test case untuk itu, yang memberi Anda cara untuk mengetahui berperilaku seperti yang diharapkan, dan juga berfungsi sebagai dokumentasi untuk suatu persyaratan yang mungkin tidak jelas. untuk pengembang baru.

Saya akan menambahkan pemikiran yang saya dapatkan dari instruktur TDD. Jangan menguji metodenya. Uji perilakunya. Dengan kata lain, Anda tidak menguji yang savePeopleberfungsi, Anda menguji bahwa banyak pengguna dapat disimpan dalam satu panggilan.

Saya menemukan kemampuan saya untuk melakukan pengujian unit kualitas dan TDD membaik ketika saya berhenti berpikir tentang pengujian unit sebagai memverifikasi bahwa suatu program berfungsi, tetapi, mereka memverifikasi bahwa unit kode melakukan apa yang saya harapkan . Itu berbeda. Mereka tidak memverifikasi itu berfungsi, tetapi mereka memverifikasi itu melakukan apa yang saya pikir tidak. Ketika saya mulai berpikir seperti itu, perspektif saya berubah.


Contoh refactoring sisipan massal adalah yang baik. Tes unit yang mungkin saya sarankan dalam OP - bahwa tiruan dataStore telah savePersonmemanggilnya untuk setiap orang dalam daftar - akan pecah dengan refactoring memasukkan massal, namun. Yang bagi saya menunjukkan bahwa ini adalah tes unit yang buruk. Namun, saya tidak melihat alternatif yang akan melewati implementasi curah dan satu-save-per-orang, tanpa menggunakan database pengujian yang sebenarnya dan menyatakan menentangnya, yang tampaknya salah. Bisakah Anda memberikan tes yang berfungsi untuk kedua implementasi?
Jonah

1
@ jpmc26 Bagaimana dengan tes yang menguji bahwa orang-orang diselamatkan ...?
user253751

1
@ Imib saya tidak mengerti apa artinya itu. Agaknya, toko nyata didukung oleh database, jadi Anda harus mengejek atau mematikannya untuk unit test. Jadi pada titik itu, Anda akan menguji bahwa tiruan atau rintisan Anda dapat menyimpan objek. Itu sama sekali tidak berguna. Yang terbaik yang bisa Anda lakukan adalah menegaskan bahwa savePersonmetode dipanggil untuk setiap input, dan jika Anda mengganti loop dengan insert massal, Anda tidak akan memanggil metode itu lagi. Jadi tes Anda akan pecah. Jika Anda memiliki hal lain dalam pikiran, saya terbuka untuk itu, tetapi saya belum melihatnya. (Dan tidak melihat maksud saya.)
jpmc26

1
@ imibis Saya tidak menganggap itu sebagai tes yang berguna. Menggunakan toko data palsu tidak memberi saya kepercayaan diri bahwa itu akan bekerja dengan hal yang asli. Bagaimana saya tahu karya palsu saya seperti yang asli? Saya lebih suka membiarkan serangkaian tes integrasi menutupinya. (Saya mungkin harus mengklarifikasi bahwa saya maksudkan " tes unit apa pun " dalam komentar pertama saya di sini.)
jpmc26

1
@ Imibis saya benar-benar mempertimbangkan kembali posisi saya. Saya ragu tentang nilai unit test karena masalah seperti ini, tapi mungkin saya meremehkan nilai bahkan jika Anda memalsukan input. Aku tidak tahu bahwa pengujian input / output cenderung jauh lebih berguna daripada tes berat pura-pura, tapi mungkin penolakan untuk mengganti input dengan palsu sebenarnya adalah bagian dari masalah di sini.
jpmc26

6

Harus bakeCookies()diuji? Iya.

Bagaimana seharusnya fungsi seperti ini diuji unit, dengan asumsi Anda pikir seharusnya demikian? Sulit bagi saya untuk membayangkan segala jenis unit test yang tidak hanya mengolok-olok adonan, panci, dan oven, dan kemudian menyatakan bahwa metode dipanggil pada mereka.

Tidak juga. Perhatikan dengan seksama pada APA fungsi yang seharusnya dilakukan - itu seharusnya mengatur ovenobjek ke keadaan tertentu. Melihat kode itu tampak bahwa keadaan pandan doughobjek tidak terlalu penting. Jadi, Anda harus melewati sebuah ovenobjek (atau mengejeknya) dan menyatakan bahwa ia dalam keadaan tertentu di akhir pemanggilan fungsi.

Dengan kata lain, Anda harus menyatakan bahwa bakeCookies()kue itu dipanggang .

Untuk fungsi yang sangat singkat, tes unit mungkin tampak sedikit lebih dari tautologi. Tapi jangan lupa, program Anda akan bertahan jauh lebih lama daripada waktu Anda dipekerjakan menulisnya. Fungsi itu mungkin atau mungkin tidak berubah di masa depan.

Tes unit melayani dua fungsi:

  1. Ini menguji bahwa semuanya berfungsi. Ini adalah tes unit fungsi yang paling tidak berguna dan tampaknya Anda hanya mempertimbangkan fungsionalitas ini ketika mengajukan pertanyaan.

  2. Ia memeriksa untuk melihat bahwa modifikasi program di masa depan tidak merusak fungsi yang sebelumnya diterapkan. Ini adalah fungsi yang paling berguna dari unit test dan mencegah masuknya bug ke dalam program besar. Ini berguna dalam pengkodean normal ketika menambahkan fitur ke program tetapi lebih berguna dalam refactoring dan optimisasi di mana algoritma inti yang mengimplementasikan program diubah secara dramatis tanpa mengubah perilaku program yang dapat diamati.

Jangan menguji kode di dalam fungsi. Alih-alih menguji bahwa fungsi melakukan apa yang dikatakannya. Ketika Anda melihat unit test dengan cara ini (fungsi pengujian, bukan kode) maka Anda akan menyadari bahwa Anda tidak pernah menguji konstruksi bahasa atau bahkan logika aplikasi. Anda sedang menguji API.


Hai, terima kasih atas balasan Anda. Maukah Anda melihat pembaruan ke-2 saya dan memikirkan tentang cara menguji fungsi unit dalam contoh itu?
Jonah

Saya menemukan bahwa ini bisa efektif ketika Anda bisa menggunakan oven nyata atau oven palsu, tetapi secara signifikan kurang efektif dengan oven tiruan. Mengejek (oleh definisi Meszaros) tidak memiliki keadaan apa pun, selain catatan metode yang telah dipanggil pada mereka. Pengalaman saya adalah ketika fungsi-fungsi seperti bakeCookiesdiuji dengan cara ini, mereka cenderung rusak selama refaktor yang tidak akan memengaruhi perilaku aplikasi yang dapat diamati.
James_pic

@ James_pic, tepatnya. Dan ya, itulah definisi tiruan yang saya gunakan. Jadi, dengan komentar Anda, apa yang Anda lakukan dalam kasus seperti ini? Lupakan ujiannya? Tuliskan tes rapuh, pengulangan implementasi? Sesuatu yang lain
Jonah

@Jonah Saya biasanya akan melihat menguji komponen itu dengan tes integrasi (saya telah menemukan peringatan tentang hal itu menjadi lebih sulit untuk di-debug untuk menjadi berlebihan, mungkin karena kualitas perkakas modern), atau pergi ke kesulitan menciptakan sebuah palsu setengah meyakinkan.
James_pic

3

Haruskah savePeople () diuji unit, atau apakah tes semacam itu berarti menguji konstruksi bahasa forEach bawaan?

Iya. Tetapi Anda bisa melakukannya dengan cara yang hanya akan menguji ulang konstruk.

Hal yang perlu diperhatikan di sini adalah bagaimana fungsi ini berperilaku ketika savePersongagal setengah jalan? Bagaimana cara kerjanya?

Itu adalah semacam perilaku halus yang disediakan fungsi yang harus Anda laksanakan dengan tes unit.


Ya, saya setuju bahwa kondisi kesalahan halus harus diuji, tetapi saya bukan pertanyaan yang menarik - jawabannya jelas. Karenanya alasan saya secara khusus menyatakan bahwa, untuk keperluan pertanyaan saya, savePeopletidak boleh bertanggung jawab atas penanganan kesalahan. Untuk mengklarifikasi lagi, dengan asumsi bahwa hanyasavePeople bertanggung jawab untuk iterasi melalui daftar dan mendelegasikan penyimpanan setiap item ke metode lain, haruskah itu masih diuji?
Jonah

@Jonah: Jika Anda bersikeras untuk membatasi tes unit Anda hanya untuk foreachkonstruksi, dan bukan kondisi, efek samping atau perilaku di luarnya, maka Anda benar; unit test baru benar-benar tidak terlalu menarik.
Robert Harvey

1
@jonah - haruskah ia beralih dan menyimpan sebanyak mungkin atau berhenti karena kesalahan? Satu penyimpanan tidak dapat memutuskan hal itu, karena tidak dapat mengetahui cara menggunakannya.
Telastyn

1
@jonah - selamat datang di situs. Salah satu komponen utama dari format Tanya Jawab kami adalah bahwa kami tidak di sini untuk membantu Anda . Pertanyaan Anda tentu saja membantu Anda, tetapi itu juga membantu banyak orang lain yang datang ke situs untuk mencari jawaban atas pertanyaan mereka. Saya menjawab pertanyaan yang Anda ajukan. Bukan salah saya jika Anda tidak suka jawabannya atau lebih suka mengubah tiang gawang. Dan sejujurnya, sepertinya jawaban lain mengatakan hal dasar yang sama, meskipun lebih fasih.
Telastyn

1
@ Telastyn, saya mencoba mendapatkan wawasan tentang pengujian unit. Pertanyaan awal saya tidak cukup jelas, jadi saya menambahkan klarifikasi untuk mengarahkan percakapan ke pertanyaan saya yang sebenarnya . Anda memilih untuk menafsirkan itu karena saya entah bagaimana menipu Anda dalam permainan "menjadi benar." Saya telah menghabiskan ratusan jam menjawab pertanyaan tentang tinjauan kode dan SO. Tujuan saya adalah selalu membantu orang yang saya jawab. Jika Anda tidak, itu pilihan Anda. Anda tidak harus menjawab pertanyaan saya.
Jonah

3

Kuncinya di sini adalah perspektif Anda tentang fungsi tertentu sebagai hal sepele. Sebagian besar pemrograman adalah sepele: tetapkan nilai, lakukan beberapa matematika, buat keputusan: jika ini maka itu, lanjutkan perulangan sampai ... Dalam isolasi, semuanya sepele. Anda baru saja membaca 5 bab pertama dari setiap buku yang mengajarkan bahasa pemrograman.

Fakta bahwa menulis tes sangat mudah harus menjadi pertanda bahwa desain Anda tidak terlalu buruk. Apakah Anda lebih suka desain yang tidak mudah diuji?

"Itu tidak akan pernah berubah." adalah bagaimana proyek yang paling gagal dimulai. Tes unit hanya menentukan apakah unit berfungsi seperti yang diharapkan dalam serangkaian keadaan tertentu. Dapatkan untuk lulus dan kemudian Anda bisa melupakan detail implementasi dan hanya menggunakannya. Gunakan ruang otak itu untuk tugas selanjutnya.

Mengetahui segala sesuatunya berjalan seperti yang diharapkan sangat penting dan tidak sepele dalam proyek besar dan terutama tim besar. Jika ada satu kesamaan yang dimiliki programmer, adalah fakta bahwa kita semua harus berurusan dengan kode buruk orang lain. Paling tidak yang bisa kita lakukan adalah melakukan beberapa tes. Jika ragu, tulis tes dan lanjutkan.


Terima kasih atas umpan balik Anda. Poin bagus. Pertanyaan yang saya benar-benar ingin dijawab (saya baru saja menambahkan pembaruan lain untuk mengklarifikasi) adalah cara yang tepat untuk menguji fungsi yang tidak lebih dari memanggil urutan layanan lain melalui delegasi. Dalam kasus seperti itu, tampaknya tes unit yang sesuai untuk "mendokumentasikan kontrak" hanyalah pernyataan ulang implementasi fungsi, yang menyatakan bahwa metode dipanggil pada berbagai ejekan. Namun tes yang identik dengan implementasi dalam kasus-kasus itu terasa salah ....
Jonah

1

Haruskah savePeople () diuji unit, atau apakah tes semacam itu berarti menguji konstruksi bahasa forEach bawaan?

Ini sudah dijawab oleh @BryanOakley, tapi saya punya beberapa argumen tambahan (saya kira):

Pertama uji unit adalah untuk menguji pemenuhan kontrak, bukan implementasi API; tes harus menetapkan prasyarat kemudian memanggil, kemudian memeriksa efek, efek samping, setiap invarian dan kondisi posting. Saat Anda memutuskan apa yang akan diuji, implementasi API tidak masalah (dan tidak seharusnya) .

Kedua, tes Anda akan ada di sana untuk memeriksa invarian ketika fungsi berubah . Fakta itu tidak berubah sekarang tidak berarti Anda tidak harus mengikuti tes.

Ketiga, ada nilai dalam menerapkan tes sepele, baik dalam pendekatan TDD (yang mengamanatkannya) dan di luar itu.

Saat menulis C ++, untuk kelas saya, saya cenderung menulis tes sepele yang instantiate objek dan memeriksa invarian (ditugaskan, reguler, dll). Saya menemukan itu mengejutkan berapa kali tes ini rusak selama pengembangan (dengan - misalnya - menambahkan anggota yang tidak bergerak di kelas, karena kesalahan).


1

Saya pikir pertanyaan Anda intinya adalah:

Bagaimana cara unit menguji fungsi kosong tanpa menjadi tes integrasi?

Jika kami mengubah fungsi memanggang cookie Anda untuk mengembalikan cookie misalnya, segera menjadi jelas apa tes yang seharusnya.

Jika kita harus memanggil pan. Dapatkan Cookies setelah memanggil fungsi meskipun kita dapat mempertanyakan apakah ini benar-benar sebuah uji integrasi atau bukan, tetapi apakah kita hanya menguji objek pan?

Saya pikir Anda benar karena memiliki unit test dengan segala yang diejek dan hanya memeriksa fungsi xy dan z disebut nilai kurang.

Tapi! Saya berpendapat bahwa dalam kasus ini Anda harus memperbaiki fungsi void Anda untuk mengembalikan hasil yang dapat diuji ATAU menggunakan objek nyata dan membuat tes integrasi

--- Perbarui untuk contoh createNewUser

  • catatan pengguna baru perlu dibuat dalam database
  • email selamat datang perlu dikirim
  • alamat IP pengguna perlu direkam untuk tujuan penipuan.

OK jadi kali ini hasil fungsi tidak mudah dikembalikan. Kami ingin mengubah status parameter.

Di sinilah saya mendapatkan sedikit kontroversial. Saya membuat implementasi tiruan konkret untuk parameter stateful

tolong, para pembaca yang budiman, cobalah dan kendalikan amarahmu!

begitu...

var validatedUserData = new UserData(); //we can use the real object for this
var emailService = new MockEmailService(); //a simple mock which saves sentEmails to a List<string>
var dataStore = new MockDataStore(); //a simple mock which saves ips to a List<string>

//run the test
target.createNewUser(validatedUserData, emailService, dataStore);

//check the results
Assert.AreEqual(1, emailService.EmailsSent.Count());
Assert.AreEqual(1, dataStore.IpsRecorded.Count());
Assert.AreEqual(1, dataStore.UsersSaved.Count());

Ini memisahkan detail implementasi dari metode yang diuji dari perilaku yang diinginkan. Implementasi alternatif:

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.bulkInsedrtUserRecords(new [] {validateduserData});
  emailService.addEmailToQueue(validatedUserData);
  emailService.ProcessQueue();
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

Masih akan lulus uji unit. Plus Anda memiliki keuntungan untuk dapat menggunakan kembali benda-benda tiruan di seluruh tes dan juga menyuntikkannya ke dalam aplikasi Anda untuk UI atau Tes Integrasi.


itu bukan tes integrasi hanya karena Anda menyebutkan nama-nama 2 kelas konkret ... tes integrasi adalah tentang pengujian integrasi dengan sistem eksternal, seperti disk IO, DB, layanan web eksternal, dan sebagainya. memanggil pan.getCookies () ada di -memory, cepat, memeriksa hal-hal yang kami minati dll. Saya setuju bahwa memiliki metode mengembalikan cookie langsung terasa seperti desain yang lebih baik.
sara

3
Tunggu. Sejauh yang kami tahu pan.getcookies mengirim email ke koki yang meminta mereka untuk mengambil cookie dari oven ketika mereka mendapat kesempatan
Ewan

Saya kira secara teori itu mungkin, tetapi itu akan menjadi nama yang cukup menyesatkan. yang pernah mendengar tentang peralatan oven yang mengirim email? tapi saya mengerti maksud Anda, itu tergantung. Saya berasumsi bahwa kelas-kelas yang berkolaborasi ini adalah objek daun atau hanya hal-hal sederhana dalam memori, tetapi jika mereka melakukan hal-hal licik, maka diperlukan kehati-hatian. Saya pikir pengiriman email pasti harus dilakukan pada tingkat yang lebih tinggi daripada ini. ini seperti logika bisnis turun dan kotor di entitas.
sara

2
Itu adalah pertanyaan retoris, tetapi: "siapa yang pernah mendengar tentang peralatan oven yang mengirim email?" venturebeat.com/2016/03/08/...
clacke

Hai Ewan, saya pikir jawaban ini paling mendekati sejauh ini dengan apa yang sebenarnya saya tanyakan. Saya pikir poin Anda tentang bakeCookiesmengembalikan cookie yang dipanggang tepat, dan saya punya beberapa pemikiran setelah mempostingnya. Jadi saya pikir itu lagi bukan contoh yang bagus. Saya menambahkan satu lagi pembaruan yang semoga memberikan contoh yang lebih realistis tentang apa yang memotivasi pertanyaan saya. Saya menghargai masukan Anda.
Jonah

0

Anda juga harus menguji bakeCookies- apa yang akan / harus Anda hasilkan bakeCookies(egg, pan, oven)? Telur goreng atau pengecualian? Sendiri, tidak panjuga tidak ovenakan peduli tentang bahan-bahan yang sebenarnya, karena tidak satupun dari mereka yang seharusnya, tetapi bakeCookiesharus biasanya menghasilkan cookies. Lebih umum itu dapat bergantung pada bagaimana doughdiperoleh dan apakah ada adalah kesempatan itu menjadi belaka eggatau misalnya watersebagai gantinya.

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.