Apakah masuk akal untuk menulis tes untuk kode warisan ketika tidak ada waktu untuk refactoring lengkap?

72

Saya biasanya mencoba mengikuti saran buku Bekerja Efektif dengan Legacy Cod e . Saya memecahkan dependensi, memindahkan bagian kode ke @VisibleForTesting public staticmetode dan ke kelas baru untuk membuat kode (atau setidaknya beberapa bagian darinya) dapat diuji. Dan saya menulis tes untuk memastikan bahwa saya tidak merusak apa pun ketika saya memodifikasi atau menambahkan fungsi baru.

Seorang kolega mengatakan bahwa saya tidak boleh melakukan ini. Alasannya:

  • Kode asli mungkin tidak berfungsi dengan baik sejak awal. Dan menulis tes untuk itu membuat perbaikan dan modifikasi di masa depan lebih sulit karena devs harus memahami dan memodifikasi tes juga.
  • Jika itu kode GUI dengan beberapa logika (~ 12 baris, 2-3 jika / selain itu blok, misalnya), tes tidak sebanding dengan masalahnya karena kode tersebut terlalu sepele untuk memulai.
  • Pola buruk yang serupa bisa ada di bagian lain dari basis kode juga (yang belum saya lihat, saya agak baru); akan lebih mudah untuk membersihkan semuanya dalam satu refactoring besar. Mengekstrak logika bisa merusak kemungkinan di masa depan ini.

Haruskah saya menghindari mengeluarkan bagian yang dapat diuji dan menulis tes jika kita tidak punya waktu untuk menyelesaikan refactoring? Apakah ada kerugian untuk hal ini yang harus saya pertimbangkan?

is4
sumber
29
Sepertinya kolega Anda hanya mengajukan alasan karena dia tidak bekerja seperti itu. Orang terkadang bersikap seperti ini karena terlalu gigih untuk mengubah cara adopsi mereka dalam melakukan sesuatu.
Doc Brown
3
apa yang harus digolongkan sebagai bug dapat diandalkan oleh bagian lain dari kode mengubahnya menjadi fitur
ratchet freak
1
Satu-satunya argumen yang bagus yang dapat saya pikirkan adalah bahwa refactoring Anda sendiri dapat memperkenalkan bug baru jika Anda salah membaca / salah menyalin sesuatu. Untuk alasan itu saya bebas melakukan refactor dan memperbaiki isi hatiku pada versi yang saat ini sedang dikembangkan - tetapi perbaikan pada versi sebelumnya menghadapi rintangan yang jauh lebih tinggi dan mungkin tidak disetujui jika hanya "pembersihan" kosmetik / struktural sejak risiko dianggap melebihi potensi keuntungan. Ketahuilah budaya lokal Anda - bukan hanya satu ide dari sapi - dan punya alasan yang sangat kuat sebelum melakukan hal lain.
keshlam
6
Poin pertama agak lucu - "Jangan mengujinya, mungkin buggy." Ya, ya? Maka ada baiknya untuk mengetahui bahwa - apakah kita ingin memperbaikinya atau kita tidak ingin ada orang yang mengubah perilaku aktual ke apa yang dikatakan oleh beberapa spesifikasi desain. Either way, pengujian (dan menjalankan tes dalam sistem otomatis) bermanfaat.
Christopher Creutzig
3
Terlalu sering "satu refactoring besar" yang akan terjadi dan yang akan menyembuhkan semua penyakit adalah mitos, dibuat oleh mereka yang hanya ingin mendorong hal-hal yang mereka anggap membosankan (tes menulis) ke masa depan yang jauh. Dan jika itu benar-benar menjadi nyata, mereka akan sangat menyesal telah membiarkannya menjadi begitu besar!
Julia Hayward

Jawaban:

100

