Apakah TDD membuat pemrograman defensif berlebihan?


104

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?


120
Anda menyerahkan pustaka yang sepenuhnya diuji unit Anda tanpa konstruktor memeriksa ke klien untuk digunakan, dan mereka melanggar kontrak kelas. Apa gunanya tes unit itu bagimu sekarang?
Robert Harvey

42
IMO sebaliknya. Pemrograman defensif, pra-dan pro-kondisi yang tepat, dan sistem tipe kaya membuat tes berlebihan.
Gardenhead

37
Dapatkah saya mengirim jawaban yang hanya mengatakan "Kesedihan yang bagus?" Pemrograman defensif melindungi sistem saat runtime. Pengujian memeriksa semua kondisi runtime potensial yang dapat dipikirkan oleh penguji, termasuk argumen tidak valid yang diteruskan ke konstruktor dan metode lain. Tes, jika lengkap, akan mengonfirmasi bahwa perilaku runtime akan seperti yang diharapkan, termasuk pengecualian yang sesuai yang dilemparkan atau perilaku disengaja lainnya terjadi ketika argumen yang tidak valid disahkan. Tetapi tes tidak melakukan apa-apa untuk melindungi sistem saat runtime.
Craig

16
"Tes unit harus menangkap penggunaan kelas yang salah" - eh, bagaimana? Tes unit akan menunjukkan kepada Anda perilaku yang diberikan argumen yang benar, dan ketika diberikan argumen yang salah; mereka tidak bisa menunjukkan semua argumen yang akan diberikan.
OJFord

34
Saya tidak berpikir saya telah melihat contoh yang lebih baik tentang bagaimana pemikiran dogmatis tentang pengembangan perangkat lunak dapat mengarah pada kesimpulan yang berbahaya.
sdenham

Jawaban:


196

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.

Atau apakah kedua teknik saling melengkapi?

TDD akan mengembangkan tes. Menerapkan validasi parameter akan membuatnya lulus.


7
Saya tidak setuju dengan keyakinan bahwa validasi prekondisi harus diuji, tetapi saya tidak setuju dengan pendapat rekan saya bahwa pekerjaan tambahan yang disebabkan oleh kebutuhan untuk menguji validasi prasyarat adalah argumen untuk tidak membuat validasi prasyarat di awal. tempat. Saya telah mengedit posting saya untuk mengklarifikasi.
user2180613

20
@ user2180613 Buat tes yang menguji bahwa kegagalan prasyarat ditangani dengan tepat: sekarang menambahkan cek bukan pekerjaan "ekstra", itu pekerjaan yang diminta oleh TDD untuk membuat tes hijau. Jika pendapat kolega Anda adalah bahwa Anda harus melakukan tes, mengamati itu gagal, dan kemudian dan hanya kemudian melaksanakan pemeriksaan prasyarat, maka ia mungkin memiliki poin dari sudut pandang TDD-purist. Jika dia mengatakan hanya mengabaikan cek sepenuhnya, maka dia bodoh. Tidak ada dalam TDD yang mengatakan Anda tidak bisa proaktif dalam menulis tes untuk mode kegagalan potensial.
RM

4
@RM Anda tidak sedang menulis tes untuk menguji pemeriksaan prasyarat. Anda sedang menulis tes untuk menguji perilaku yang benar yang diharapkan dari kode yang dipanggil. Pemeriksaan prasyarat adalah, dari sudut pandang tes, detail implementasi buram yang memastikan perilaku yang benar. Jika Anda memikirkan cara yang lebih baik untuk memastikan status yang benar dalam kode yang dipanggil, lakukan dengan cara itu daripada menggunakan pemeriksaan prasyarat tradisional. The tes akan menanggung apakah atau tidak Anda berhasil, dan masih tidak akan tahu atau peduli bagaimana Anda melakukannya.
Craig

@ user2180613 Itulah beberapa justifikasi yang luar biasa: D jika tujuan Anda dalam menulis perangkat lunak adalah Anda mengurangi jumlah tes yang Anda butuhkan untuk penulis dan jalankan, jangan menulis perangkat lunak apa pun - nol tes!
Gusdor

3
Kalimat terakhir dari jawaban ini tepat.
Robert Grant

32

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.


Apakah benar untuk mengatakan bahwa validasi parameter adalah bentuk validasi prekondisi dan tes unit adalah validasi pascondisi, yang mengapa mereka saling melengkapi?
user2180613

1
"Ini sama dengan menguji kode lain, Anda ingin memastikan semua penggunaan, bahkan yang tidak valid, memiliki hasil yang diharapkan." Ini. Seharusnya tidak ada kode yang melewati ketika input yang dilewatinya tidak dirancang untuk ditangani. Ini melanggar prinsip "gagal cepat", dan itu bisa menjadikan debugging sebagai mimpi buruk.
jpmc26

