Apakah pengujian unit mengarah ke generalisasi prematur (khususnya dalam konteks C ++)?

20

Catatan awal

Saya tidak akan membahas perbedaan jenis tes yang ada, sudah ada beberapa pertanyaan di situs ini mengenai hal itu.

Saya akan mengambil apa yang ada di sana dan yang mengatakan: pengujian unit dalam arti "menguji unit isolasi terkecil dari aplikasi" dari mana pertanyaan ini sebenarnya berasal

Masalah isolasi

Apa unit terkecil yang dapat dipisahkan dari suatu program. Yah, seperti yang saya lihat, itu (sangat?) Tergantung pada bahasa apa yang Anda coding.

Micheal Feathers berbicara tentang konsep jahitan : [WEwLC, p31]

Jahitan adalah tempat di mana Anda dapat mengubah perilaku di program Anda tanpa mengedit di tempat itu.

Dan tanpa masuk ke detailnya, saya memahami sebuah jahitan - dalam konteks pengujian unit - untuk menjadi tempat dalam program di mana "tes" Anda dapat berinteraksi dengan "unit" Anda.

Contohnya

Tes unit - terutama di C ++ - memerlukan dari kode yang diuji untuk menambahkan lebih banyak jahitan yang akan secara ketat dipanggil untuk masalah yang diberikan.

Contoh:

  • Menambahkan antarmuka virtual di mana implementasi non-virtual sudah cukup
  • Memisahkan - menyamaratakan (?) - kelas (bertubuh kecil) lebih lanjut "hanya" untuk memfasilitasi menambahkan tes.
  • Memisahkan proyek yang dapat dieksekusi tunggal menjadi lib yang tampaknya "independen", "hanya" untuk memudahkan penyusunannya secara independen untuk pengujian.

Pertanyaan

Saya akan mencoba beberapa versi yang semoga bertanya tentang hal yang sama:

  • Apakah cara Unit Tests mengharuskan seseorang untuk menyusun kode aplikasi "hanya" bermanfaat untuk pengujian unit atau itu sebenarnya bermanfaat bagi struktur aplikasi.
  • Apakah generalisasi kode yang diperlukan untuk membuatnya unit-testable berguna untuk apa pun selain unit test?
  • Apakah menambahkan tes unit memaksa seseorang untuk melakukan generalisasi yang tidak perlu?
  • Apakah tes bentuk unit berlaku pada kode "selalu" juga bentuk yang baik untuk kode secara umum seperti yang terlihat dari domain masalah?

Saya ingat aturan praktis yang mengatakan jangan menyamaratakan sampai Anda perlu / sampai ada tempat kedua yang menggunakan kode. Dengan Tes Unit, selalu ada tempat kedua yang menggunakan kode - yaitu tes unit. Jadi, apakah alasan ini cukup untuk digeneralisasi?

Martin Ba
sumber
8
Sebuah meme yang umum adalah bahwa pola apa pun dapat digunakan secara berlebihan untuk menjadi anti-pola. Hal yang sama berlaku dengan TDD. Seseorang dapat menambahkan antarmuka yang dapat diuji melewati titik pengembalian yang semakin berkurang, di mana kode yang diuji kurang dari antarmuka uji umum yang ditambahkan, serta ke area biaya-manfaat yang terlalu rendah. Sebuah game kasual dengan antarmuka tambahan untuk pengujian seperti OS misi ruang angkasa yang dalam dapat benar-benar kehilangan jendela pasarnya. Pastikan bahwa pengujian tambahan dilakukan sebelum titik perubahan tersebut.
hotpaw2
@ hotpaw2 Penghujatan! :)
maple_shaft

Jawaban:

23

Tes unit - terutama di C ++ - memerlukan dari kode yang diuji untuk menambahkan lebih banyak jahitan yang akan secara ketat dipanggil untuk masalah yang diberikan.

Hanya jika Anda tidak menganggap pengujian sebagai bagian integral dari pemecahan masalah. Untuk masalah nontrivial, seharusnya, tidak hanya di dunia perangkat lunak.

