Bagaimana tes unit memfasilitasi desain?

43

Rekan kami mempromosikan tes unit penulisan sebagai benar-benar membantu kami untuk memperbaiki hal-hal desain dan refactor kami, tetapi saya tidak mengerti caranya. Jika saya memuat file CSV dan menguraikannya, bagaimana uji unit (memvalidasi nilai di bidang) akan membantu saya memverifikasi desain saya? Dia menyebutkan coupling dan modularitas dll. Tetapi bagi saya itu tidak masuk akal - tapi saya tidak punya banyak latar belakang teoretis.

Itu tidak sama dengan pertanyaan yang Anda tandai sebagai duplikat, saya akan tertarik pada contoh aktual bagaimana ini membantu, bukan hanya teori yang mengatakan "itu membantu". Saya suka jawaban di bawah dan komentar tetapi saya ingin belajar lebih banyak.

Pengguna039402
sumber
7
Kemungkinan duplikat Apakah TDD mengarah ke desain yang baik?
nyamuk
3
Jawaban di bawah ini benar-benar semua yang perlu Anda ketahui. Duduk di sebelah orang-orang yang menulis agregat-akar ketergantungan pabrik pabrik disuntik sepanjang hari adalah orang yang diam-diam menulis kode sederhana terhadap unit test yang berfungsi dengan benar, mudah diverifikasi, dan sudah didokumentasikan.
Robert Harvey
4
@gnat melakukan pengujian unit tidak secara otomatis menyiratkan TDD, itu pertanyaan yang berbeda
Joppe
11
"unit test (memvalidasi nilai-nilai di bidang)" - Anda tampaknya menggabungkan pengujian unit dengan validasi input.
jonrsharpe
1
@jonrsharpe Mengingat bahwa itu kode parsing file CSV, dia mungkin berbicara tentang tes unit nyata yang memverifikasi string CSV tertentu memberikan hasil yang diharapkan.
JollyJoker

Jawaban:

3

Unit test tidak hanya memfasilitasi desain, tetapi itu adalah salah satu manfaat utama mereka.

Penulisan uji-pertama mengusir modularitas dan struktur kode bersih.

Ketika Anda menulis tes kode Anda terlebih dahulu, Anda akan menemukan bahwa "kondisi" dari unit kode tertentu secara alami didorong ke dependensi (biasanya melalui mengejek atau bertopik) ketika Anda menganggapnya dalam kode Anda.

"Diberikan kondisi x, perkirakan perilaku y," akan sering menjadi rintisan untuk memasok x(yang merupakan skenario di mana tes perlu memverifikasi perilaku komponen saat ini) dan yakan menjadi tiruan, panggilan yang akan diverifikasi di akhir tes (kecuali jika itu "harus kembali y," dalam hal tes hanya akan memverifikasi nilai kembali secara eksplisit).

Kemudian, setelah unit ini berperilaku seperti yang ditentukan, Anda beralih ke menulis dependensi (untuk xdan y) yang Anda temukan.

Hal ini membuat penulisan kode modular yang bersih menjadi proses yang sangat mudah dan alami, jika tidak demikian, seringkali mudah untuk mengaburkan tanggung jawab dan perilaku pasangan tanpa disadari.

Tes menulis nanti akan memberi tahu Anda ketika kode Anda terstruktur dengan buruk.

Ketika menulis tes untuk sepotong kode menjadi sulit karena ada terlalu banyak hal untuk di-stub atau diolok-olok, atau karena hal-hal yang terlalu erat digabungkan bersama-sama, Anda tahu Anda memiliki perbaikan untuk dibuat dalam kode Anda.

Ketika "mengubah tes" menjadi beban karena ada begitu banyak perilaku dalam satu unit, Anda tahu Anda memiliki perbaikan untuk dibuat dalam kode Anda (atau hanya dalam pendekatan Anda dalam menulis tes - tetapi ini tidak biasanya terjadi dalam pengalaman saya) .

Ketika skenario Anda menjadi terlalu rumit ( "jika xdan ydan zkemudian ...") karena Anda perlu lebih abstrak, Anda tahu Anda memiliki perbaikan untuk membuat dalam kode Anda.

Ketika Anda berakhir dengan tes yang sama di dua fixture berbeda karena duplikasi dan redundansi, Anda tahu Anda memiliki perbaikan yang harus dilakukan dalam kode Anda.

