Refactoring - apakah pantas untuk hanya menulis ulang kode, selama semua tes lulus?

9

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 tickfungsi asli , tetapi dari awal dengan mengacu pada test_brie_*unit test. Setelah semua tes unit ini berlalu, brie_tickdianggap sudah selesai. Setelah semua fungsi kecil selesai, monolitik aslitick fungsi dihapus.

Sayangnya, presenter sepertinya tidak menyadari bahwa pendekatan ini menyebabkan tiga dari empat *_tickfungsi salah (dan yang lainnya kosong!). Ada kasus tepi di mana perilaku *_tickfungsi berbeda dari tickfungsi aslinya . Misalnya, @days_remaining <= 0dalam brie_tickseharusnya < 0- jadi brie_ticktidak berfungsi dengan benar ketika dipanggil dengan days_remaining == 1dan 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?

pengguna200783
sumber
2
Saya tidak yakin saya mendapatkan pertanyaan. Tentu saja OK untuk menulis ulang kode. Saya tidak yakin apa yang Anda maksud khusus oleh "tidak apa-apa untuk hanya menulis ulang kode." Jika Anda bertanya "Apakah boleh menulis ulang kode tanpa banyak memikirkannya," jawabannya tidak, sama seperti tidak boleh menulis kode dengan cara itu.
John Wu
Hal ini sering terjadi karena rencana pengujian terutama berfokus pada pengujian kasus penggunaan yang sukses dan sangat sedikit (atau tidak sama sekali) pada kasus penggunaan kesalahan atau kasus sub-penggunaan. Jadi itu terutama kebocoran cakupan. Kebocoran pengujian.
Laiv
@ JohnWu - Saya mendapat kesan bahwa refactoring umumnya dilakukan sebagai serangkaian transformasi kecil ke kode sumber ("ekstrak-metode" dll.) Daripada dengan hanya menulis ulang kode (yang saya maksudkan menulisnya lagi dari awal tanpa melihat kode yang ada, seperti yang dilakukan dalam presentasi yang ditautkan).
user200783
@ JohnWu - Apakah menulis ulang dari awal merupakan teknik refactoring yang dapat diterima? Jika tidak, akan mengecewakan melihat presentasi yang sangat dihargai tentang refactoring yang mengambil pendekatan itu. OTOH jika itu dapat diterima, maka perubahan perilaku yang tidak diinginkan dapat disalahkan pada tes yang hilang - tetapi apakah ada cara untuk yakin bahwa tes mencakup semua kasus tepi yang mungkin?
user200783
@ User200783 Nah itu adalah pertanyaan yang lebih besar, bukan (bagaimana cara memastikan tes saya komprehensif?) Secara pragmatis, saya mungkin akan menjalankan laporan cakupan kode sebelum membuat perubahan, dan dengan hati-hati memeriksa area kode yang tidak berolahraga, memastikan tim pengembangan memperhatikan mereka saat mereka menulis ulang logika.
John Wu

Jawaban:

11

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?

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.

Doc Brown
sumber
1
Terima kasih, itu masuk akal. Jadi, jika solusi akhir untuk perubahan perilaku yang tidak diinginkan adalah memiliki tes komprehensif, apakah ada cara untuk yakin bahwa tes mencakup semua kasus tepi yang mungkin? Sebagai contoh, adalah mungkin untuk memiliki cakupan 100% brie_ticksementara masih tidak pernah menguji @days_remaining == 1kasus yang bermasalah dengan, misalnya, pengujian dengan @days_remainingset ke 10dan -10.
user200783
2
Anda tidak pernah bisa benar-benar yakin bahwa tes mencakup semua kasus tepi yang mungkin, karena tidak layak untuk menguji dengan semua input yang mungkin. Tetapi ada banyak cara untuk mendapatkan lebih banyak kepercayaan dalam tes. Anda dapat melihat pengujian mutasi , yang merupakan cara untuk menguji efektivitas tes.
bdsl
1
Dalam hal ini, cabang yang terlewatkan bisa saja tertangkap dengan alat cakupan kode saat mengembangkan tes.
cbojar
2

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?

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 :

Sekarang ini adalah refactoring nyata sesuai dengan definisi refactoring; Saya akan memperbaiki kode ini. Saya akan mengubah pengaturannya tanpa mengubah perilakunya.

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.

Atau kegagalan refactoring - karena kode seharusnya ditransformasi selangkah demi selangkah daripada ditulis ulang dari awal?

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; ANDtidak OR.

VoiceOfUnreason
sumber
2

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.

gnasher729
sumber
1

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 truekapan dalam kasus tepi untuk mendokumentasikan bahwa Anda tidak peduli apa perilaku itu.

Caleth
sumber