Inilah kesan pribadi saya yang tidak ilmiah: ketiga alasan itu terdengar seperti ilusi kognitif yang tersebar luas tetapi salah.

  1. Tentu, kode yang ada mungkin salah. Mungkin juga benar. Karena aplikasi secara keseluruhan tampaknya memiliki nilai bagi Anda (jika tidak, Anda hanya akan membuangnya), tanpa adanya informasi yang lebih spesifik, Anda harus menganggap bahwa itu sebagian besar benar. "Tes menulis membuat segalanya lebih sulit karena ada lebih banyak kode yang terlibat secara keseluruhan" adalah sikap yang sederhana, dan sangat salah.
  2. Dengan segala cara, keluarkan upaya refactoring, pengujian, dan peningkatan Anda di tempat-tempat di mana mereka menambahkan nilai paling banyak dengan sedikit usaha. Subrutin GUI pemformatan nilai seringkali bukan prioritas pertama. Tetapi tidak menguji sesuatu karena "sederhana" juga merupakan sikap yang sangat salah. Hampir semua kesalahan parah dilakukan karena orang berpikir mereka memahami sesuatu yang lebih baik daripada yang sebenarnya mereka lakukan.
  3. "Kami akan melakukan semuanya dalam satu gerakan besar di masa depan" adalah pemikiran yang bagus. Biasanya gerakan besar tetap kuat di masa depan, sementara di masa sekarang tidak ada yang terjadi. Saya, saya yakin dengan keyakinan "lambat dan mantap memenangkan perlombaan".
Kilian Foth
sumber
23
+1 untuk "Sebenarnya semua kesalahan parah dilakukan karena orang berpikir mereka memahami sesuatu yang lebih baik daripada yang sebenarnya mereka lakukan."
rem
Poin 1 - dengan BDD , tes mendokumentasikan diri ...
Robbie Dee
2
Seperti yang ditunjukkan oleh @ guillaume31, bagian dari nilai tes menulis menunjukkan bagaimana kode tersebut bekerja - yang mungkin atau mungkin tidak sesuai dengan spesifikasi. Tetapi bisa jadi spek itu "salah": kebutuhan bisnis mungkin telah berubah dan kode mencerminkan persyaratan baru tetapi spek itu tidak. Anggap saja kode itu "salah" terlalu sederhana (lihat poin 1). Dan lagi tes akan memberi tahu Anda apa kode sebenarnya, bukan apa yang dipikirkan / dikatakan orang (lihat poin 2).
David
bahkan jika Anda melakukan satu gerakan, Anda perlu memahami kode tersebut. Tes akan membantu Anda menangkap perilaku tak terduga bahkan jika Anda tidak refactor tetapi menulis ulang (dan jika Anda refactor, mereka membantu memastikan refactoring Anda tidak melanggar perilaku lama - atau hanya di mana Anda ingin istirahat). Jangan ragu untuk bergabung atau tidak - sesuai keinginan.
Frank Hopkins
50

Beberapa pemikiran:

Saat Anda refactoring kode lawas, tidak masalah jika beberapa tes yang Anda tulis kebetulan bertentangan dengan spesifikasi ideal. Yang penting adalah mereka menguji perilaku program saat ini . Refactoring adalah tentang mengambil langkah-langkah iso-fungsional kecil untuk membuat kode lebih bersih; Anda tidak ingin terlibat dalam perbaikan bug saat Anda melakukan refactoring. Selain itu, jika Anda menemukan bug yang mencolok, itu tidak akan hilang. Anda selalu dapat menulis tes regresi untuk itu dan menonaktifkannya sementara, atau menyisipkan tugas perbaikan bug di backlog Anda untuk nanti. Satu hal dalam satu waktu.

Saya setuju bahwa kode GUI murni sulit untuk diuji dan mungkin tidak cocok untuk " Bekerja Secara Efektif ... " - gaya refactoring. Namun, ini tidak berarti Anda tidak harus mengekstrak perilaku yang tidak ada hubungannya di lapisan GUI dan menguji kode yang diekstraksi. Dan "12 baris, 2-3 jika / blok lain" tidak sepele. Semua kode dengan setidaknya sedikit logika kondisional harus diuji.

Dalam pengalaman saya, refactoring besar tidak mudah dan mereka jarang bekerja. Jika Anda tidak menetapkan diri Anda sendiri dengan tepat, tujuan-tujuan kecil, ada risiko tinggi bahwa Anda memulai pengerjaan ulang yang tidak pernah berakhir, menarik rambut di mana Anda tidak akan pernah mendarat di kaki Anda pada akhirnya. Semakin besar perubahan, semakin Anda berisiko melanggar sesuatu dan semakin banyak kesulitan yang Anda miliki untuk mengetahui di mana Anda gagal.