Berikut ini adalah pembicaraan yang sangat baik oleh Michael Feathers menunjukkan hubungan yang sangat dekat antara testability dan desain dalam kode (awalnya diposting oleh displayName di komentar). Pembicaraan juga membahas beberapa keluhan umum dan kesalahpahaman tentang desain yang baik dan kemampuan uji pada umumnya.

Semut P
sumber
@SSECommunity: Dengan hanya 2 upvotes pada hari ini, jawaban ini sangat mudah untuk diabaikan. Saya sangat merekomendasikan Talk oleh Michael Feathers yang telah ditautkan dalam jawaban ini.
displayName
103

Hal yang hebat tentang unit test adalah mereka memungkinkan Anda untuk menggunakan kode Anda bagaimana programmer lain akan menggunakan kode Anda.

Jika kode Anda canggung untuk unit test, maka mungkin akan terasa canggung untuk digunakan. Jika Anda tidak dapat menyuntikkan dependensi tanpa melewati rintangan, maka kode Anda mungkin tidak fleksibel untuk digunakan. Dan jika Anda perlu menghabiskan banyak waktu menyiapkan data atau mencari tahu untuk melakukan apa, kode Anda yang sedang diuji mungkin memiliki terlalu banyak sambungan dan akan sulit untuk dikerjakan.

Telastyn
sumber
7
Jawaban yang bagus Saya selalu suka menganggap tes saya sebagai klien pertama dari kode; jika menyakitkan untuk menulis tes, itu akan menyakitkan untuk menulis kode yang mengkonsumsi API atau apa pun yang saya kembangkan.
Stephen Byrne
41
Dalam pengalaman saya, sebagian besar tes unit tidak "menggunakan kode Anda bagaimana programmer lain akan menggunakan kode Anda." Mereka menggunakan kode Anda karena unit test akan menggunakan kode tersebut. Benar, mereka akan mengungkapkan banyak kelemahan serius. Tetapi API yang dirancang untuk pengujian unit mungkin bukan API yang paling cocok untuk penggunaan umum. Unit test yang ditulis secara sederhana seringkali memerlukan kode yang mendasarinya untuk mengekspos terlalu banyak internal. Sekali lagi, berdasarkan pengalaman saya - akan tertarik mendengar bagaimana Anda menangani ini. (Lihat jawaban saya di bawah ini)
user949300
7
@ user949300 - Saya bukan orang yang percaya dalam ujian pertama. Jawaban saya didasarkan pada ide kode (dan tentunya desain) terlebih dahulu. API seharusnya tidak dirancang untuk pengujian unit, mereka harus dirancang untuk pelanggan Anda. Tes unit membantu memperkirakan pelanggan Anda, tetapi itu adalah alat. Mereka ada di sana untuk melayani Anda, bukan sebaliknya. Dan mereka pasti tidak akan menghentikan Anda membuat kode jelek.
Telastyn
3
Masalah terbesar dengan tes unit dalam pengalaman saya adalah bahwa menulis yang bagus sama sulitnya dengan menulis kode yang baik. Jika Anda tidak dapat mengetahui kode yang baik dari kode yang buruk, menulis unit test tidak akan membuat kode Anda lebih baik. Saat menulis unit test, Anda harus bisa membedakan mana yang lancar, menyenangkan, dan "canggung" atau sulit. Mereka mungkin membuat Anda menggunakan sedikit kode Anda, tetapi mereka tidak memaksa Anda untuk mengakui bahwa apa yang Anda lakukan itu buruk.
jpmc26
2
@ user949300 - contoh klasik yang ada dalam pikiran saya di sini adalah Repositori yang membutuhkan connString. Misalkan Anda mengeksposnya sebagai properti yang dapat ditulisi publik, dan harus mengaturnya setelah Anda baru () sebuah Repositori. Idenya adalah bahwa setelah ke-5 atau ke-6 Anda telah menulis tes yang lupa untuk mengambil langkah itu - dan dengan demikian crash - Anda akan "secara alami" cenderung memaksa connString menjadi kelas invarian - lulus dalam konstruktor - sehingga membuat Anda API lebih baik dan membuatnya lebih mungkin bahwa kode produksi dapat ditulis yang menghindari jebakan ini. Itu bukan jaminan tapi itu membantu, imo.
Stephen Byrne
31

Butuh waktu cukup lama bagi saya untuk menyadari, tetapi manfaat sebenarnya (sunting: bagi saya, milage Anda mungkin berbeda) dari melakukan pengembangan yang digerakkan oleh tes ( menggunakan unit test) adalah Anda harus melakukan desain API di depan !