@ user2180613 - tidak juga, tetapi lebih dari tes unit memeriksa kondisi kegagalan yang diharapkan pengembang, sementara teknik pemrograman defensif memeriksa kondisi yang tidak diharapkan pengembang. Tes unit dapat digunakan untuk memvalidasi prasyarat (dengan menggunakan objek tiruan yang disuntikkan ke pemanggil yang memeriksa prasyarat).
Periata Breatta

1
@ jpmc26 Ya, kegagalan adalah "hasil yang diharapkan" untuk ujian. Anda menguji untuk menunjukkan bahwa itu gagal, daripada diam-diam menunjukkan beberapa perilaku yang tidak terduga (tidak terduga).
KRyan

6
TDD menangkap kesalahan dalam kode Anda sendiri, pemrograman defensif menangkap kesalahan dalam kode orang lain. TDD dengan demikian dapat membantu memastikan bahwa Anda cukup defensif :)
jwenting

30

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.


16

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.


2
Itu lucu, downvote datang begitu cepat, sekarang benar-benar ada cara downvoter bisa membaca di luar paragraf pertama.
Craig

1
:-) Saya baru saja memilih tanpa membaca paragraf pertama, jadi mudah-mudahan itu akan menyeimbangkannya ...
SusanW

1
Sepertinya yang paling bisa saya lakukan :-) (Sebenarnya, saya memang membaca sisanya hanya untuk memastikan. Tidak boleh ceroboh - terutama pada topik seperti ini!)
SusanW

1
Saya pikir Anda mungkin punya. :)
Craig

pemeriksaan defensif dapat dilakukan pada waktu kompilasi dengan alat-alat seperti Kontrak Kode.
Matthew Whited

9

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:

  1. Pernyataan bersifat opsional, jadi jika Anda tidak menyukainya, jalankan saja sistem Anda dengan pernyataan dinonaktifkan.

  2. 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.)

  3. 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.

  4. 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?

  5. 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.

  6. 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.

  7. 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.

  8. 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 kondisinya x == nullselalu salah. Itu bisa sangat berguna.

Di atas adalah ringkasan posting dari blog saya, 2014-09-21 "Pernyataan dan Pengujian"


Saya pikir saya sebagian besar tidak setuju dengan jawaban ini. (5) Dalam TDD, test suite adalah spesifikasinya. Anda seharusnya menulis kode paling sederhana yang membuat tes lulus, tidak lebih. (4) Alur kerja merah-hijau memastikan bahwa tes gagal ketika seharusnya dan berlalu ketika fungsionalitas yang dimaksud hadir. Pernyataan tidak banyak membantu di sini. (3,7) Dokumentasi adalah dokumentasi, pernyataan tidak. Tetapi dengan membuat asumsi eksplisit, kode menjadi lebih banyak mendokumentasikan diri. Saya akan menganggap mereka sebagai komentar yang dapat dieksekusi. (2) Pengujian kotak putih dapat menjadi bagian dari strategi pengujian yang valid.
amon

5
"Dalam TDD, test suite adalah spesifikasinya. Anda seharusnya menulis kode yang paling sederhana untuk membuat tes lulus, tidak lebih.": Saya tidak berpikir ini selalu merupakan ide yang baik: Seperti yang ditunjukkan dalam jawaban, ada asumsi internal tambahan dalam kode yang mungkin ingin diverifikasi. Bagaimana dengan bug internal yang saling membatalkan? Tes Anda lulus tetapi beberapa asumsi di dalam kode Anda salah, yang dapat menyebabkan bug berbahaya nantinya.
Giorgio

5

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.


4
Jika prosedur yang dipanggil tidak memverifikasi validitas input (yang merupakan debat asli) maka pengujian unit Anda tidak dapat memastikan bahwa modul yang dimaksud hanya dipanggil dengan input yang valid. Secara khusus, mungkin disebut dengan input yang tidak valid tetapi kebetulan mengembalikan hasil yang benar pula dalam kasus yang diuji - ada berbagai jenis perilaku yang tidak terdefinisi, penanganan overflow, dll yang dapat mengembalikan hasil yang diharapkan dalam lingkungan pengujian dengan optimasi yang dinonaktifkan tetapi gagal dalam produksi.
Peteris

@Peteris: Apakah Anda memikirkan perilaku tidak terdefinisi seperti di C? Memunculkan perilaku tidak terdefinisi yang memiliki hasil berbeda di lingkungan yang berbeda jelas merupakan bug, tetapi juga tidak dapat dicegah dengan pemeriksaan prakondisi. Misalnya, bagaimana Anda memeriksa argumen pointer menunjuk ke memori yang valid?
JacquesB