Membuat segalanya menjadi lebih baik secara pro hoc dengan refactor kecil tidak "merongrong kemungkinan masa depan", itu memungkinkan mereka - memperkuat tanah berawa di mana aplikasi Anda berada. Anda pasti harus melakukannya.

guillaume31
sumber
5
+1 untuk "tes yang Anda tulis menguji perilaku program saat ini "
David
17

Juga ulang: "Kode asli mungkin tidak berfungsi dengan baik" - itu tidak berarti Anda hanya mengubah perilaku kode tanpa khawatir tentang dampaknya. Kode lain mungkin mengandalkan perilaku yang tampaknya rusak, atau efek samping dari implementasi saat ini. Cakupan uji coba aplikasi yang ada seharusnya membuatnya lebih mudah untuk refactor nanti, karena itu akan membantu Anda mengetahui ketika Anda secara tidak sengaja memecahkan sesuatu. Anda harus menguji bagian terpenting terlebih dahulu.

Rory Hunter
sumber
Sayangnya benar. Kami memiliki beberapa bug jelas yang bermanifestasi dalam kasus tepi yang tidak dapat kami perbaiki karena klien kami lebih memilih konsistensi daripada kebenaran. (Itu disebabkan karena kode pengumpulan data yang memungkinkan hal-hal yang tidak diperhitungkan kode pelaporan, seperti membiarkan satu bidang kosong dalam serangkaian bidang kosong)
Izkata
14

Jawaban Kilian mencakup aspek yang paling penting, tetapi saya ingin memperluas poin 1 dan 3.

Jika pengembang ingin mengubah kode (refactor, extended, debug), ia harus memahaminya. Dia harus memastikan perubahannya memengaruhi perilaku yang diinginkannya (tidak ada dalam hal refactoring), dan tidak ada yang lain.

Jika ada tes, maka dia harus memahami tes juga, tentu. Pada saat yang sama, tes harus membantunya memahami kode utama, dan tes jauh lebih mudah dipahami daripada kode fungsional (kecuali mereka tes yang buruk). Dan tes membantu menunjukkan apa yang berubah dalam perilaku kode lama. Bahkan jika kode asli salah, dan tes menguji perilaku salah itu, itu masih merupakan keuntungan.

Namun, ini mengharuskan tes didokumentasikan sebagai pengujian perilaku yang sudah ada, bukan spesifikasi.

