Mengapa menulis tes untuk kode yang akan saya refactor?

15

Saya refactoring kelas kode warisan besar. Refactoring (saya kira) menganjurkan ini:

  1. tulis tes untuk kelas warisan
  2. refactor sih keluar dari kelas

Masalah: setelah saya refactor kelas, tes saya pada langkah 1 perlu diubah. Sebagai contoh, apa yang tadinya dalam metode warisan, sekarang mungkin menjadi kelas yang terpisah sebagai gantinya. Apa yang dulu satu metode mungkin sekarang beberapa metode. Seluruh lanskap kelas legacy dapat dilenyapkan menjadi sesuatu yang baru, sehingga tes yang saya tulis di langkah 1 akan hampir batal dan tidak berlaku. Intinya saya akan menambahkan Langkah 3. menulis ulang tes saya sebanyak-banyaknya

Apa tujuan menulis tes sebelum refactor? Kedengarannya lebih seperti latihan akademis untuk menciptakan lebih banyak pekerjaan untuk saya sendiri. Saya menulis tes untuk metode sekarang dan saya belajar lebih banyak tentang cara menguji sesuatu dan cara kerja metode legacy. Seseorang dapat mempelajari ini dengan hanya membaca kode warisan itu sendiri, tetapi menulis tes hampir seperti menggosok hidung saya di dalamnya, dan juga mendokumentasikan pengetahuan sementara ini dalam tes terpisah. Jadi dengan cara ini saya hampir tidak punya pilihan selain mempelajari apa yang dilakukan kode. Saya katakan sementara di sini, karena saya akan refactor heck keluar dari kode dan semua dokumentasi dan tes saya akan batal demi hukum untuk sebagian yang signifikan, kecuali pengetahuan saya akan tetap dan memungkinkan saya untuk menjadi lebih segar di refactoring.

Apakah itu alasan sebenarnya untuk menulis tes sebelum refactor - untuk membantu saya memahami kode dengan lebih baik? Pasti ada alasan lain!

Tolong jelaskan!

catatan:

Ada posting ini: Apakah masuk akal untuk menulis tes untuk kode warisan ketika tidak ada waktu untuk refactoring lengkap? tetapi dikatakan "menulis tes sebelum refactor", tetapi tidak mengatakan "mengapa", atau apa yang harus dilakukan jika "menulis tes" sepertinya "pekerjaan sibuk yang akan segera dihancurkan"

Dennis
sumber
1
Premis Anda salah. Anda tidak akan mengubah tes Anda. Anda akan menulis tes baru. Langkah 3 adalah "hapus semua tes yang sekarang tidak aktif."
pdr
1
Langkah 3 kemudian dapat membaca "Tulis tes baru. Hapus tes yang tidak berfungsi". Saya pikir itu masih sama dengan menghancurkan karya asli
Dennis
3
Tidak, Anda ingin menulis tes baru selama langkah 2. Dan ya, langkah 1 hancur. Tapi apakah itu buang-buang waktu? Tidak, karena memberi Anda satu ton kepastian bahwa Anda tidak melanggar apa pun selama langkah 2. Tes baru Anda tidak.
pdr
3
@Dennis - walaupun saya memiliki banyak kekhawatiran yang sama dengan Anda mengenai situasi ini, kami dapat menganggap sebagian besar upaya refactoring sebagai "menghancurkan pekerjaan asli" tetapi jika kami tidak pernah menghancurkannya, kami tidak akan pernah pindah dari kode spaghetti dengan 10k baris dalam satu mengajukan. Mungkin harus sama untuk tes unit, mereka berjalan seiring dengan kode yang mereka uji. Ketika kode berevolusi dan segala sesuatunya dipindahkan dan / atau dihapus, begitu pula tes unit berkembang dengannya.
DXM
"Memahami kode" bukan keuntungan kecil. Bagaimana Anda berharap untuk memperbaiki suatu program yang tidak Anda mengerti? Ini tidak bisa dihindari, dan cara apa yang lebih baik untuk menunjukkan pemahaman yang benar tentang suatu program daripada menulis tes menyeluruh. Juga harus dikatakan bahwa semakin abstrak pengujian, semakin kecil kemungkinan Anda harus menggaruknya nanti, jadi jika ada, tetap gunakan pengujian tingkat tinggi pada awalnya.
Neil