3
Ini hanya akan berfungsi di toko terkecil. Setelah tim Anda melampaui, katakanlah, enam orang, Anda akan tetap memerlukan pemeriksaan validasi.
Robert Harvey

1
@RobertHarvey: Dalam hal ini sistem harus dipartisi ke dalam subsistem dengan antarmuka yang terdefinisi dengan baik, dan validasi input dilakukan pada antarmuka.
JacquesB

ini. Tergantung pada kodenya, apakah kode ini digunakan oleh tim? Apakah tim memiliki akses ke kode sumber? Jika itu murni kode internal kemudian memeriksa argumen mungkin hanya menjadi beban, misalnya, Anda memeriksa 0 lalu melempar pengecualian, dan penelepon kemudian melihat ke dalam kode oh kelas ini dapat membuang pengecualian dll dll dan tunggu .. dalam hal ini bahwa objek tidak akan pernah menerima 0 karena mereka disaring 2 lvls sebelumnya. Kalau itu kode perpustakaan yang akan digunakan oleh pihak ke-3 itu cerita lain. Tidak semua kode ditulis untuk digunakan oleh seluruh dunia.
Aleksander Fular

3

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 .


2

Saya pikir saya menafsirkan komentar kolega Anda secara berbeda dari sebagian besar sisa jawaban.

Menurut saya argumennya adalah:

  • Semua kode kami adalah unit yang diuji.
  • Semua kode yang menggunakan komponen Anda adalah kode kami, atau jika tidak, unit ini diuji oleh orang lain (tidak dinyatakan secara eksplisit, tetapi itulah yang saya pahami dari "unit test harus menangkap penggunaan kelas yang salah").
  • Oleh karena itu, untuk setiap pemanggil dari fungsi Anda ada tes unit di suatu tempat yang mengejek komponen Anda, dan tes gagal jika pemanggil memberikan nilai yang tidak valid ke tiruan itu.
  • Karena itu, tidak masalah apa fungsi Anda ketika memberikan nilai yang tidak valid, karena pengujian kami mengatakan itu tidak dapat terjadi.

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!


Itu bisnis menempatkan penekanan pada pengujian penelepon untuk memastikan mereka tidak melewati nilai-nilai buruk tampaknya meminjamkan dirinya sendiri ke kode rapuh dengan banyak ketergantungan bass ackwards, dan tidak ada pemisahan keprihatinan yang bersih. Saya benar-benar tidak berpikir saya suka kode yang akan dihasilkan dari pemikiran di balik pendekatan itu.
Craig

@Craig: lihat dengan cara ini, jika Anda telah mengisolasi komponen untuk pengujian dengan mengejek dependensinya, lalu mengapa Anda tidak menguji bahwa itu hanya memberikan nilai yang benar ke dependensi itu? Dan jika Anda tidak dapat mengisolasi komponen, apakah Anda benar-benar telah memisahkan masalah? Saya tidak setuju dengan pengkodean defensif, tetapi jika pemeriksaan defensif adalah cara yang Anda gunakan untuk menguji kebenaran kode panggilan maka itu berantakan. Jadi saya pikir kolega si penanya itu benar bahwa cek itu berlebihan, tetapi salah melihat ini sebagai alasan untuk tidak menuliskannya :-)
Steve Jessop

satu-satunya lubang mencolok yang saya lihat adalah bahwa saya masih hanya menguji bahwa komponen saya sendiri tidak dapat memberikan nilai yang tidak valid untuk dependensi tersebut, yang saya sepenuhnya setuju harus dilakukan, tetapi berapa banyak keputusan yang diambil oleh berapa banyak manajer bisnis untuk membuat pribadi komponen publik sehingga mitra dapat menyebutnya? Ini sebenarnya mengingatkan saya pada desain database, dan semua hubungan cinta saat ini dengan ORM, sehingga banyak orang (kebanyakan lebih muda) menyatakan bahwa database hanyalah penyimpanan jaringan yang bodoh dan tidak boleh melindungi diri mereka dengan kendala, kunci asing dan prosedur tersimpan.
Craig

Hal lain yang saya lihat adalah bahwa dalam skenario itu, tentu saja, adalah bahwa Anda hanya menguji panggilan untuk mengolok-olok, bukan untuk dependensi yang sebenarnya. Pada akhirnya, kode dalam dependensi tersebut yang dapat atau tidak dapat bekerja dengan tepat dengan nilai yang diteruskan tertentu, bukan kode di pemanggil. Jadi ketergantungan perlu melakukan hal yang benar, dan perlu ada cakupan uji independen yang cukup dari ketergantungan untuk memastikan hal itu terjadi. Ingat, tes yang sedang kita bicarakan ini disebut tes "unit". Setiap ketergantungan adalah satu unit. :)
Craig