Pendekatan khas untuk pengembangan adalah pertama-tama mencari tahu bagaimana menyelesaikan masalah yang diberikan, dan dengan pengetahuan itu dan desain implementasi awal beberapa cara untuk meminta solusi Anda. Ini mungkin memberikan beberapa hasil yang agak menarik.

Ketika melakukan TDD Anda harus menulis kode yang akan menggunakan solusi Anda. Masukkan parameter, dan output yang diharapkan sehingga Anda dapat memastikan itu benar. Itu pada gilirannya mengharuskan Anda untuk mencari tahu apa yang sebenarnya perlu Anda lakukan, sehingga Anda dapat membuat tes yang bermakna. Kemudian dan baru Anda mengimplementasikan solusi. Ini juga pengalaman saya bahwa ketika Anda tahu persis apa yang seharusnya dicapai kode Anda, itu menjadi lebih jelas.

Kemudian, setelah pengujian unit implementasi membantu Anda memastikan bahwa refactoring tidak merusak fungsi, dan memberikan dokumentasi tentang cara menggunakan kode Anda (yang Anda tahu benar saat tes berlalu!). Tetapi ini adalah yang kedua - manfaat terbesar adalah pola pikir dalam menciptakan kode sejak awal.

Thorbjørn Ravn Andersen
sumber
Itu tentu saja merupakan manfaat tetapi saya tidak berpikir itu adalah manfaat "nyata" - manfaat nyata berasal dari kenyataan bahwa tes menulis untuk kode Anda secara alami mendorong "kondisi" ke dependensi dan meredam injeksi ketergantungan yang berlebihan (lebih lanjut mempromosikan abstraksi ) sebelum dimulai.
Semut P
Masalahnya adalah bahwa Anda menulis seluruh rangkaian tes di muka yang cocok dengan API itu, maka itu tidak berfungsi persis seperti yang diperlukan dan Anda harus menulis ulang kode Anda dan semua tes. Untuk API yang menghadap publik, kemungkinan mereka tidak akan berubah dan pendekatan ini baik-baik saja. Namun, API untuk kode yang hanya digunakan secara internal banyak berubah saat Anda mengetahui cara mengimplementasikan fitur yang membutuhkan banyak API semi-swasta yang bekerja bersama
Juan Mendes
@AntP Ya, ini adalah bagian dari desain API.
Thorbjørn Ravn Andersen
@JuanMendes Ini tidak biasa dan tes-tes itu perlu diubah, sama seperti kode lain ketika Anda mengubah persyaratan. IDE yang baik akan membantu Anda memperbaiki kelas sebagai bagian dari pekerjaan yang dilakukan secara otomatis ketika Anda mengubah tanda tangan metode, dll.
Thorbjørn Ravn Andersen
@JuanMendes jika Anda menulis tes yang bagus dan unit kecil dampak dari efek yang Anda gambarkan itu kecil atau tidak ada dalam praktiknya.
Semut P
6

Saya setuju 100% bahwa unit test membantu "membantu kami untuk memperbaiki hal-hal desain dan refactor kami".

Saya bingung apakah mereka membantu Anda melakukan desain awal . Ya, mereka mengungkapkan kelemahan yang jelas, dan apakah memaksa Anda untuk berpikir tentang "bagaimana saya bisa membuat kode diuji"? Ini akan menyebabkan lebih sedikit efek samping, konfigurasi dan pengaturan yang lebih mudah, dll.

Namun, dalam pengalaman saya, tes unit yang terlalu sederhana, ditulis sebelum Anda benar-benar memahami apa yang seharusnya menjadi desain, (memang, itu berlebihan TDD hard-core, tetapi terlalu sering coders menulis tes sebelum mereka berpikir banyak) sering menyebabkan anemia model domain yang mengekspos terlalu banyak internal.

Pengalaman saya dengan TDD adalah beberapa tahun yang lalu, jadi saya tertarik mendengar teknik baru apa yang mungkin membantu dalam menulis tes yang tidak terlalu bias pada desain yang mendasarinya. Terima kasih.

pengguna949300
sumber
Jumlah parameter metode yang panjang adalah bau kode dan cacat desain.
Sufian
5

Uji unit memungkinkan Anda melihat bagaimana antarmuka antar fungsi bekerja, dan sering memberi Anda wawasan tentang cara meningkatkan desain lokal dan desain keseluruhan. Selain itu jika Anda mengembangkan tes unit Anda saat mengembangkan kode Anda, Anda memiliki suite uji regresi siap pakai. Tidak masalah jika Anda mengembangkan UI atau perpustakaan backend.