Jawaban:

46

Refactoring adalah membersihkan sepotong kode (misalnya meningkatkan gaya, desain, atau algoritma), tanpa mengubah perilaku (terlihat secara eksternal). Anda menulis tes untuk tidak memastikan bahwa kode sebelum dan sesudah refactoring adalah sama, alih-alih Anda menulis tes sebagai indikator bahwa aplikasi Anda sebelum dan sesudah refactoring berperilaku sama: Kode baru ini kompatibel, dan tidak ada bug baru yang diperkenalkan.

Perhatian utama Anda adalah menulis unit test untuk antarmuka publik dari perangkat lunak Anda. Antarmuka ini tidak boleh berubah, jadi tes (yang merupakan pemeriksaan otomatis untuk antarmuka ini) juga tidak boleh berubah.

Namun, tes juga berguna untuk menemukan kesalahan, sehingga masuk akal untuk menulis tes untuk bagian pribadi perangkat lunak Anda juga. Tes-tes ini diharapkan berubah sepanjang refactoring. Jika Anda ingin mengubah detail implementasi (seperti penamaan fungsi privat), Anda pertama-tama memperbarui tes untuk mencerminkan harapan yang berubah, kemudian pastikan bahwa tes gagal (harapan Anda tidak terpenuhi), maka Anda mengubah kode aktual dan periksa apakah semua tes lulus lagi. Pada titik mana tes untuk antarmuka publik mulai gagal.

Ini lebih sulit ketika melakukan perubahan pada skala yang lebih besar, misalnya mendesain ulang banyak bagian codependent. Tetapi akan ada semacam batasan, dan pada batas itu Anda akan dapat menulis tes.

amon
sumber
6
+1. Baca pikiranku, tulis jawabanku. Poin penting: Anda mungkin perlu menulis unit test untuk menunjukkan bahwa bug yang sama masih ada setelah refactoring!
david.pfx
Pertanyaan: mengapa pada contoh perubahan nama fungsi Anda, apakah Anda mengubah tes terlebih dahulu untuk memastikan gagal? Saya ingin mengatakan bahwa tentu saja itu akan gagal ketika Anda mengubahnya - Anda memutuskan koneksi yang digunakan linker untuk mengikat kode! Apakah Anda mungkin berharap bahwa mungkin ada fungsi pribadi lain yang ada dengan nama yang baru saja Anda pilih dan Anda harus memverifikasi itu tidak terjadi jika Anda melewatkannya? Saya melihat bahwa ini akan memberi Anda jaminan tertentu yang berbatasan dengan OCD, tetapi dalam kasus ini rasanya seperti berlebihan. Adakah kemungkinan alasan pengujian dalam contoh Anda tidak akan gagal?
Dennis
^ cont: sebagai teknik umum saya melihat bahwa adalah baik untuk melakukan pemeriksaan kewarasan langkah-demi-langkah dari kode Anda untuk mengetahui ada yang salah sedini mungkin. Jenis seperti Anda mungkin tidak sakit jika Anda tidak mencuci tangan setiap saat, tetapi hanya mencuci tangan sebagai kebiasaan akan membuat Anda sehat secara keseluruhan, apakah Anda bersentuhan dengan hal-hal yang terkontaminasi atau tidak. Di sini Anda dapat mencuci tangan sesekali, atau menguji kode secara berlebihan, tetapi ini membantu menjaga Anda dan kode Anda tetap sehat. Apakah itu maksud Anda?
Dennis
@Dennis sebenarnya, saya secara tidak sadar menggambarkan percobaan yang benar secara ilmiah: Kami tidak dapat menentukan parameter mana yang benar-benar mempengaruhi hasil ketika mengubah lebih banyak param. Ingatlah bahwa tes adalah kode, dan setiap kode mengandung bug. Apakah Anda akan pergi ke neraka programmer untuk tidak menjalankan tes sebelum menyentuh kode? Tentunya tidak: saat menjalankan tes akan ideal, itu penilaian profesional Anda apakah itu perlu. Perhatikan lebih lanjut bahwa pengujian gagal jika tidak dikompilasi, dan bahwa jawaban saya juga berlaku untuk bahasa dinamis, tidak hanya untuk bahasa statis dengan tautan.
amon
2
Dari memperbaiki berbagai kesalahan saat refactoring saya menyadari bahwa saya tidak akan melakukan pemindahan kode semudah tanpa melakukan tes. Tes mengingatkan saya tentang "perbedaan" perilaku / fungsional yang saya perkenalkan dengan mengubah kode saya.
Dennis
7