1

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.


Judul dan paragraf pertama ini benar karena bukan tes unit yang akan menjalankan kode saat runtime. Apapun kode runtime lainnya dan mengubah kondisi dunia nyata serta input pengguna yang buruk dan upaya peretasan berinteraksi dengan kode tersebut.
Craig

1

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.


Ini terkait dengan kesulitan filosofis umum dalam menangani apa yang mungkin persyaratan longgar. Jika suatu fungsi dibolehkan signifikan tetapi tidak total pemerintahan bebas untuk berperilaku sewenang-wenang ketika diberikan input yang salah bentuk (misalnya jika dekoder gambar dapat memenuhi persyaratan jika dapat dijamin untuk - pada waktu senggangnya - baik menghasilkan kombinasi piksel yang sewenang-wenang atau mengakhiri secara tidak normal , tetapi tidak jika itu memungkinkan input yang dibuat dengan jahat untuk mengeksekusi kode arbitrer), mungkin tidak jelas kasus uji apa yang sesuai untuk memastikan bahwa tidak ada input yang menghasilkan perilaku yang tidak dapat diterima.
supercat

1

Tes menentukan kontrak kelas Anda.

Sebagai akibat wajar, tidak adanya tes mendefinisikan kontrak yang mencakup perilaku tidak terdefinisi . Jadi ketika Anda melewati nullke Foo::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 nullke Foo::Frobnicate(Widget widget).

Dan Anda mendokumentasikan keputusan itu dengan memasukkan a

[Test]
void Foo_FrobnicatesANullWidget_ThrowsInvalidArgument() 
{
    Given(Foo foo);
    When(foo.Frobnicate(null));
    Then(Expect_Exception(InvalidArgument));
}

1

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.


0

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.


1
Saya tidak downvote, tapi saya setuju dengan downvotes pada premis yang menambahkan perbedaan halus untuk argumen semacam ini membingungkan air.
Craig

@Craig Saya tertarik dengan umpan balik Anda tentang contoh spesifik yang saya tambahkan.
Blackhawk

Saya suka spesifisitas dari contoh ini. Satu-satunya kekhawatiran saya masih memiliki adalah umum untuk seluruh argumen. Misalnya; bersama datang beberapa pengembang baru di tim dan menulis komponen baru yang menggunakan modul keuangan itu. Orang baru tidak menyadari semua seluk-beluk sistem, apalagi fakta bahwa semua jenis pengetahuan ahli tentang bagaimana sistem seharusnya beroperasi tertanam dalam tes daripada kode yang diuji.
Craig

Jadi orang baru gagal membuat beberapa tes penting, DAN Anda berakhir dengan redundansi dalam tes Anda - tes di berbagai bagian sistem memeriksa kondisi yang sama, dan tumbuh tidak konsisten seiring berjalannya waktu, alih-alih hanya menempatkan pernyataan yang sesuai dan pemeriksaan prekondisi dalam kode di mana tindakan itu.
Craig

1
Sesuatu seperti itu. Kecuali bahwa banyak argumen di sini adalah tentang memiliki tes untuk kode panggilan melakukan semua pemeriksaan. Tetapi jika Anda memiliki tingkat penggemar sama sekali, Anda akhirnya melakukan pemeriksaan yang sama dari banyak tempat yang berbeda, dan itu adalah masalah pemeliharaan. Bagaimana jika rentang input yang valid untuk suatu prosedur berubah, tetapi Anda memiliki pengetahuan domain untuk rentang tersebut yang dibangun ke dalam pengujian yang menggunakan komponen yang berbeda? Saya masih sepenuhnya mendukung pemrograman defensif, dan menggunakan profil untuk menentukan apakah dan kapan Anda memiliki masalah kinerja untuk diatasi.
Craig

0

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:

uint8_t AddTwoBytes(uint8_t a, uint8_t b, uint8_t *sum); 

Sekarang jika Anda hanya melakukannya, *(sum) = a + bitu akan berhasil, tetapi hanya dengan beberapa input. a = 1dan b = 2akan membuat sum = 3; Namun karena ukuran penjumlahan adalah byte, a = 100dan b = 200akan membuat sum = 44karena 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.


Itu terlihat seperti contoh pertanyaan wawancara yang bagus (mengapa ia memiliki nilai balik dan parameter "keluar" - dan apa yang terjadi ketika sumpointer nol?).
Toby Speight
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.