Setelah program dikembangkan (dengan unit test), saat bug ditemukan, Anda dapat menambahkan tes untuk memastikan bahwa bug sudah diperbaiki.

Saya menggunakan TDD untuk beberapa proyek saya. Saya berupaya keras dalam membuat contoh yang saya tarik dari buku teks atau dari makalah yang dianggap benar, dan menguji kode yang saya kembangkan menggunakan contoh ini. Kesalahpahaman yang saya miliki tentang metode menjadi sangat jelas.

Saya cenderung sedikit lebih longgar daripada beberapa rekan saya, karena saya tidak peduli apakah kode itu ditulis terlebih dahulu atau tes ditulis terlebih dahulu.

Robert Baron
sumber
Itu jawaban yang bagus untuk saya. Maukah Anda memberikan beberapa contoh, misalnya satu untuk setiap kasus (ketika Anda mendapatkan wawasan tentang desain, dll.).
User039402
5

Ketika Anda ingin menguji unit parser Anda mendeteksi nilai pembatasan dengan benar, Anda mungkin ingin lulus satu baris dari file CSV. Untuk membuat tes Anda langsung dan singkat, Anda mungkin ingin mengujinya melalui satu metode yang menerima satu baris.

Ini secara otomatis akan membuat Anda memisahkan pembacaan garis dari membaca nilai individual.

Pada tingkat lain Anda mungkin tidak ingin meletakkan semua jenis file CSV fisik dalam proyek pengujian Anda tetapi melakukan sesuatu yang lebih mudah dibaca, cukup mendeklarasikan string CSV besar di dalam pengujian Anda untuk meningkatkan keterbacaan dan maksud pengujian. Ini akan mengarahkan Anda untuk memisahkan parser Anda dari I / O apa pun yang akan Anda lakukan di tempat lain.

Hanya contoh dasar, mulailah berlatih, Anda akan merasakan keajaiban di beberapa titik (saya punya).

Joppe
sumber
4

Sederhananya, penulisan unit test membantu mengekspos kesalahan dalam kode Anda.

Panduan spektakuler untuk menulis kode yang dapat diuji ini , yang ditulis oleh Jonathan Wolter, Russ Ruffer, dan Miško Hevery, berisi banyak contoh tentang bagaimana cacat dalam kode, yang terjadi untuk menghambat pengujian, juga mencegah penggunaan ulang yang mudah dan fleksibilitas dari kode yang sama. Dengan demikian, jika kode Anda dapat diuji, lebih mudah digunakan. Sebagian besar "moral" adalah tips sederhana yang sangat meningkatkan desain kode ( Dependency Injection FTW).

Sebagai contoh: Sangat sulit untuk menguji apakah metode computeStuff beroperasi dengan benar ketika cache mulai mengusir barang. Ini karena Anda harus menambahkan sampah secara manual ke cache hingga "bigCache" hampir penuh.