Ah, menjaga sistem warisan.

Idealnya tes Anda memperlakukan kelas hanya melalui antarmuka dengan sisa basis kode, sistem lain, dan / atau antarmuka pengguna. Antarmuka. Anda tidak dapat memperbarui antarmuka tanpa memengaruhi komponen hulu atau hilir tersebut. Jika itu semua salah satu kekacauan yang sangat erat maka Anda mungkin juga mempertimbangkan upaya menulis ulang daripada refactoring, tetapi sebagian besar semantik.

Sunting: Katakanlah bagian dari kode Anda mengukur sesuatu dan memiliki fungsi yang hanya mengembalikan nilai. Satu-satunya antarmuka memanggil fungsi / metode / yang lainnya dan menerima nilai yang dikembalikan. Ini adalah kopling longgar dan mudah untuk unit test. Jika program utama Anda memiliki sub-komponen yang mengelola buffer, dan semua panggilan ke sana tergantung pada buffer itu sendiri, beberapa variabel kontrol, dan menendang kembali pesan kesalahan melalui bagian kode yang lain, maka Anda dapat mengatakan bahwa itu digabungkan secara ketat dan itu sulit untuk unit test. Anda masih bisa melakukannya dengan jumlah objek tiruan yang cukup dan yang lainnya, tetapi akan berantakan. Terutama di c. Setiap jumlah refactoring cara kerja buffer akan merusak sub-komponen.
Akhiri Edit

Jika Anda menguji kelas Anda melalui antarmuka yang tetap stabil maka tes Anda harus valid sebelum dan sesudah refactoring. Ini memungkinkan Anda membuat perubahan dengan keyakinan bahwa Anda tidak melanggarnya. Setidaknya, lebih percaya diri.

Ini juga memungkinkan Anda membuat perubahan tambahan. Jika ini adalah proyek besar, saya tidak berpikir Anda ingin merobohkan semuanya, membangun sistem baru dan kemudian mulai mengembangkan tes. Anda dapat mengubah satu bagian dari itu, mengujinya, dan memastikan bahwa perubahan itu tidak menurunkan sisa sistem. Atau jika itu terjadi, Anda setidaknya bisa melihat kekacauan kusut raksasa berkembang daripada terkejut ketika Anda melepaskannya.

Meskipun Anda mungkin membagi metode menjadi tiga, mereka masih akan melakukan hal yang sama seperti metode sebelumnya, sehingga Anda dapat mengikuti tes untuk metode lama, dan membaginya menjadi tiga. Upaya menulis tes pertama tidak sia-sia.

Juga, memperlakukan pengetahuan tentang sistem warisan sebagai "pengetahuan sementara" tidak akan berjalan dengan baik. Mengetahui bagaimana hal itu sebelumnya dilakukan itu sangat penting ketika datang ke sistem warisan. Sangat berguna untuk pertanyaan kuno "mengapa dia melakukan itu?"

Philip
sumber
Saya pikir saya mengerti, tetapi Anda kehilangan saya di antarmuka. yaitu tes yang saya tulis sekarang periksa apakah variabel-variabel tertentu telah diisi dengan benar, setelah memanggil metode-di-tes. Jika variabel-variabel itu diubah atau dire-reforasi, maka akan menjadi tes saya. Kelas warisan yang ada yang saya kerjakan tidak memiliki antarmuka / getter / setter per hari, yang akan membuat perubahan variabel atau kurang kerja intensif. Tapi sekali lagi saya tidak yakin apa yang Anda maksud dengan antarmuka ketika datang ke kode warisan. Mungkin saya bisa membuatnya? Tapi itu akan menjadi refactoring.
Dennis
1
Ya, jika Anda memiliki satu kelas dewa yang melakukan segalanya maka sebenarnya tidak ada antarmuka. Tetapi jika ia memanggil kelas lain, kelas atas mengharapkannya berperilaku dengan cara tertentu, dan unit test dapat memverifikasi itu melakukannya. Namun, saya tidak akan berpura-pura Anda tidak harus memperbarui pengujian unit Anda saat refactoring.
Philip
4