Beberapa pemikiran pada poin 3 juga: selain fakta bahwa "big swoop" jarang pernah benar-benar terjadi, ada juga hal lain: itu sebenarnya tidak mudah. Agar lebih mudah, beberapa kondisi harus diterapkan:

  • Antipattern yang akan di-refactored perlu mudah ditemukan. Apakah semua lajang Anda bernama XYZSingleton? Apakah pengambil contoh mereka selalu dipanggil getInstance()? Dan bagaimana Anda menemukan hierarki Anda yang terlalu dalam? Bagaimana Anda mencari benda dewa Anda? Ini memerlukan analisis metrik kode dan kemudian memeriksa metrik secara manual. Atau Anda hanya tersandung saat mereka bekerja, seperti yang Anda lakukan.
  • Refactoring harus mekanis. Dalam kebanyakan kasus, bagian sulit dari refactoring adalah memahami kode yang ada dengan cukup baik untuk mengetahui bagaimana mengubahnya. Singletons lagi: jika singleton hilang, bagaimana Anda mendapatkan informasi yang diperlukan untuk penggunanya? Ini sering berarti memahami kaligraf lokal sehingga Anda tahu dari mana mendapatkan informasi. Sekarang apa yang lebih mudah: mencari sepuluh lajang di aplikasi Anda, memahami penggunaan masing-masing (yang mengarah pada kebutuhan untuk memahami 60% dari basis kode), dan merobeknya? Atau mengambil kode yang sudah Anda mengerti (karena Anda sedang mengerjakannya sekarang) dan merobek lajang yang digunakan di sana? Jika refactoring tidak terlalu mekanis sehingga hanya membutuhkan sedikit atau tidak ada pengetahuan tentang kode di sekitarnya, tidak ada gunanya mengumpulkannya.
  • Refactoring perlu diotomatisasi. Ini agak berdasarkan pendapat, tapi begini saja. Sedikit refactoring itu menyenangkan dan memuaskan. Banyak refactoring membosankan dan membosankan. Meninggalkan potongan kode yang baru saja Anda kerjakan dalam keadaan yang lebih baik memberi Anda perasaan hangat dan menyenangkan, sebelum Anda beralih ke hal-hal yang lebih menarik. Mencoba untuk memperbaiki seluruh basis kode akan membuat Anda frustrasi dan marah pada programmer bodoh yang menulisnya. Jika Anda ingin melakukan refactoring besar-besaran, maka perlu sebagian besar otomatis untuk meminimalkan frustrasi. Ini adalah, dengan cara, berbaur dari dua poin pertama: Anda hanya dapat mengotomatiskan refactoring jika Anda dapat mengotomatiskan menemukan kode yang buruk (yaitu mudah ditemukan), dan mengotomatiskan mengubahnya (yaitu mekanis).
  • Perbaikan bertahap membuat kasus bisnis yang lebih baik. Refactoring gerakan besar sangat mengganggu. Jika Anda membuat ulang sepotong kode, Anda selalu bergabung dengan konflik dengan orang lain yang mengerjakannya, karena Anda baru saja membagi metode yang mereka ubah menjadi lima bagian. Ketika Anda refactor sepotong kode berukuran cukup, Anda mendapatkan konflik dengan beberapa orang (1-2 saat membagi megafungsi 600-line, 2-4 saat memecah objek dewa, 5 ketika merobek singleton dari sebuah modul ), tetapi Anda tetap akan memiliki konflik itu karena suntingan utama Anda. Ketika Anda melakukan refactoring lebar basis kode, Anda bertentangan dengan semua orang. Belum lagi itu mengikat beberapa pengembang selama berhari-hari. Peningkatan bertahap menyebabkan setiap modifikasi kode membutuhkan waktu lebih lama. Ini membuatnya lebih dapat diprediksi, dan tidak ada periode waktu yang terlihat ketika tidak ada yang terjadi kecuali pembersihan.
Sebastian Redl
sumber
12

Ada budaya di beberapa perusahaan di mana mereka segan untuk memungkinkan pengembang kapan saja untuk meningkatkan kode yang tidak secara langsung memberikan nilai tambahan misalnya fungsi baru.

Saya mungkin berkhotbah kepada orang-orang yang bertobat di sini, tetapi itu jelas merupakan ekonomi palsu. Kode yang bersih dan ringkas bermanfaat bagi pengembang selanjutnya. Hanya saja pengembaliannya tidak segera terbukti.

Saya pribadi berlangganan Prinsip Pramuka tetapi yang lain (seperti yang sudah Anda lihat) tidak.

Yang mengatakan, perangkat lunak menderita entropi dan membangun utang teknis. Pengembang sebelumnya yang kekurangan waktu (atau mungkin hanya malas atau tidak berpengalaman) mungkin telah menerapkan solusi kereta sub-optimal daripada yang dirancang dengan baik. Meskipun tampaknya diinginkan untuk memperbaiki ini, Anda berisiko memperkenalkan bug baru pada kode kerja (bagi pengguna).

Beberapa perubahan berisiko lebih rendah daripada yang lain. Misalnya, tempat saya bekerja cenderung ada banyak kode duplikat yang dapat dengan aman dipindahkan ke subrutin dengan dampak minimal.

Pada akhirnya, Anda harus membuat penilaian untuk seberapa jauh Anda mengambil refactoring tetapi ada nilai yang tidak dapat disangkal dalam menambahkan tes otomatis jika belum ada.

