Hari ini saya melakukan diskusi yang menarik dengan seorang kolega.
Saya seorang programmer defensif. Saya percaya bahwa aturan " kelas harus memastikan bahwa objeknya memiliki keadaan yang valid ketika berinteraksi dengan dari luar kelas " harus selalu dipatuhi. Alasan aturan ini adalah bahwa kelas tidak tahu siapa penggunanya dan bahwa ia dapat diprediksi gagal ketika berinteraksi dengan secara ilegal. Menurut pendapat saya aturan itu berlaku untuk semua kelas.
Dalam situasi khusus di mana saya berdiskusi hari ini, saya menulis kode yang memvalidasi bahwa argumen untuk konstruktor saya benar (misalnya parameter integer harus> 0) dan jika prasyarat tidak terpenuhi, maka pengecualian dilemparkan. Sebaliknya, kolega saya percaya bahwa pemeriksaan semacam itu berlebihan, karena tes unit harus menangkap penggunaan kelas yang salah. Selain itu ia percaya bahwa validasi pemrograman defensif juga harus diuji unit, sehingga pemrograman defensif menambah banyak pekerjaan dan karenanya tidak optimal untuk TDD.
Benarkah TDD mampu menggantikan pemrograman defensif? Apakah validasi parameter (dan maksud saya bukan input pengguna) tidak perlu sebagai konsekuensinya? Atau apakah kedua teknik saling melengkapi?
sumber
Jawaban:
Itu konyol. TDD memaksa kode untuk lulus tes dan memaksa semua kode untuk melakukan beberapa tes di sekitarnya. Itu tidak mencegah konsumen Anda dari salah memanggil kode, juga tidak secara ajaib mencegah programer kehilangan kasus pengujian.
Tidak ada metodologi yang dapat memaksa pengguna untuk menggunakan kode dengan benar.
Ada adalah argumen sedikit harus dibuat bahwa jika Anda sempurna melakukan TDD Anda akan menangkap Anda> 0 cek dalam kasus tes, sebelum mengimplementasikannya, dan membahas hal ini - mungkin oleh Anda menambahkan cek. Tetapi jika Anda melakukan TDD, kebutuhan Anda (> 0 dalam konstruktor) pertama kali akan muncul sebagai testcase yang gagal. Dengan demikian memberi Anda tes setelah Anda menambahkan cek Anda.
Juga masuk akal untuk menguji beberapa kondisi defensif (Anda menambahkan logika, mengapa Anda tidak ingin menguji sesuatu yang begitu mudah diuji?). Saya tidak yakin mengapa Anda tampaknya tidak setuju dengan ini.
TDD akan mengembangkan tes. Menerapkan validasi parameter akan membuatnya lulus.
sumber
Program defensive dan unit test adalah dua cara berbeda untuk menangkap kesalahan dan masing-masing memiliki kekuatan yang berbeda. Menggunakan hanya satu cara mendeteksi kesalahan membuat mekanisme pendeteksian kesalahan Anda rapuh. Menggunakan keduanya akan menangkap kesalahan yang mungkin terlewatkan oleh satu atau yang lain, bahkan dalam kode yang bukan API yang menghadap publik; misalnya, seseorang mungkin lupa menambahkan unit test untuk data tidak valid yang diteruskan ke API publik. Memeriksa segala sesuatu di tempat yang tepat berarti lebih banyak peluang untuk menangkap kesalahan.
Dalam keamanan informasi, ini disebut Pertahanan Dalam Kedalaman. Memiliki beberapa lapis pertahanan memastikan bahwa jika seseorang gagal, masih ada yang lain untuk menangkapnya.
Rekan Anda benar tentang satu hal: Anda harus menguji validasi Anda, tetapi ini bukan "pekerjaan yang tidak perlu". Ini sama dengan menguji kode lain, Anda ingin memastikan semua penggunaan, bahkan yang tidak valid, memiliki hasil yang diharapkan.
sumber
TDD sama sekali tidak menggantikan pemrograman defensif. Sebagai gantinya, Anda dapat menggunakan TDD untuk memastikan semua pertahanan ada dan berfungsi seperti yang diharapkan.
Di TDD, Anda tidak seharusnya menulis kode tanpa menulis tes terlebih dahulu - ikuti siklus refactor merah-hijau secara religius. Itu berarti bahwa jika Anda ingin menambahkan validasi, tulis terlebih dahulu tes yang membutuhkan validasi ini. Panggil metode yang dipermasalahkan dengan angka negatif dan dengan nol, dan berharap metode ini mengeluarkan pengecualian.
Juga, jangan lupa langkah "refactor". Meskipun TDD digerakkan oleh tes , ini tidak berarti hanya untuk pengujian . Anda harus tetap menerapkan desain yang tepat, dan menulis kode yang masuk akal. Menulis kode defensif adalah kode yang masuk akal, karena membuat ekspektasi lebih eksplisit dan kode Anda secara keseluruhan lebih kuat - melihat kemungkinan kesalahan lebih awal membuat mereka lebih mudah di-debug.
Tapi bukankah kita seharusnya menggunakan tes untuk menemukan kesalahan? Pernyataan dan tes saling melengkapi. Strategi pengujian yang baik akan memadukan berbagai pendekatan untuk memastikan perangkat lunak tersebut kuat. Hanya pengujian unit atau hanya pengujian integrasi atau hanya pernyataan dalam kode yang semuanya tidak memuaskan, Anda memerlukan kombinasi yang baik untuk mencapai tingkat kepercayaan yang cukup pada perangkat lunak Anda dengan upaya yang dapat diterima.
Lalu ada kesalahpahaman konseptual yang sangat besar tentang rekan kerja Anda: Tes unit tidak pernah bisa menguji penggunaan kelas Anda, hanya saja kelas itu sendiri berfungsi seperti yang diharapkan dalam isolasi. Anda akan menggunakan tes integrasi untuk memeriksa bahwa interaksi antara berbagai komponen berfungsi, tetapi ledakan kombinatorial dari kasus uji yang memungkinkan membuatnya tidak mungkin untuk menguji semuanya. Tes integrasi karenanya harus membatasi diri pada beberapa kasus penting. Tes yang lebih terperinci yang juga mencakup kasus tepi dan kasus kesalahan lebih cocok untuk pengujian unit.
sumber
Tes ada untuk mendukung dan memastikan pemrograman defensif
Pemrograman defensif melindungi integritas sistem saat runtime.
Tes adalah alat diagnostik (kebanyakan statis). Saat runtime, tes Anda tidak terlihat. Mereka seperti perancah yang digunakan untuk memasang tembok bata tinggi atau kubah batu. Anda tidak meninggalkan bagian penting dari struktur karena Anda memiliki perancah yang menahannya selama konstruksi. Anda memiliki perancah yang menahannya selama konstruksi untuk memudahkan memasukkan semua bagian penting.
EDIT: Sebuah analogi
Bagaimana dengan analogi komentar dalam kode?
Komentar memiliki tujuan, tetapi bisa berlebihan atau bahkan berbahaya. Misalnya, jika Anda memasukkan pengetahuan intrinsik tentang kode ke dalam komentar , lalu ubah kodenya, komentar menjadi tidak relevan yang terbaik dan berbahaya paling buruk.
Jadi katakan Anda memasukkan banyak pengetahuan intrinsik dari basis kode Anda ke dalam tes, seperti MethodA tidak dapat mengambil nol dan argumen MethodB harus
> 0
. Kemudian kodenya berubah. Null oke untuk A sekarang, dan B dapat mengambil nilai sekecil -10. Tes yang ada sekarang salah secara fungsional, tetapi akan terus berlalu.Ya, Anda harus memperbarui tes pada saat yang sama Anda memperbarui kode. Anda juga harus memperbarui (atau menghapus) komentar pada saat yang sama saat Anda memperbarui kode. Tetapi kita semua tahu hal-hal ini tidak selalu terjadi, dan bahwa kesalahan telah terjadi.
Tes memverifikasi perilaku sistem. Perilaku aktual itu intrinsik ke sistem itu sendiri, bukan intrinsik pada tes.
Apa yang mungkin salah?
Tujuan yang berkaitan dengan tes adalah untuk memikirkan segala sesuatu yang bisa salah, menulis tes untuk itu yang memeriksa perilaku yang benar, kemudian menyusun kode runtime sehingga melewati semua tes.
Yang berarti pemrograman defensif adalah intinya .
TDD menggerakkan pemrograman defensif, jika tesnya komprehensif.
Lebih banyak tes, mendorong pemrograman yang lebih defensif
Ketika bug ditemukan, lebih banyak tes ditulis untuk memodelkan kondisi yang memanifestasikan bug. Kemudian kode diperbaiki, dengan kode untuk membuat tes - tes itu berlalu, dan tes-tes baru tetap di dalam test suite.
Seperangkat tes yang baik akan melewati argumen baik dan buruk ke fungsi / metode, dan mengharapkan hasil yang konsisten. Ini, pada gilirannya, berarti komponen yang diuji akan menggunakan pemeriksaan prakondisi (pemrograman defensif) untuk mengkonfirmasi argumen yang diberikan kepadanya.
Secara umum ...
Misalnya, jika argumen nol untuk prosedur tertentu tidak valid, maka setidaknya satu tes akan lulus nol, dan itu akan mengharapkan pengecualian / kesalahan "argumen nol tidak valid" dari beberapa jenis.
Setidaknya satu tes lain akan melewati argumen yang valid , tentu saja - atau loop melalui array besar dan melewati beberapa argumen yang valid - dan mengkonfirmasi bahwa keadaan yang dihasilkan sesuai.
Jika tes tidak lulus argumen nol itu dan ditampar dengan pengecualian yang diharapkan (dan pengecualian itu dilemparkan karena kode memeriksa keadaan yang diteruskan secara defensif), maka nol dapat berakhir ditugaskan ke properti kelas atau dikubur dalam koleksi semacam di mana seharusnya tidak.
Ini mungkin menyebabkan perilaku tak terduga di beberapa bagian sistem yang sama sekali berbeda tempat instance kelas diteruskan, di beberapa lokasi geografis yang jauh setelah perangkat lunak dikirimkan . Dan itu adalah hal yang sebenarnya kita coba hindari, kan?
Itu bahkan bisa lebih buruk. Instance class dengan state yang tidak valid dapat diserialisasi dan disimpan, hanya untuk menyebabkan kegagalan ketika itu disusun kembali untuk digunakan nanti. Ya ampun, saya tidak tahu, mungkin ini semacam sistem kontrol mekanis yang tidak dapat memulai kembali setelah dimatikan karena tidak dapat menghapus status konfigurasi persistennya sendiri. Atau instance kelas dapat serial dan diteruskan ke beberapa sistem yang sama sekali berbeda yang dibuat oleh beberapa entitas lain, dan bahwa sistem mungkin crash.
Terutama jika pemrogram sistem lain itu tidak kode pertahanan.
sumber
Alih-alih TDD mari kita bicara tentang "pengujian perangkat lunak" secara umum, dan bukannya "pemrograman defensif" secara umum, mari kita bicara tentang cara favorit saya melakukan pemrograman defensif, yaitu dengan menggunakan pernyataan.
Jadi, karena kita melakukan pengujian perangkat lunak, kita harus berhenti menempatkan pernyataan tegas dalam kode produksi, kan? Biarkan saya menghitung cara yang salah:
Pernyataan bersifat opsional, jadi jika Anda tidak menyukainya, jalankan saja sistem Anda dengan pernyataan dinonaktifkan.
Pernyataan memeriksa hal-hal yang pengujian tidak dapat (dan tidak boleh.) Karena pengujian seharusnya memiliki tampilan kotak hitam dari sistem Anda, sedangkan pernyataan memiliki tampilan kotak putih. (Tentu saja, karena mereka tinggal di dalamnya.)
Pernyataan adalah alat dokumentasi yang sangat baik. Tidak ada komentar yang pernah, atau akan pernah, sama jelasnya dengan sepotong kode yang menyatakan hal yang sama. Juga, dokumentasi cenderung menjadi ketinggalan jaman ketika kode berevolusi, dan itu sama sekali tidak dapat ditegakkan oleh kompiler.
Pernyataan bisa menangkap kesalahan dalam kode pengujian. Pernahkah Anda mengalami situasi di mana tes gagal, dan Anda tidak tahu siapa yang salah - kode produksi, atau tes?
Pernyataan bisa lebih relevan daripada pengujian. Tes akan memeriksa apa yang ditentukan oleh persyaratan fungsional, tetapi kode seringkali harus membuat asumsi tertentu yang jauh lebih teknis dari itu. Orang yang menulis dokumen persyaratan fungsional jarang berpikir pembagian dengan nol.
Asumsi menunjukkan kesalahan yang hanya diuji secara luas pada pengujian. Jadi, tes Anda menyiapkan beberapa prasyarat luas, meminta beberapa potong kode panjang, mengumpulkan hasilnya, dan menemukan bahwa mereka tidak seperti yang diharapkan. Dengan pemecahan masalah yang cukup Anda akhirnya akan menemukan secara tepat di mana kesalahan terjadi, tetapi penegasan biasanya akan menemukannya terlebih dahulu.
Pernyataan mengurangi kompleksitas program. Setiap baris kode yang Anda tulis meningkatkan kompleksitas program. Pernyataan dan kata kunci
final
(readonly
) adalah dua konstruksi yang saya tahu yang sebenarnya mengurangi kompleksitas program. Itu sangat berharga.Pernyataan membantu kompiler lebih memahami kode Anda. Silakan coba ini di rumah:
void foo( Object x ) { assert x != null; if( x == null ) { } }
kompiler Anda harus mengeluarkan peringatan yang memberi tahu Anda bahwa kondisinyax == null
selalu salah. Itu bisa sangat berguna.Di atas adalah ringkasan posting dari blog saya, 2014-09-21 "Pernyataan dan Pengujian"
sumber
Saya percaya sebagian besar jawaban tidak memiliki perbedaan kritis: Itu tergantung pada bagaimana kode Anda akan digunakan.
Apakah modul yang dipermasalahkan akan digunakan oleh klien lain independen dari aplikasi yang Anda uji? Jika Anda menyediakan perpustakaan atau API untuk digunakan oleh pihak ketiga, Anda tidak memiliki cara untuk memastikan mereka hanya memanggil kode Anda dengan input yang valid. Anda harus memvalidasi semua input.
Tetapi jika modul tersebut hanya digunakan oleh kode yang Anda kontrol, maka teman Anda mungkin benar. Anda dapat menggunakan tes unit untuk memverifikasi bahwa modul yang dimaksud hanya dipanggil dengan input yang valid. Pemeriksaan prekondisi masih dapat dianggap sebagai praktik yang baik, tetapi ini merupakan trade-off: Jika Anda membuang-buang kode yang memeriksa kondisi yang Anda tahu tidak akan pernah muncul, itu hanya mengaburkan maksud kode.
Saya tidak setuju bahwa pemeriksaan prekondisi memerlukan lebih banyak unit-tes. Jika Anda memutuskan tidak perlu menguji beberapa bentuk input yang tidak valid, maka tidak masalah jika fungsi tersebut berisi pemeriksaan prasyarat atau tidak. Ingat tes harus memverifikasi perilaku, bukan detail implementasi.
sumber
Argumen ini agak membingungkan saya, karena ketika saya mulai berlatih TDD, unit saya menguji bentuk "objek merespon dengan cara tertentu> ketika <input tidak valid>" meningkat 2 atau 3 kali. Saya ingin tahu bagaimana kolega Anda berhasil lulus tes unit seperti itu tanpa fungsinya melakukan validasi.
Kasus sebaliknya, bahwa tes unit menunjukkan Anda tidak pernah menghasilkan output buruk yang akan diteruskan ke argumen fungsi lain, jauh lebih sulit untuk dibuktikan. Seperti halnya case pertama, ini sangat tergantung pada cakupan menyeluruh dari edge case, tetapi Anda memiliki persyaratan tambahan bahwa semua input fungsi Anda harus berasal dari output fungsi lain yang outputnya telah Anda uji unit dan bukan dari, katakanlah, input pengguna atau modul pihak ketiga.
Dengan kata lain, apa yang dilakukan TDD tidak mencegah Anda dari membutuhkan kode validasi sebanyak membantu Anda menghindari melupakannya .
sumber
Saya pikir saya menafsirkan komentar kolega Anda secara berbeda dari sebagian besar sisa jawaban.
Menurut saya argumennya adalah:
Bagi saya, argumen ini memiliki beberapa logika untuk itu, tetapi terlalu banyak bergantung pada unit test untuk mencakup setiap situasi yang mungkin. Fakta sederhananya adalah bahwa 100% garis / cabang / jalur cakupan tidak serta merta melaksanakan setiap nilai yang mungkin dilewati oleh penelepon, sedangkan cakupan 100% dari semua kemungkinan status penelepon (yaitu, semua nilai yang mungkin dari inputnya dan variabel) tidak layak secara komputasi.
Oleh karena itu saya cenderung lebih suka menguji unit penelepon untuk memastikan bahwa (sejauh tes berlangsung) mereka tidak pernah lulus dalam nilai buruk, dan juga mengharuskan komponen Anda gagal dalam beberapa cara yang dapat dikenali ketika nilai buruk dilewatkan ( setidaknya sejauh mungkin untuk mengenali nilai-nilai buruk dalam bahasa pilihan Anda). Ini akan membantu debugging ketika masalah terjadi dalam tes integrasi, dan juga akan membantu setiap pengguna kelas Anda yang kurang teliti dalam mengisolasi unit kode mereka dari ketergantungan itu.
Namun perlu diperhatikan, bahwa jika Anda mendokumentasikan dan menguji perilaku fungsi Anda ketika nilai <= 0 dilewatkan, maka nilai negatif tidak lagi tidak valid (setidaknya, tidak lebih tidak valid daripada argumen apa pun
throw
, karena itu juga didokumentasikan untuk melempar pengecualian!). Penelepon berhak untuk mengandalkan perilaku defensif itu. Bahasa mengizinkan, ini mungkin merupakan skenario terbaik - fungsi tidak memiliki "input tidak valid", tetapi penelepon yang berharap tidak memprovokasi fungsi untuk melempar pengecualian harus diuji unit cukup untuk memastikan mereka tidak ' t lulus nilai apa pun yang menyebabkan itu.Meskipun berpikir bahwa kolega Anda agak kurang sepenuhnya salah dari kebanyakan jawaban, saya mencapai kesimpulan yang sama, yaitu bahwa kedua teknik saling melengkapi. Program defensif, dokumentasikan cek defensif Anda, dan ujilah. Pekerjaan itu hanya "tidak perlu" jika pengguna kode Anda tidak dapat mengambil manfaat dari pesan kesalahan yang berguna ketika mereka melakukan kesalahan. Secara teori jika mereka benar-benar menguji semua kode mereka sebelum mengintegrasikannya dengan kode Anda, dan tidak pernah ada kesalahan dalam pengujian mereka, maka mereka tidak akan pernah melihat pesan kesalahan. Dalam prakteknya bahkan jika mereka melakukan TDD dan injeksi ketergantungan total, mereka mungkin masih mengeksplorasi selama pengembangan atau mungkin ada selang dalam pengujian mereka. Hasilnya adalah mereka memanggil kode Anda sebelum kode mereka sempurna!
sumber
Antarmuka publik dapat dan akan disalahgunakan
Klaim rekan kerja Anda "pengujian unit harus menangkap penggunaan yang salah dari kelas" adalah sepenuhnya salah untuk antarmuka apa pun yang tidak pribadi. Jika fungsi publik dapat dipanggil dengan argumen integer, maka ia dapat dan akan dipanggil dengan argumen integer apa pun , dan kode tersebut harus berperilaku dengan tepat. Jika tanda tangan fungsi publik menerima misalnya tipe Java Double, maka null, NaN, MAX_VALUE, -Inf adalah semua nilai yang mungkin. Tes unit Anda tidak dapat menangkap penggunaan kelas yang salah karena tes tersebut tidak dapat menguji kode yang akan menggunakan kelas ini, karena kode itu belum ditulis, mungkin belum ditulis oleh Anda, dan pasti akan berada di luar ruang lingkup pengujian unit Anda .
Di sisi lain, pendekatan ini mungkin berlaku untuk properti pribadi (semoga jauh lebih banyak) - jika sebuah kelas dapat memastikan bahwa beberapa fakta selalu benar (misalnya properti X tidak boleh nol, posisi integer tidak melebihi panjang maksimum , ketika fungsi A dipanggil, semua struktur data prasyarat terbentuk dengan baik) maka dapat tepat untuk menghindari memverifikasi ini lagi dan lagi untuk alasan kinerja, dan sebagai gantinya bergantung pada unit test.
sumber
Pertahanan terhadap penyalahgunaan adalah fitur , dikembangkan karena persyaratan untuk itu. (Tidak semua antarmuka memerlukan pemeriksaan ketat terhadap penyalahgunaan; misalnya yang internal sangat sempit.)
Fitur ini memerlukan pengujian: apakah pertahanan terhadap penyalahgunaan benar-benar berfungsi? Tujuan dari pengujian fitur ini adalah untuk mencoba menunjukkan bahwa itu tidak: untuk membuat kesalahan penggunaan modul yang tidak terdeteksi oleh pemeriksaannya.
Jika pemeriksaan khusus merupakan fitur yang diperlukan, memang tidak masuk akal untuk menyatakan bahwa keberadaan beberapa tes membuatnya tidak perlu. Jika ini adalah fitur dari beberapa fungsi yang (misalnya) dilempar pengecualian ketika parameter tiga negatif, maka itu tidak bisa dinegosiasikan; itu akan melakukan itu.
Namun, saya menduga bahwa kolega Anda sebenarnya masuk akal dari sudut pandang situasi di mana tidak ada persyaratan untuk pemeriksaan khusus pada input, dengan respons spesifik terhadap input buruk: situasi di mana hanya ada persyaratan umum yang dipahami untuk kekokohan.
Pemeriksaan saat masuk ke beberapa fungsi tingkat atas ada, sebagian, untuk melindungi beberapa kode internal yang lemah atau tidak diuji dari kombinasi parameter yang tidak terduga (sehingga jika kode tersebut diuji dengan baik, pemeriksaan tidak perlu: kode hanya dapat " cuaca "parameter buruk).
Ada kebenaran dalam ide kolega itu, dan apa yang kemungkinan ia maksudkan adalah ini: jika kita membangun sebuah fungsi dari bagian-bagian tingkat bawah yang sangat kuat yang diberi kode pertahanan dan diuji secara individual terhadap semua penyalahgunaan, maka mungkin fungsi tingkat yang lebih tinggi untuk menjadi kuat tanpa harus melakukan pemeriksaan sendiri yang ekstensif.
Jika kontraknya dilanggar, maka itu akan diterjemahkan ke beberapa penyalahgunaan fungsi tingkat bawah, mungkin dengan melemparkan pengecualian atau apa pun.
Satu-satunya masalah dengan itu adalah bahwa pengecualian tingkat bawah tidak spesifik untuk antarmuka tingkat yang lebih tinggi. Apakah itu masalah tergantung pada apa persyaratannya. Jika persyaratannya hanya "fungsinya harus kuat terhadap penyalahgunaan dan melemparkan semacam pengecualian daripada crash, atau terus menghitung dengan data sampah" maka pada kenyataannya itu mungkin tercakup oleh semua kekokohan potongan-potongan tingkat bawah di mana itu adalah dibangun di.
Jika fungsi itu memiliki persyaratan untuk pelaporan kesalahan yang sangat spesifik dan terperinci yang terkait dengan parameternya, maka pemeriksaan tingkat bawah tidak sepenuhnya memenuhi persyaratan tersebut. Mereka memastikan hanya bahwa fungsi meledak entah bagaimana (tidak melanjutkan dengan kombinasi parameter yang buruk, menghasilkan hasil sampah). Jika kode klien ditulis untuk secara khusus menangkap kesalahan tertentu dan menanganinya, itu mungkin tidak berfungsi dengan benar. Kode klien itu sendiri dapat memperoleh, sebagai input, data yang menjadi dasar parameter, dan mungkin mengharapkan fungsi untuk memeriksa ini dan untuk menerjemahkan nilai-nilai buruk ke kesalahan spesifik seperti yang didokumentasikan (sehingga dapat menangani kesalahan dengan benar) daripada beberapa kesalahan lain yang tidak ditangani dan mungkin menghentikan gambar perangkat lunak.
TL; DR: kolega Anda mungkin bukan idiot; Anda hanya berbicara melewati satu sama lain dengan perspektif berbeda tentang hal yang sama, karena persyaratannya tidak sepenuhnya dipakukan dan Anda masing-masing memiliki gagasan yang berbeda tentang apa "persyaratan tidak tertulis" itu. Anda berpikir bahwa ketika tidak ada persyaratan khusus tentang pengecekan parameter, Anda harus tetap mendaftar pengecekan terperinci; kolega itu berpikir, biarkan saja kode tingkat rendah yang kuat meledak ketika parameternya salah. Agak tidak produktif untuk berdebat tentang persyaratan tidak tertulis melalui kode: mengakui bahwa Anda tidak setuju tentang persyaratan daripada kode. Cara pengkodean Anda mencerminkan persyaratan yang menurut Anda; cara kolega mewakili pandangannya tentang persyaratan. Jika Anda melihatnya seperti itu, jelas bahwa apa yang benar atau salah bukan t dalam kode itu sendiri; kode ini hanya proxy untuk pendapat Anda tentang spesifikasi apa yang seharusnya.
sumber
Tes menentukan kontrak kelas Anda.
Sebagai akibat wajar, tidak adanya tes mendefinisikan kontrak yang mencakup perilaku tidak terdefinisi . Jadi ketika Anda melewati
null
keFoo::Frobnicate(Widget widget)
, dan tak terhitung run-time malapetaka terjadi kemudian, Anda masih dalam kontrak kelas Anda.Kemudian Anda memutuskan, "kami tidak ingin kemungkinan perilaku tidak terdefinisi", yang merupakan pilihan yang masuk akal. Itu berarti bahwa Anda harus memiliki perilaku yang diharapkan untuk melewati
null
keFoo::Frobnicate(Widget widget)
.Dan Anda mendokumentasikan keputusan itu dengan memasukkan a
sumber
Serangkaian tes yang baik akan melatih antarmuka eksternal kelas Anda dan memastikan bahwa penyalahgunaan tersebut menghasilkan respons yang benar (pengecualian, atau apa pun yang Anda definisikan sebagai "benar"). Faktanya, test case pertama yang saya tulis untuk sebuah kelas adalah memanggil konstruktornya dengan argumen di luar jangkauan.
Jenis pemrograman defensif yang cenderung dihilangkan dengan pendekatan yang sepenuhnya diuji unit adalah validasi invarian internal yang tidak perlu yang tidak dapat dilanggar oleh kode eksternal.
Ide berguna yang kadang-kadang saya terapkan adalah untuk memberikan metode yang menguji invarian objek; metode merobohkan Anda dapat memanggilnya untuk memvalidasi bahwa tindakan eksternal Anda pada objek tidak pernah melanggar invarian.
sumber
Tes TDD akan menangkap kesalahan selama pengembangan kode .
Batas yang Anda uraikan sebagai bagian dari pemrograman defensif akan menangkap kesalahan selama penggunaan kode .
Jika kedua domain itu sama, yaitu kode yang Anda tulis hanya pernah digunakan secara internal oleh proyek khusus ini, maka mungkin benar bahwa TDD akan menghalangi perlunya batas-batas pemrograman defensif yang memeriksa Anda jelaskan, tetapi hanya jika jenis tersebut pemeriksaan batas secara khusus dilakukan dalam tes TDD .
Sebagai contoh spesifik, anggaplah bahwa perpustakaan kode keuangan dikembangkan menggunakan TDD. Salah satu tes mungkin menyatakan bahwa nilai tertentu tidak pernah bisa negatif. Itu memastikan bahwa pengembang perpustakaan tidak secara tidak sengaja menyalahgunakan kelas saat mereka mengimplementasikan fitur.
Tetapi setelah perpustakaan dirilis dan saya menggunakannya dalam program saya sendiri, tes TDD tidak mencegah saya menetapkan nilai negatif (dengan asumsi itu terbuka). Batas memeriksa akan.
Maksud saya adalah bahwa sementara pernyataan TDD dapat mengatasi masalah nilai negatif jika kode hanya pernah digunakan secara internal sebagai bagian dari pengembangan aplikasi yang lebih besar (di bawah TDD), jika itu akan menjadi perpustakaan yang digunakan oleh programmer lain tanpa TDD. kerangka kerja dan tes , batas memeriksa hal-hal.
sumber
TDD dan pemrograman defensif berjalan seiring. Menggunakan keduanya tidak berlebihan, tetapi pada kenyataannya saling melengkapi. Ketika Anda memiliki fungsi, Anda ingin memastikan bahwa fungsi berfungsi seperti yang dijelaskan dan tulis tes untuknya; jika Anda tidak membahas apa yang terjadi ketika dalam hal input buruk, pengembalian buruk, keadaan buruk, dll. maka Anda tidak cukup menulis tes Anda, dan kode Anda akan rapuh meskipun semua tes Anda lulus.
Sebagai insinyur yang disematkan, saya suka menggunakan contoh penulisan fungsi untuk hanya menambahkan dua byte bersama dan mengembalikan hasilnya seperti ini:
Sekarang jika Anda hanya melakukannya,
*(sum) = a + b
itu akan berhasil, tetapi hanya dengan beberapa input.a = 1
danb = 2
akan membuatsum = 3
; Namun karena ukuran penjumlahan adalah byte,a = 100
danb = 200
akan membuatsum = 44
karena melimpah. Dalam C, Anda akan mengembalikan kesalahan dalam hal ini untuk menandakan fungsi gagal; melempar pengecualian adalah hal yang sama dalam kode Anda. Tidak mempertimbangkan kegagalan atau menguji cara menanganinya tidak akan bekerja dalam jangka panjang, karena jika kondisi itu terjadi, mereka tidak akan ditangani dan dapat menyebabkan sejumlah masalah.sumber
sum
pointer nol?).