Bagaimana Anda secara efisien menjaga tes Anda bekerja saat Anda mendesain ulang?

14

Basis kode yang teruji dengan baik memiliki sejumlah manfaat, tetapi pengujian aspek-aspek tertentu dari sistem menghasilkan basis kode yang tahan terhadap beberapa jenis perubahan.

Contoh sedang menguji untuk output spesifik - misalnya, teks atau HTML. Tes sering (secara naif?) Ditulis untuk mengharapkan blok teks tertentu sebagai output untuk beberapa parameter input, atau untuk mencari bagian-bagian tertentu dalam sebuah blok.

Mengubah perilaku kode, untuk memenuhi persyaratan baru atau karena pengujian kegunaan telah mengakibatkan perubahan pada antarmuka, memerlukan perubahan tes juga - mungkin bahkan tes yang tidak secara khusus tes unit untuk kode yang sedang diubah.

  • Bagaimana Anda mengelola pekerjaan menemukan dan menulis ulang tes ini? Bagaimana jika Anda tidak bisa begitu saja "menjalankan semuanya dan membiarkan kerangka kerja menyelesaikannya"?

  • Apa jenis lain dari kode-dalam-pengujian menghasilkan tes rapuh biasa?

Alex Feinman
sumber
Bagaimana ini berbeda secara signifikan dari programmers.stackexchange.com/questions/5898/… ?
AShelly
4
Pertanyaan itu secara keliru ditanyakan tentang tes unit refactoring harus menjadi invarian di bawah refactoring.
Alex Feinman

Jawaban:

9

Saya tahu orang-orang TDD akan membenci jawaban ini, tetapi sebagian besar dari itu bagi saya adalah memilih dengan hati-hati di mana menguji sesuatu.

Jika saya menjadi terlalu gila dengan unit test di tingkat bawah maka tidak ada perubahan berarti yang dapat dilakukan tanpa mengubah tes unit. Jika antarmuka tidak pernah terbuka dan tidak dimaksudkan untuk digunakan kembali di luar aplikasi maka ini hanya overhead yang tidak perlu untuk apa yang mungkin merupakan perubahan cepat.

Sebaliknya jika apa yang ingin Anda ubah terkena atau digunakan kembali setiap tes yang harus Anda ubah adalah bukti dari sesuatu yang mungkin Anda langgar di tempat lain.

Dalam beberapa proyek, ini mungkin sama dengan merancang tes Anda dari tingkat penerimaan daripada dari unit pengujian. dan memiliki lebih sedikit tes unit dan lebih banyak tes gaya integrasi.

Itu tidak berarti bahwa Anda masih tidak dapat mengidentifikasi satu fitur dan kode sampai fitur itu memenuhi kriteria penerimaannya. Ini berarti bahwa dalam beberapa kasus Anda tidak akhirnya mengukur kriteria penerimaan dengan tes unit.

Tagihan
sumber
Saya pikir Anda bermaksud menulis "di luar modul", bukan "di luar aplikasi".
SamB
SamB, itu tergantung. Jika antarmuka adalah internal ke beberapa tempat dengan satu aplikasi, tetapi tidak untuk umum saya akan mempertimbangkan pengujian pada tingkat yang lebih tinggi jika saya pikir antarmuka tersebut cenderung tidak stabil.
Bill
Saya menemukan pendekatan ini sangat kompatibel dengan TDD. Saya suka memulai di lapisan atas aplikasi lebih dekat ke pengguna akhir sehingga saya dapat merancang lapisan bawah mengetahui bagaimana lapisan atas perlu menggunakan lapisan bawah. Pada dasarnya membangun top-down memungkinkan Anda untuk merancang antarmuka antara satu lapisan dengan lapisan lainnya secara lebih akurat.
Greg Burghardt
4

Saya baru saja menyelesaikan perombakan besar pada tumpukan SIP saya, menulis ulang seluruh transport TCP. (Ini adalah refactor dekat, pada skala yang agak besar, relatif terhadap kebanyakan refactoring.)

Singkatnya, ada TIdSipTcpTransport, subkelas TIdSipTransport. Semua TIdSipTransports berbagi rangkaian uji umum. Internal ke TIdSipTcpTransport adalah sejumlah kelas - peta yang berisi pasangan koneksi / inisiat-pesan, klien TCP berulir, server TCP berulir, dan sebagainya.

Inilah yang saya lakukan:

  • Menghapus kelas yang akan saya ganti.
  • Suite tes dihapus untuk kelas-kelas itu.
  • Meninggalkan test suite khusus untuk TIdSipTcpTransport (dan masih ada test suite yang umum untuk semua TIdSipTransport).
  • Jalankan tes TIdSipTransport / TIdSipTcpTransport, untuk memastikan semuanya gagal.
  • Mengomentari semua kecuali satu tes TIdSipTransport / TIdSipTcpTransport.
  • Jika saya perlu menambahkan kelas, saya akan menambahkannya menulis tes untuk membangun fungsionalitas yang cukup yang lulus uji uncommented tunggal.
  • Busa, bilas, ulangi.

Karena itu saya tahu apa yang masih perlu saya lakukan, dalam bentuk tes komentar (*), dan tahu bahwa kode baru berfungsi seperti yang diharapkan, berkat tes baru yang saya tulis.

(*) Sungguh, Anda tidak perlu berkomentar. Hanya saja jangan jalankan mereka; 100 tes gagal tidak terlalu menggembirakan. Juga, dalam pengaturan khusus saya mengkompilasi tes lebih sedikit berarti loop tes-tulis-refactor lebih cepat.