Robbie Dee
sumber
2
Saya sepenuhnya setuju pada prinsipnya, tetapi di banyak perusahaan itu tergantung pada waktu dan uang. Jika bagian "merapikan" hanya membutuhkan beberapa menit maka itu tidak masalah, tetapi begitu perkiraan untuk merapikan mulai menjadi lebih besar (untuk beberapa definisi besar), Anda, orang yang berkode perlu mendelegasikan keputusan itu kepada atasan Anda atau manajer proyek. Bukan tempat Anda untuk memutuskan nilai waktu yang dihabiskan. Mengerjakan perbaikan bug X, atau fitur baru Y mungkin memiliki nilai yang jauh lebih tinggi untuk proyek / perusahaan / pelanggan.
ozz
2
Anda juga mungkin tidak menyadari masalah yang lebih besar seperti proyek yang dibatalkan dalam waktu 6 bulan, atau hanya bahwa perusahaan lebih menghargai waktu Anda (mis. Anda melakukan sesuatu yang mereka anggap lebih penting, dan orang lain dapat melakukan pekerjaan refeactoring). Pekerjaan refactoring juga dapat mempengaruhi pengujian. Akankah refactoring besar memicu regresi uji penuh? Apakah perusahaan memiliki sumber daya yang dapat digunakan untuk melakukan ini?
ozz
Ya, karena Anda telah menyentuh ada banyak alasan mengapa operasi kode besar mungkin atau mungkin bukan ide yang baik: prioritas pengembangan lainnya, masa pakai perangkat lunak, sumber daya pengujian, pengalaman pengembang, pemasangan, siklus rilis, keakraban dengan kode basis, dokumentasi, kekritisan misi, budaya perusahaan dll. dll. Ini adalah panggilan penilaian
Robbie Dee
4

Dalam pengalaman saya, tes karakterisasi semacam bekerja dengan baik. Ini memberi Anda cakupan tes yang luas tapi tidak terlalu spesifik relatif cepat, tetapi bisa sulit untuk diterapkan untuk aplikasi GUI.

Saya kemudian akan menulis unit test untuk bagian-bagian yang ingin Anda ubah dan melakukannya setiap kali Anda ingin melakukan perubahan sehingga meningkatkan cakupan tes unit Anda dari waktu ke waktu.

Pendekatan ini memberi Anda ide yang baik jika perubahan mempengaruhi bagian lain dari sistem dan mari Anda masuk ke posisi untuk melakukan perubahan yang diperlukan lebih awal.

jamesj
sumber
3

Re: "Kode asli mungkin tidak berfungsi dengan baik":

Tes tidak ditulis dalam batu. Mereka bisa diubah. Dan jika Anda menguji fitur yang salah, seharusnya mudah untuk menulis ulang tes dengan lebih benar. Bagaimanapun, hanya hasil yang diharapkan dari fungsi yang diuji yang harus berubah.

rem
sumber
1
IMO, tes individu harus ditulis dalam batu, setidaknya sampai fitur yang mereka uji sudah mati dan hilang. Mereka adalah yang memverifikasi perilaku sistem yang ada, dan membantu memastikan pengelola bahwa perubahan mereka tidak akan melanggar kode lama yang mungkin sudah bergantung pada perilaku itu. Ubah tes untuk fitur langsung, dan Anda menghapus jaminan itu.
cHao
3

Baiklah. Menjawab sebagai insinyur uji perangkat lunak. Pertama, Anda harus menguji semua yang pernah Anda lakukan. Karena jika tidak, Anda tidak tahu apakah itu berfungsi atau tidak. Ini mungkin tampak jelas bagi kami, tetapi saya memiliki rekan yang melihatnya secara berbeda. Sekalipun proyek Anda adalah proyek kecil yang mungkin tidak pernah dikirimkan, Anda harus menatap wajah pengguna dan mengatakan Anda tahu itu berhasil karena Anda mengujinya.

Kode non-sepele selalu mengandung bug (mengutip seorang pria dari uni; dan jika tidak ada bug di dalamnya, itu sepele) dan tugas kami adalah menemukannya sebelum pelanggan melakukannya. Kode lama memiliki bug lama. Jika kode asli tidak berfungsi sebagaimana mestinya, Anda ingin mengetahuinya, percayalah. Bug tidak masalah jika Anda mengetahuinya, jangan takut untuk menemukannya, itulah gunanya catatan rilis.

Jika saya ingat benar buku Refactoring mengatakan untuk menguji terus-menerus, jadi itu bagian dari proses.

RedSonja
sumber
3

Lakukan cakupan tes otomatis.

