Saya refactoring kelas kode warisan besar. Refactoring (saya kira) menganjurkan ini:
- tulis tes untuk kelas warisan
- 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"
sumber
Jawaban:
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.
sumber
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?"
sumber
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.
sumber
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.
sumber
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:
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 kebar()
function flee()
juga membuat panggilan ke fungsibar()
Hanya untuk variasi,
flam()
lakukan panggilan kefoo()
Semuanya bekerja dengan baik (tampaknya, setidaknya).
Kamu refactor ...
bar()
diganti namanya menjadibarista()
flee()
diubah menjadi panggilanbarista()
foo()
adalah tidak berubah untuk panggilanbarista()
Jelas, tes Anda untuk keduanya
foo()
danflam()
sekarang gagal.Mungkin Anda tidak sadar
foo()
dipanggilbar()
sejak awal. Anda tentu tidak menyadari bahwaflam()
itu tergantung padabar()
carafoo()
.Masa bodo. Intinya adalah bahwa tes Anda akan mengungkap perilaku keduanya yang baru rusak
foo()
danflam()
, 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()
istirahatfoo()
, makafoo()
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.
sumber