Jawaban / realisasi saya sendiri:

Dari memperbaiki berbagai kesalahan saat refactoring saya menyadari bahwa saya tidak akan melakukan pemindahan kode semudah tanpa melakukan tes. Tes mengingatkan saya tentang "perbedaan" perilaku / fungsional yang saya perkenalkan dengan mengubah kode saya.

Anda tidak perlu terlalu sadar ketika ada tes yang bagus di tempat. Anda dapat mengedit kode Anda dalam sikap yang lebih santai. Tes melakukan verifikasi dan pemeriksaan kewarasan untuk Anda.

Juga, tes saya tetap sama seperti saya refactored dan tidak hancur. Saya benar-benar memperhatikan beberapa peluang tambahan untuk menambahkan pernyataan pada tes saya ketika saya menggali lebih dalam kode.

MEMPERBARUI

Nah, sekarang saya banyak mengubah tes saya: / Karena saya refactored fungsi asli keluar (menghapus fungsi dan menciptakan kelas pembersih baru, memindahkan bulu yang dulu berada di dalam fungsi di luar kelas baru), jadi sekarang kode-tes yang saya jalankan sebelumnya mengambil parameter yang berbeda di bawah nama kelas yang berbeda, dan menghasilkan hasil yang berbeda (kode asli dengan bulu memiliki hasil lebih banyak untuk diuji). Jadi tes saya perlu mencerminkan perubahan ini dan pada dasarnya saya menulis ulang tes saya menjadi sesuatu yang baru.

Saya kira ada solusi lain yang bisa saya lakukan untuk menghindari tes menulis ulang. Yaitu menyimpan nama fungsi lama dengan kode baru dan bulu di dalamnya ... tapi saya tidak tahu apakah itu ide terbaik dan saya belum memiliki banyak pengalaman untuk membuat penilaian menilai apa yang harus dilakukan.

Dennis
sumber
Kedengarannya lebih seperti Anda mendesain ulang aplikasi bersama dengan refactoring.
JeffO
Kapan itu refactoring dan kapan itu mendesain ulang? yaitu ketika refactoring sulit untuk tidak memecah kelas yang lebih besar menjadi lebih kecil, dan juga memindahkan mereka. Jadi ya, saya tidak begitu yakin dengan perbedaannya, tetapi mungkin saya melakukan keduanya.
Dennis
3

Gunakan tes Anda untuk mengarahkan kode Anda saat melakukannya. Dalam kode lawas ini berarti menulis tes untuk kode yang akan Anda ubah. Dengan begitu mereka bukan artefak yang terpisah. Tes harus tentang apa yang perlu dicapai kode dan bukan tentang nyali batin bagaimana melakukannya.

Umumnya Anda ingin menambahkan tes pada kode yang tidak ada) untuk kode yang akan Anda refactor untuk memastikan bahwa perilaku kode terus berfungsi seperti yang diharapkan. Dengan demikian terus menjalankan test suite sementara refactoring adalah jaring pengaman yang fantastis. Memikirkan mengubah kode tanpa test suite untuk mengonfirmasi perubahan tidak memengaruhi sesuatu yang tidak terduga adalah menakutkan.

Adapun seluk beluk memperbarui tes lama, menulis tes baru, menghapus tes lama, dll. Saya hanya melihat itu sebagai bagian dari biaya pengembangan perangkat lunak profesional modern.

Michael Durrant
sumber
Paragraf pertama Anda tampaknya menganjurkan mengabaikan langkah 1 dan menulis tes saat ia berjalan; paragraf kedua Anda tampaknya bertentangan dengan itu.
pdr
Memperbarui jawaban saya.
Michael Durrant
2

Apa tujuan refactoring dalam kasus spesifik Anda?

Anggap untuk tujuan memasang dengan jawaban saya bahwa kita semua percaya (sampai taraf tertentu) dalam TDD (Test-Driven Development).