Frank Shearar
sumber
Saya sudah melakukan ini terlalu beberapa bulan yang lalu dan itu bekerja dengan baik untuk saya. Namun saya tidak bisa benar-benar menerapkan metode ini ketika berpasangan dengan seorang rekan dalam pendesainan ulang modul model domain kami (yang pada gilirannya memicu pendesainan ulang semua modul lain dalam proyek).
Marco Ciambrone
3

Ketika tes rapuh, saya menemukannya biasanya karena saya menguji hal yang salah. Ambil contoh, output HTML. Jika Anda memeriksa hasil HTML yang sebenarnya, pengujian Anda akan rapuh. Tetapi Anda tidak tertarik dengan output aktual, Anda tertarik apakah itu menyampaikan informasi yang seharusnya. Sayangnya, melakukan hal itu memerlukan membuat pernyataan tentang isi otak pengguna sehingga tidak dapat dilakukan secara otomatis.

Kamu bisa:

  • Buat HTML sebagai tes asap untuk memastikan itu benar-benar berjalan
  • Gunakan sistem template, sehingga Anda dapat menguji prosesor template dan data yang dikirim ke template, tanpa benar-benar menguji template itu sendiri.

Hal yang sama terjadi dengan SQL. Jika Anda menegaskan SQL yang sebenarnya yang berusaha dibuat oleh kelas Anda, Anda akan berada dalam masalah. Anda benar-benar ingin menegaskan hasilnya. Oleh karena itu saya menggunakan database memori SQLITE selama pengujian unit saya untuk memastikan bahwa SQL saya benar-benar melakukan apa yang seharusnya.

Winston Ewert
sumber
Mungkin juga membantu menggunakan HTML struktural.
SamB
@ Sam tentu saja itu akan membantu, tapi saya tidak berpikir itu akan menyelesaikan masalah sepenuhnya
Winston Ewert
tentu saja tidak, tidak ada yang bisa :-)
SamB
-1

Pertama buat API BARU, yang melakukan apa yang Anda inginkan perilaku API BARU Anda. Jika kebetulan bahwa API baru ini memiliki nama yang sama dengan API TUA, maka saya menambahkan nama _NEW ke nama API baru.

int DoSomethingInterestingAPI ();

menjadi:

int DoSomethingInterestingAPI_NEW (int take_more_arguments); int DoSomethingInterestingAPI_OLD (); int DoSomethingInterestingAPI () {DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API); OK - pada tahap ini - semua tes regresi Anda masih lulus - menggunakan nama DoSomethingInterestingAPI ().

NEXT, buka kode Anda dan ubah semua panggilan ke DoSomethingInterestingAPI () ke varian yang sesuai dari DoSomethingInterestingAPI_NEW (). Ini termasuk memperbarui / menulis ulang bagian apa pun dari tes regresi Anda yang perlu diubah untuk menggunakan API baru.

NEXT, tandai DoSomethingInterestingAPI_OLD () sebagai [[usang ()]]. Tetap gunakan API yang sudah tidak digunakan lagi selama Anda mau (sampai Anda telah memperbarui semua kode yang mungkin bergantung dengan aman).

Dengan pendekatan ini, setiap kegagalan dalam tes regresi Anda hanyalah bug dalam tes regresi itu atau mengidentifikasi bug dalam kode Anda - persis seperti yang Anda inginkan. Proses bertahap untuk merevisi API dengan secara eksplisit membuat versi _NEW dan _OLD dari API ini memungkinkan Anda untuk memiliki sedikit kode baru dan lama yang hidup berdampingan untuk sementara waktu.

Berikut adalah contoh yang baik (sulit) dari pendekatan ini dalam praktiknya. Saya memiliki fungsi BitSubstring () - di mana saya telah menggunakan pendekatan memiliki parameter ketiga menjadi COUNT bit dalam substring. Agar konsisten dengan API dan pola lain dalam C ++, saya ingin beralih untuk memulai / mengakhiri sebagai argumen untuk fungsi.

https://github.com/SophistSolutions/Stroika/commit/003dd8707405c43e735ca71116c773b108c217c0

Saya membuat fungsi BitSubstring_NEW dengan API baru, dan memperbarui semua kode saya untuk menggunakannya (meninggalkan NO LEBIH BANYAK PANGGILAN ke BitSubString). Tetapi saya meninggalkan implementasi untuk beberapa rilis (bulan) - dan menandainya sudah usang - sehingga semua orang dapat beralih ke BitSubString_NEW (dan pada saat itu mengubah argumen dari hitungan menjadi gaya awal / akhir).

LALU - ketika transisi itu selesai, saya melakukan komit lain menghapus BitSubString () dan mengganti nama BitSubString_NEW-> BitSubString () (dan mencabut nama BitSubString_NEW).

Lewis Pringle
sumber
Jangan pernah menambahkan sufiks yang tidak memiliki arti, atau mencela diri sendiri atas nama. Selalu berusaha memberikan nama yang bermakna.
Basilevs
Anda benar-benar melewatkan intinya. Pertama - ini bukan sufiks yang "tidak memiliki arti". Mereka membawa arti bahwa API transisi dari yang lebih lama ke yang lebih baru. Faktanya, itulah inti dari PERTANYAAN yang saya jawab, dan seluruh inti dari jawabannya. Nama-nama dengan JELASNYA berkomunikasi yang merupakan API TUA, yang merupakan API BARU, dan yang merupakan nama target akhirnya dari API setelah transisi selesai. DAN - sufiks _OLD / _NEW bersifat sementara - HANYA selama transisi perubahan API.
Lewis Pringle
Semoga berhasil dengan API versi NEW_NEW_3 tiga tahun kemudian.
Basilev