Di dunia perangkat keras, ini telah dipelajari sejak lama - dengan cara yang sulit. Para pembuat berbagai peralatan telah belajar selama berabad-abad dari jembatan berjatuhan yang tak terhitung jumlahnya, meledaknya mobil, merokok CPU, dll. Apa yang kita pelajari sekarang di dunia perangkat lunak. Semua dari mereka membangun "jahitan ekstra" ke dalam produk mereka untuk membuatnya dapat diuji. Sebagian besar mobil baru saat ini memiliki port diagnostik untuk tukang untuk mendapatkan data tentang apa yang terjadi di dalam mesin. Sebagian besar transistor pada setiap CPU memiliki tujuan diagnostik. Dalam dunia perangkat keras, setiap bit dari biaya barang "ekstra", dan ketika sebuah produk diproduksi oleh jutaan, biaya ini pasti menambah jumlah uang yang besar. Tetap saja, pabrikannya rela menghabiskan seluruh uangnya untuk uji coba.

Kembali ke dunia perangkat lunak, C ++ memang lebih sulit untuk diuji unit daripada bahasa yang lebih belakangan menampilkan pemuatan kelas dinamis, refleksi dll. Namun, sebagian besar masalah setidaknya dapat dikurangi. Dalam satu proyek C ++ di mana saya menggunakan unit test sejauh ini, kami tidak menjalankan tes sesering yang akan kami lakukan misalnya dalam proyek Java - tetapi masih merupakan bagian dari pembangunan CI kami, dan kami menemukan mereka berguna.

Apakah cara Unit Tests mengharuskan seseorang untuk menyusun kode aplikasi "hanya" bermanfaat untuk pengujian unit atau apakah sebenarnya bermanfaat bagi struktur aplikasi?

Dalam pengalaman saya, desain yang dapat diuji bermanfaat secara keseluruhan, bukan "hanya" untuk pengujian unit itu sendiri. Manfaat-manfaat ini datang pada tingkat yang berbeda:

  • Menjadikan desain Anda dapat diuji memaksa Anda untuk memotong aplikasi Anda menjadi bagian-bagian kecil, kurang lebih independen yang hanya dapat saling mempengaruhi dalam cara yang terbatas dan terdefinisi dengan baik - ini sangat penting untuk stabilitas jangka panjang dan pemeliharaan program Anda. Tanpa ini, kode cenderung memburuk menjadi kode spageti di mana setiap perubahan yang dibuat di bagian mana pun dari basis kode dapat menyebabkan efek yang tak terduga di bagian program yang tampaknya tidak terkait dan berbeda. Yang, tentu saja, adalah mimpi buruk setiap programmer.
  • Menulis tes sendiri dengan gaya TDD sebenarnya melatih API, kelas, dan metode Anda, dan berfungsi sebagai tes yang sangat efektif untuk mendeteksi apakah desain Anda masuk akal - jika tes menulis melawan dan antarmuka terasa canggung atau sulit, Anda mendapatkan umpan balik awal yang berharga ketika itu masih mudah untuk membentuk API. Dengan kata lain, ini melindungi Anda dari menerbitkan API Anda sebelum waktunya.
  • Pola pengembangan yang diberlakukan oleh TDD membantu Anda fokus pada tugas nyata yang harus dilakukan, dan membuat Anda tetap pada target, meminimalkan kemungkinan Anda berkeliaran menyelesaikan masalah lain dari yang seharusnya, menambahkan fitur dan kompleksitas tambahan yang tidak perlu. , dll.
  • Umpan balik cepat dari unit test memungkinkan Anda untuk berani dalam refactoring kode, memungkinkan Anda untuk terus-menerus beradaptasi dan mengembangkan desain selama masa pakai kode, sehingga secara efektif mencegah entropi kode.