Jika tujuan refactoring Anda adalah untuk membersihkan kode yang ada tanpa mengubah perilaku yang ada, maka menulis tes sebelum refactoring adalah bagaimana Anda memastikan bahwa Anda tidak mengubah perilaku kode, jika Anda berhasil, maka tes akan berhasil sebelum dan sesudah Anda refactor.

  • Tes akan membantu Anda memastikan bahwa pekerjaan baru Anda benar-benar berfungsi.

  • Tes mungkin juga akan mengungkap kasus di mana karya asli tidak berfungsi.

Tetapi bagaimana Anda benar-benar melakukan refactoring yang signifikan tanpa mempengaruhi perilaku sampai batas tertentu ?

Berikut adalah daftar singkat beberapa hal yang mungkin terjadi selama refactoring:

  • ganti nama variabel
  • ganti nama fungsinya
  • tambahkan fungsi
  • hapus fungsi
  • fungsi split menjadi dua fungsi atau lebih
  • menggabungkan dua atau lebih fungsi menjadi satu fungsi
  • kelas terpisah
  • menggabungkan kelas
  • ganti nama kelas

Saya akan berargumen bahwa setiap aktivitas yang terdaftar itu mengubah perilaku dengan cara tertentu.

Dan saya akan berargumen bahwa jika refactoring Anda mengubah perilaku, tes Anda masih akan menjadi cara Anda memastikan bahwa Anda tidak merusak apa pun.

Mungkin perilaku tidak berubah pada tingkat makro, tetapi titik pengujian unit bukan untuk memastikan perilaku makro. Itu pengujian integrasi . Maksud dari pengujian unit adalah untuk memastikan bahwa setiap bit yang Anda buat dari produk Anda tidak rusak. Rantai, tautan terlemah, dll.

Bagaimana dengan skenario ini:

  • Anggap saja sudah function bar()

  • function foo() melakukan panggilan ke bar()

  • function flee() juga membuat panggilan ke fungsi bar()

  • Hanya untuk variasi, flam()lakukan panggilan kefoo()

  • Semuanya bekerja dengan baik (tampaknya, setidaknya).

  • Kamu refactor ...

  • bar() diganti namanya menjadi barista()

  • flee() diubah menjadi panggilan barista()

  • foo()adalah tidak berubah untuk panggilanbarista()

Jelas, tes Anda untuk keduanya foo()dan flam()sekarang gagal.

Mungkin Anda tidak sadar foo()dipanggil bar()sejak awal. Anda tentu tidak menyadari bahwa flam()itu tergantung pada bar()cara foo().

Masa bodo. Intinya adalah bahwa tes Anda akan mengungkap perilaku keduanya yang baru rusak foo()dan flam(), secara bertahap selama pekerjaan refactoring Anda.

Tes akhirnya membantu Anda melakukan refactor dengan baik.

Kecuali Anda tidak memiliki tes apa pun.

Itu sedikit contoh yang dibuat-buat. Ada orang-orang yang berpendapat bahwa jika mengubah bar()istirahat foo(), maka foo()terlalu rumit untuk memulai dan harus dipecah. Tetapi prosedur dapat memanggil prosedur lain karena suatu alasan dan tidak mungkin untuk menghilangkan semua kompleksitas, bukan? Tugas kita adalah mengelola kompleksitas dengan cukup baik.

Pertimbangkan skenario lain.

Anda sedang membangun sebuah bangunan.

Anda membangun perancah untuk membantu memastikan bahwa bangunan dibangun dengan benar.

Perancah membantu Anda membangun poros lift, di antaranya. Setelah itu, Anda merobohkan perancah, tetapi poros lift tetap ada. Anda telah menghancurkan "karya asli" dengan menghancurkan perancah.

Analogi ini lemah, tetapi intinya adalah bahwa itu tidak pernah terjadi untuk membangun alat untuk membantu Anda membangun produk. Bahkan jika alat tersebut tidak permanen, mereka berguna (bahkan perlu). Tukang kayu membuat jig sepanjang waktu, kadang-kadang hanya untuk satu pekerjaan. Kemudian mereka mencabik-cabik jig, kadang-kadang menggunakan bagian untuk membangun jig lain untuk pekerjaan lain, kadang tidak. Tapi itu tidak membuat jig tidak berguna atau usaha sia-sia.

Craig
sumber