Saya baru-baru ini menonton "All the Little Things" dari RailsConf 2014. Selama pembicaraan ini, Sandi Metz refactor fungsi yang mencakup pernyataan if-bersarang besar:
def tick
if @name != 'Aged Brie' && @name != 'Backstage passes to a TAFKAL80ETC concert'
if @quality > 0
if @name != 'Sulfuras, Hand of Ragnaros'
@quality -= 1
end
end
else
...
end
...
end
Langkah pertama adalah memecah fungsi menjadi beberapa yang lebih kecil:
def tick
case name
when 'Aged Brie'
return brie_tick
...
end
end
def brie_tick
@days_remaining -= 1
return if quality >= 50
@quality += 1
@quality += 1 if @days_remaining <= 0
end
Apa yang saya temukan menarik adalah cara fungsi-fungsi kecil ini ditulis. brie_tick
, misalnya, tidak ditulis dengan mengekstraksi bagian yang relevan dari tick
fungsi asli , tetapi dari awal dengan mengacu pada test_brie_*
unit test. Setelah semua tes unit ini berlalu, brie_tick
dianggap sudah selesai. Setelah semua fungsi kecil selesai, monolitik aslitick
fungsi dihapus.
Sayangnya, presenter sepertinya tidak menyadari bahwa pendekatan ini menyebabkan tiga dari empat *_tick
fungsi salah (dan yang lainnya kosong!). Ada kasus tepi di mana perilaku *_tick
fungsi berbeda dari tick
fungsi aslinya . Misalnya, @days_remaining <= 0
dalam brie_tick
seharusnya < 0
- jadi brie_tick
tidak berfungsi dengan benar ketika dipanggil dengan days_remaining == 1
dan quality < 50
.
Apa yang salah di sini? Apakah ini kegagalan pengujian - karena tidak ada tes untuk kasus tepi khusus ini? Atau kegagalan refactoring - karena kode seharusnya ditransformasi selangkah demi selangkah daripada ditulis ulang dari awal?
sumber
Jawaban:
Kedua. Refactoring hanya menggunakan langkah-langkah standar dari buku asli Fowlers jelas lebih rentan kesalahan daripada melakukan penulisan ulang, sehingga sering lebih baik menggunakan langkah-langkah bayi semacam ini saja. Bahkan jika tidak ada tes unit untuk setiap kasus tepi, dan bahkan jika lingkungan tidak menyediakan refactoring otomatis, perubahan kode tunggal seperti "memperkenalkan variabel yang menjelaskan" atau "fungsi ekstrak" memiliki peluang yang jauh lebih kecil untuk mengubah rincian perilaku dari kode yang ada daripada penulisan ulang fungsi secara lengkap.
Namun, kadang-kadang, menulis ulang bagian kode adalah apa yang perlu atau ingin Anda lakukan. Dan jika itu masalahnya, Anda perlu tes yang lebih baik.
Perhatikan bahwa bahkan ketika menggunakan alat refactoring, selalu ada risiko tertentu kesalahan ketika Anda mengubah kode, terlepas dari menerapkan langkah-langkah yang lebih kecil atau lebih besar. Itu sebabnya refactoring selalu membutuhkan tes. Perhatikan juga bahwa tes hanya dapat mengurangi kemungkinan bug, tetapi tidak pernah membuktikan ketidakhadiran mereka - namun menggunakan teknik seperti melihat kode dan cakupan cabang dapat memberi Anda tingkat kepercayaan yang tinggi, dan dalam kasus penulisan ulang bagian kode, itu adalah sering layak untuk menerapkan teknik seperti itu.
sumber
brie_tick
sementara masih tidak pernah menguji@days_remaining == 1
kasus yang bermasalah dengan, misalnya, pengujian dengan@days_remaining
set ke10
dan-10
.Salah satu hal yang sangat sulit tentang bekerja dengan kode warisan: memperoleh pemahaman lengkap tentang perilaku saat ini.
Kode warisan tanpa tes yang membatasi semua perilaku adalah pola umum di alam liar. Yang membuat Anda menebak: apakah itu berarti bahwa perilaku yang tidak dibatasi adalah variabel bebas? atau persyaratan yang tidak ditentukan?
Dari pembicaraan :
Ini adalah pendekatan yang lebih konservatif; jika persyaratan mungkin tidak ditentukan secara spesifik, jika tes tidak menangkap semua logika yang ada, maka Anda harus sangat berhati-hati tentang bagaimana Anda melanjutkan.
Yang pasti, Anda dapat menegaskan bahwa jika tes tidak cukup menggambarkan perilaku sistem, bahwa Anda memiliki "kegagalan pengujian". Dan saya pikir itu adil - tetapi sebenarnya tidak berguna; ini adalah masalah umum yang ada di alam liar.
Masalahnya bukan cukup bahwa transformasi seharusnya langkah-demi-langkah; tetapi lebih karena pilihan alat refactoring (operator keyboard manusia - bukan otomatisasi yang dipandu) tidak selaras dengan cakupan uji, karena tingkat kesalahan yang lebih tinggi.
Ini bisa diatasi baik dengan menggunakan alat refactoring dengan keandalan yang lebih tinggi atau dengan memperkenalkan baterai yang lebih luas tes untuk meningkatkan kendala pada sistem.
Jadi saya pikir kata penghubung Anda dipilih dengan buruk;
AND
tidakOR
.sumber
Refactoring tidak boleh mengubah perilaku kode Anda yang terlihat secara eksternal. Itulah tujuannya.
Jika pengujian unit Anda gagal, itu menunjukkan Anda mengubah perilaku. Tetapi lulus unit test tidak pernah menjadi tujuannya. Ini membantu kurang lebih untuk mencapai tujuan Anda. Jika refactoring mengubah perilaku yang terlihat secara eksternal, dan semua tes unit lulus, maka refactoring Anda gagal.
Tes unit kerja dalam kasus ini hanya memberi Anda perasaan sukses yang salah. Tapi apa yang salah? Dua hal: Refactoring ceroboh, dan unit test tidak terlalu baik.
sumber
Jika Anda mendefinisikan "benar" menjadi "tes lulus", maka menurut definisi itu tidak salah untuk mengubah perilaku yang belum diuji.
Jika perilaku tepi tertentu harus didefinisikan, tambahkan tes untuk itu, jika tidak, maka boleh saja untuk tidak peduli apa yang terjadi. Jika Anda benar-benar jagoan, Anda dapat menulis tes yang memeriksa
true
kapan dalam kasus tepi untuk mendokumentasikan bahwa Anda tidak peduli apa perilaku itu.sumber