Saya ingat aturan praktis yang mengatakan jangan menyamaratakan sampai Anda perlu / sampai ada tempat kedua yang menggunakan kode. Dengan Tes Unit, selalu ada tempat kedua yang menggunakan kode - yaitu tes unit. Jadi, apakah alasan ini cukup untuk digeneralisasi?

Jika Anda dapat membuktikan bahwa perangkat lunak Anda melakukan apa yang seharusnya dilakukan - dan membuktikannya dengan cara yang cepat, berulang, murah, dan deterministik untuk memuaskan pelanggan Anda - tanpa generalisasi "ekstra" atau lapisan yang dipaksakan oleh unit test, lakukan saja (dan beri tahu kami bagaimana Anda melakukannya, karena saya yakin banyak orang di forum ini akan sama tertariknya dengan saya :-)

Btw saya berasumsi dengan "generalisasi" maksud Anda hal-hal seperti memperkenalkan antarmuka (kelas abstrak) dan polimorfisme alih-alih kelas beton tunggal - jika tidak, mohon klarifikasi.

Péter Török
sumber
Pak, saya salut kepada Anda.
GordonM
Catatan singkat, tetapi bertele-tele: "port diagnostik" sebagian besar ada karena pemerintah telah mengamanatkan mereka sebagai bagian dari skema kontrol emisi. Akibatnya, ia memiliki keterbatasan parah; ada banyak hal yang berpotensi didiagnosis dengan port ini yang tidak (yaitu apa pun yang tidak ada hubungannya dengan pengendalian emisi).
Robert Harvey
4

Saya akan melemparkan The Way of Testivus pada Anda, tetapi untuk meringkas:

Jika Anda menghabiskan banyak waktu dan energi membuat kode Anda lebih rumit untuk menguji satu bagian dari sistem, mungkin struktur Anda salah, atau bahwa pendekatan pengujian Anda salah.

Panduan paling sederhana adalah ini: Apa yang Anda uji adalah antarmuka publik dari kode Anda seperti yang dimaksudkan untuk digunakan oleh bagian lain dari sistem.

Jika pengujian Anda menjadi panjang dan rumit, ini merupakan indikasi bahwa menggunakan antarmuka publik akan menjadi sulit.

Jika Anda harus menggunakan warisan untuk memungkinkan kelas Anda digunakan oleh apa pun selain dari instance tunggal yang saat ini akan digunakan, maka ada kemungkinan besar kelas Anda terlalu terikat dengan lingkungan penggunaannya. Bisakah Anda memberi contoh situasi di mana ini benar?

Namun, waspadalah terhadap dogma unit-testing. Tulis tes yang memungkinkan Anda mendeteksi masalah yang akan menyebabkan klien meneriaki Anda .

deworde
sumber
Saya akan menambahkan yang sama: membuat api, menguji api, dari luar.
Christopher Mahan
2

TDD dan Unit Testing, baik untuk program secara keseluruhan, dan tidak hanya untuk pengujian unit. Alasan untuk ini adalah karena itu baik untuk otak.

Ini adalah presentasi tentang kerangka kerja ActionScript spesifik bernama RobotLegs. Namun jika Anda membalik-balik 10 slide pertama atau lebih, itu mulai sampai ke bagian yang baik tentang otak.

Pengujian TDD dan Unit, memaksa Anda untuk berperilaku dengan cara yang lebih baik bagi otak untuk memproses dan mengingat informasi. Jadi, sementara tugas Anda yang tepat di depan Anda hanya membuat unit test lebih baik, atau membuat kode lebih unit testable ... apa yang sebenarnya dilakukannya adalah membuat kode Anda lebih mudah dibaca, dan dengan demikian membuat kode Anda lebih mudah dikelola. Ini membuat Anda mengkode dalam habbits lebih cepat, dan membuat Anda dapat memahami kode Anda lebih cepat ketika Anda perlu menambah / menghapus fitur, memperbaiki bug, atau secara umum membuka file sumber.

Bob
sumber
1

menguji unit isolasi terkecil dari suatu aplikasi