Waspadai angan-angan, baik Anda sendiri maupun oleh pelanggan dan bos Anda. Saya sangat ingin percaya bahwa perubahan saya akan benar untuk pertama kalinya dan saya hanya perlu menguji sekali, saya telah belajar untuk memperlakukan pemikiran seperti itu dengan cara yang sama saya memperlakukan email penipuan Nigeria. Yah, kebanyakan; Saya tidak pernah mencari email penipuan tetapi baru-baru ini (ketika diteriaki) saya menyerah karena tidak menggunakan praktik terbaik. Itu adalah pengalaman menyakitkan yang menyeret (mahal) terus menerus. Tidak akan lagi!

Saya memiliki kutipan favorit dari komik web Freefall: "Apakah Anda pernah bekerja di bidang yang kompleks di mana penyelia hanya memiliki gambaran kasar tentang perincian teknis? ... Maka Anda tahu cara paling pasti untuk menyebabkan penyelia Anda gagal adalah dengan ikuti setiap perintahnya tanpa pertanyaan. "

Mungkin tepat untuk membatasi jumlah waktu Anda berinvestasi.

Technophile
sumber
1

Jika Anda berurusan dengan kode legacy dalam jumlah besar yang saat ini tidak diuji, dapatkan cakupan tes sekarang alih-alih menunggu penulisan ulang hipotetis besar di masa depan adalah langkah yang tepat. Mulai dengan menulis tes unit tidak.

Tanpa pengujian otomatis, setelah melakukan perubahan pada kode Anda perlu melakukan beberapa pengujian ujung ke ujung aplikasi secara manual untuk memastikan itu berfungsi. Mulailah dengan menulis tes integrasi tingkat tinggi untuk menggantikannya. Jika aplikasi Anda membaca file, memvalidasinya, memproses data dengan cara tertentu, dan menampilkan hasil tes yang Anda inginkan yang menangkap semua itu.

Idealnya Anda akan memiliki data dari rencana pengujian manual atau bisa mendapatkan sampel data produksi aktual untuk digunakan. Jika tidak, karena aplikasi sedang dalam produksi, dalam banyak kasus itu melakukan apa yang seharusnya, jadi hanya membuat data yang akan mencapai semua poin tinggi dan menganggap output sudah benar untuk saat ini. Ini tidak lebih buruk daripada mengambil fungsi kecil, dengan asumsi itu melakukan apa yang namanya atau komentar menyarankan itu harus dilakukan, dan menulis tes dengan asumsi itu berfungsi dengan benar.

IntegrationTestCase1()
{
    var input = ReadDataFile("path\to\test\data\case1in.ext");
    bool validInput = ValidateData(input);
    Assert.IsTrue(validInput);

    var processedData = ProcessData(input);
    Assert.AreEqual(0, processedData.Errors.Count);

    bool writeError = WriteFile(processedData, "temp\file.ext");
    Assert.IsFalse(writeError);

    bool filesAreEqual = CompareFiles("temp\file.ext", "path\to\test\data\case1out.ext");
    Assert.IsTrue(filesAreEqual);
}

Setelah Anda mendapatkan cukup dari tes tingkat tinggi ini yang ditulis untuk menangkap operasi normal aplikasi dan kasus kesalahan yang paling umum jumlah waktu yang Anda harus habiskan menggedor keyboard untuk mencoba dan menangkap kesalahan dari kode melakukan sesuatu selain dari apa Anda pikir itu seharusnya dilakukan akan turun secara signifikan membuat refactoring masa depan (atau bahkan penulisan ulang besar) jauh lebih mudah.

Karena Anda dapat memperluas cakupan tes unit, Anda dapat pare down atau bahkan pensiun sebagian besar tes integrasi. Jika aplikasi Anda membaca / menulis file atau mengakses DB, menguji bagian-bagian itu secara terpisah dan mengejeknya atau meminta pengujian Anda dimulai dengan membuat struktur data yang dibaca dari file / database adalah tempat yang jelas untuk memulai. Sebenarnya menciptakan infrastruktur pengujian akan memakan waktu lebih lama daripada menulis serangkaian tes cepat dan kotor; dan setiap kali Anda menjalankan tes integrasi 2 menit alih-alih menghabiskan waktu 30 menit secara manual menguji sebagian kecil dari apa yang dicakup tes integrasi, Anda sudah mendapatkan kemenangan besar.

Dan Neely
sumber