public OopsIHardcoded {

   Cache cacheOfExpensiveComputations;

   OopsIHardcoded() {
       this.cacheOfExpensiveComputation = buildBigCache();
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Namun, ketika kita menggunakan injeksi dependensi, jauh lebih mudah untuk menguji apakah metode computeStuff beroperasi dengan benar ketika cache mulai mengusir barang. Yang kami lakukan adalah membuat tes di mana kami memanggil new HereIUseDI(buildSmallCache()); Pemberitahuan, kami memiliki kontrol lebih bernuansa objek dan membayar dividen segera.

public HereIUseDI {

   Cache cacheOfExpensiveComputations;

   HereIUseDI(Cache cache) {
       this.cacheOfExpensiveComputation = cache;
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Manfaat serupa dapat diperoleh ketika kode kami membutuhkan data yang biasanya disimpan dalam basis data ... cukup masukkan secara tepat data yang Anda butuhkan.

Ivan
sumber
2
Jujur, saya tidak yakin bagaimana Anda memaksudkan contoh. Bagaimana metode computeStuff berhubungan dengan cache?
John V
1
@ user970696 - Ya, saya menyiratkan bahwa "computeStuff ()" menggunakan cache. Pertanyaannya adalah "Apakah computeStuff () berfungsi dengan benar sepanjang waktu (yang tergantung pada keadaan cache)" Akibatnya, sulit untuk mengkonfirmasi bahwa computeStuff () melakukan apa yang Anda inginkan UNTUK SEMUA NEGARA CACHE MUNGKIN jika Anda tidak dapat langsung mengatur / membangun cache karena Anda membuat hardcod pada baris "cacheOfExpensiveComputation = buildBigCache ();" (Berbeda dengan meneruskan cache secara langsung melalui konstruktor)
Ivan
0

Tergantung pada apa yang dimaksud dengan 'Tes Unit', saya tidak berpikir tes Unit tingkat rendah benar-benar memfasilitasi desain yang baik sebanyak tes integrasi tingkat yang sedikit lebih tinggi - tes yang menguji sekelompok pelaku (kelas, fungsi, apa pun) di kode Anda digabungkan dengan benar untuk menghasilkan sejumlah perilaku yang diinginkan yang telah disepakati antara tim pengembangan dan pemilik produk.

Jika Anda dapat menulis tes pada level tersebut, itu mendorong Anda untuk membuat kode yang bagus, logis, seperti API yang tidak memerlukan banyak dependensi gila - keinginan untuk memiliki pengaturan pengujian sederhana secara alami akan mendorong Anda untuk tidak memiliki banyak dependensi gila atau kode ketat digabungkan.

Jangan salah - Tes unit dapat mengarahkan Anda ke desain yang buruk, serta desain yang baik. Saya telah melihat pengembang mengambil sedikit kode yang sudah memiliki desain logis yang bagus dan satu perhatian, dan memisahkannya dan memperkenalkan lebih banyak antarmuka murni untuk tujuan pengujian, dan sebagai hasilnya membuat kode tersebut kurang mudah dibaca dan sulit untuk diubah , dan bahkan mungkin memiliki lebih banyak bug jika pengembang telah memutuskan bahwa memiliki banyak tes unit tingkat rendah berarti mereka tidak harus memiliki tes tingkat yang lebih tinggi. Contoh favorit tertentu adalah bug yang saya perbaiki di mana ada banyak kode yang sangat mudah rusak, 'dapat diuji' berkaitan dengan mendapatkan informasi di dalam dan di luar papan klip. Semua dipecah dan dipisahkan ke tingkat detail yang sangat kecil, dengan banyak antarmuka, banyak mengejek dalam ujian, dan hal-hal menyenangkan lainnya. Hanya satu masalah - tidak ada kode yang benar-benar berinteraksi dengan mekanisme clipboard OS,

Tes unit pasti dapat mendorong desain Anda - tetapi mereka tidak secara otomatis memandu Anda ke desain yang baik. Anda perlu memiliki ide tentang desain yang bagus yang melampaui 'kode ini diuji, oleh karena itu dapat diuji, oleh karena itu bagus'.

Tentu saja jika Anda salah satu dari orang-orang yang 'tes unit' berarti 'tes otomatis apa pun yang tidak didorong melalui UI', maka beberapa dari peringatan itu mungkin tidak begitu relevan - seperti yang saya katakan, saya pikir yang lebih tinggi -tingkat integrasi sering lebih berguna ketika datang untuk mengarahkan desain Anda.

topo Reinstate Monica
sumber
-2

Tes unit dapat membantu refactoring ketika kode baru melewati semua tes lama .

Katakanlah Anda telah mengimplementasikan bubblesort karena Anda sedang terburu-buru dan tidak peduli dengan kinerja, tetapi sekarang Anda ingin quicksort karena datanya semakin lama. Jika semua tes lulus, semuanya terlihat baik.

Tentu saja tes harus komprehensif untuk membuat pekerjaan ini. Dalam contoh saya, tes Anda mungkin tidak mencakup stabilitas karena itu tidak ada hubungannya dengan bubblesort.

om
sumber
1
Ini benar tetapi itu lebih merupakan manfaat rawatan daripada dampak langsung pada kualitas desain kode.
Semut P
@ Antnt, OP bertanya tentang refactoring dan unit test.
om
1
Pertanyaan tersebut menyebutkan refactoring tetapi pertanyaan sebenarnya adalah tentang bagaimana pengujian unit dapat meningkatkan / memverifikasi desain kode - tidak mempermudah proses refactoring itu sendiri.
Semut P
-3

Saya telah menemukan unit test paling berharga untuk memfasilitasi pemeliharaan proyek jangka panjang. Ketika saya kembali ke proyek setelah berbulan-bulan dan tidak ingat banyak detailnya, menjalankan tes membuat saya tidak merusak barang-barang.

David Baker
sumber
6
Itu tentu saja merupakan aspek penting dari tes, tetapi tidak benar-benar menjawab pertanyaan (yang bukan mengapa tes itu baik, tetapi bagaimana mereka mempengaruhi desain).
Hulk