ini benar, tetapi jika Anda mengambilnya terlalu jauh itu tidak memberi Anda banyak, dan biayanya banyak, dan saya percaya bahwa aspek inilah yang mempromosikan penggunaan istilah BDD untuk menjadi apa yang seharusnya menjadi TDD. sepanjang - unit terkecil yang dapat dipisahkan adalah apa yang Anda inginkan.

Sebagai contoh, saya pernah men-debug kelas jaringan yang memiliki (di antara bit lainnya) 2 metode: 1 untuk mengatur alamat IP, yang lain untuk mengatur nomor port. Tentu saja, ini adalah metode yang sangat sederhana dan akan lulus tes paling sepele dengan mudah, tetapi jika Anda mengatur nomor port dan kemudian mengatur alamat ip, itu akan gagal berfungsi - ip setter menimpa nomor port dengan default. Jadi Anda harus menguji kelas secara keseluruhan untuk memastikan perilaku yang benar, sesuatu yang saya pikir konsep TDD hilang tetapi BDD memberi Anda. Anda tidak benar-benar perlu menguji setiap metode kecil, ketika Anda dapat menguji area yang paling masuk akal dan terkecil dari keseluruhan aplikasi - dalam hal ini kelas jaringan.

Pada akhirnya tidak ada peluru ajaib untuk pengujian, Anda harus membuat keputusan yang masuk akal tentang berapa banyak dan berapa granularitas untuk menerapkan sumber daya pengujian terbatas Anda. Pendekatan berbasis alat yang otomatis - menghasilkan bertopik untuk Anda tidak melakukan ini, itu pendekatan tumpul.

Jadi dengan ini, Anda tidak perlu menyusun kode dengan cara tertentu untuk mencapai TDD, tetapi tingkat pengujian yang Anda lakukan akan bergantung pada struktur kode Anda - jika Anda memiliki GUI monolitik yang semua logikanya terikat erat dengan struktur GUI, maka Anda akan merasa lebih sulit untuk mengisolasi potongan-potongan itu, tetapi Anda masih bisa menulis tes unit di mana 'unit' mengacu pada GUI dan semua pekerjaan DB back-end diejek. Ini adalah contoh ekstrem, tetapi ini menunjukkan Anda masih dapat melakukan pengujian otomatis di atasnya.

Efek samping dari penataan kode Anda agar lebih mudah untuk menguji unit yang lebih kecil memang membantu Anda mendefinisikan aplikasi lebih baik, dan itu memungkinkan Anda untuk mengganti komponen dengan lebih mudah. Ini juga membantu ketika pengkodean karena kemungkinan kecil 2 devs akan bekerja pada komponen yang sama pada waktu tertentu - tidak seperti aplikasi monolitik yang memiliki ketergantungan yang saling terkait yang memecah pekerjaan orang lain datang bersamaan.

gbjbaanb
sumber
0

Anda telah mencapai realisasi yang baik tentang pengorbanan dalam desain bahasa. Beberapa keputusan desain inti dalam C ++ (mekanisme fungsi virtual dicampur dengan mekanisme panggilan fungsi statis) membuat TDD tangguh. Bahasa tidak benar-benar mendukung apa yang Anda butuhkan untuk membuatnya mudah. Sangat mudah untuk menulis C ++ yang hampir tidak mungkin untuk unit test.

Kami lebih beruntung melakukan kode TDD C ++ kami dari fungsi pola pikir quasi-fungsional bukan prosedur (fungsi yang tidak menggunakan argumen dan mengembalikan batal), dan menggunakan komposisi sedapat mungkin. Karena sulit untuk menggantikan kelas anggota ini, kami fokus pada pengujian kelas-kelas tersebut untuk membangun basis tepercaya, dan kemudian mengetahui bahwa unit dasar berfungsi ketika kami menambahkannya ke sesuatu yang lain.

Kuncinya adalah pendekatan kuasi-fungsional. Pikirkan tentang hal ini, jika semua kode C ++ Anda adalah fungsi bebas yang tidak mengakses global, itu akan menjadi uji coba unit :)

segera
sumber