Bagaimana Anda menjaga tes unit Anda berfungsi saat refactoring?

29

Dalam pertanyaan lain, terungkap bahwa salah satu kesulitan dengan TDD adalah menjaga suite pengujian tetap sinkron dengan basis kode selama dan setelah refactoring.

Sekarang, saya penggemar berat refactoring. Saya tidak akan menyerah untuk melakukan TDD. Tetapi saya juga pernah mengalami masalah tes yang ditulis sedemikian rupa sehingga refactoring minor menyebabkan banyak kegagalan tes.

Bagaimana Anda menghindari melanggar tes saat refactoring?

  • Apakah Anda menulis tes 'lebih baik'? Jika demikian, apa yang harus Anda cari?
  • Apakah Anda menghindari jenis refactoring tertentu?
  • Apakah ada alat tes-refactoring?

Sunting: Saya menulis pertanyaan baru yang menanyakan apa yang ingin saya tanyakan (tetapi menjadikan ini sebagai varian yang menarik).

Alex Feinman
sumber
7
Saya akan berpikir bahwa, dengan TDD, langkah pertama Anda dalam refactoring adalah menulis tes yang gagal dan kemudian memperbaiki kode untuk membuatnya bekerja.
Matt Ellen
Tidak bisakah IDE Anda mengetahui cara memperbaiki tes juga?
@ Thorbjørn Ravn Andersen, ya, dan saya menulis pertanyaan baru yang menanyakan apa yang ingin saya tanyakan (tapi simpan yang ini sebagai varian yang menarik; lihat jawaban azheglov, yang pada dasarnya adalah apa yang Anda katakan)
Alex Feinman
Dianggap menambahkan Info itu ke pertanyaan ini?

Jawaban:

35

Apa yang Anda coba lakukan sebenarnya bukan refactoring. Dengan refactoring, menurut definisi, Anda tidak mengubah apa yang dilakukan perangkat lunak Anda, Anda mengubah cara melakukannya.

Mulailah dengan semua tes hijau (semua lulus), kemudian buat modifikasi "di bawah tenda" (misalnya memindahkan metode dari kelas turunan ke basis, mengekstrak metode, atau merangkum Komposit dengan Builder , dll.). Tes Anda harus tetap lulus.

Apa yang Anda uraikan tampaknya bukan refactoring, tetapi desain ulang, yang juga menambah fungsionalitas perangkat lunak Anda yang sedang diuji. TDD dan refactoring (seperti yang saya coba mendefinisikannya di sini) tidak bertentangan Anda masih dapat melakukan refactor (hijau-hijau) dan menerapkan TDD (merah-hijau) untuk mengembangkan fungsionalitas "delta".

azheglov
sumber
7
Kode yang sama X disalin 15 tempat. Disesuaikan di setiap tempat. Anda menjadikannya perpustakaan umum dan parameter X atau menggunakan pola strategi untuk memungkinkan perbedaan-perbedaan ini. Saya menjamin unit test untuk X akan gagal. Klien X akan gagal karena antarmuka publik sedikit berubah. Mendesain ulang atau refactor? Saya menyebutnya refactor tetapi bagaimanapun cara itu merusak segala macam hal. Intinya adalah Anda tidak bisa refactor kecuali Anda tahu persis bagaimana semuanya cocok. Maka memperbaiki tes itu membosankan tetapi pada akhirnya sepele.
Kevin
3
Jika tes membutuhkan penyesuaian konstan, itu mungkin petunjuk memiliki tes terlalu rinci. Sebagai contoh, misalkan sepotong kode perlu memicu peristiwa A, B dan C dalam keadaan tertentu, tanpa urutan tertentu. Kode lama melakukannya dalam urutan ABC dan tes mengharapkan peristiwa dalam urutan itu. Jika kode refactored memuntahkan peristiwa agar ACB masih berfungsi sesuai dengan spesifikasi tetapi tes akan gagal.
otto
3
@ Kevin: Saya percaya apa yang Anda gambarkan adalah desain ulang, karena perubahan antarmuka publik. Definisi Fowler tentang refactoring ("mengubah struktur internal [kode] tanpa mengubah perilaku eksternalnya") cukup jelas tentang hal itu.
azheglov
3
@azheglov: mungkin tetapi dalam pengalaman saya jika implementasinya buruk, begitu juga antarmuka
Kevin
2
Sebuah pertanyaan yang benar-benar valid dan jelas berakhir pada diskusi “makna kata”. Siapa yang peduli bagaimana Anda menyebutnya, mari kita bahas di tempat lain. Sementara itu jawaban ini benar-benar menghilangkan jawaban nyata tetapi masih memiliki yang paling jauh sejauh ini. Saya mengerti mengapa orang menyebut TDD sebagai agama.
Dirk Boer
21

Salah satu manfaat dari memiliki unit test adalah agar Anda dapat dengan yakin melakukan refactor.

Jika refactoring tidak mengubah antarmuka publik maka Anda meninggalkan tes unit apa adanya dan memastikan setelah refactoring mereka semua lulus.

Jika refactoring mengubah antarmuka publik maka tes harus ditulis ulang terlebih dahulu. Refactor sampai tes baru berlalu.

Saya tidak akan pernah menghindari refactoring karena itu merusak tes. Tes unit menulis bisa menjadi sakit di pantat tetapi nilainya sakit dalam jangka panjang.

Tim Murphy
sumber
7

Berlawanan dengan jawaban yang lain, penting untuk dicatat bahwa beberapa cara pengujian dapat menjadi rapuh ketika sistem yang diuji (SUT) di-refactored, jika pengujiannya adalah whitebox.

Jika saya menggunakan kerangka kerja mengejek yang memverifikasi urutan metode yang dipanggil pada tiruan (ketika urutan tidak relevan karena panggilan bebas efek samping); maka jika kode saya lebih bersih dengan panggilan metode tersebut dalam urutan yang berbeda dan saya refactor, maka pengujian saya akan rusak. Secara umum, mengejek dapat menyebabkan kerapuhan pada tes.

Jika saya memeriksa keadaan internal SUT saya dengan mengekspos anggota pribadi atau yang dilindungi (kita bisa menggunakan "teman" dalam visual basic, atau meningkatkan tingkat akses "internal" dan menggunakan "internalsvisibleto" dalam c #; dalam banyak bahasa OO, termasuk banyak c # a " test-specific-subclass " dapat digunakan) lalu tiba-tiba keadaan internal kelas akan menjadi masalah - Anda mungkin akan refactoring kelas sebagai kotak hitam, tetapi tes kotak putih akan gagal. Misalkan satu bidang digunakan kembali untuk mengartikan hal yang berbeda (bukan praktik yang baik!) Ketika SUT berubah status - jika kita membaginya menjadi dua bidang, kita mungkin perlu menulis ulang tes yang rusak.

Subclass uji-spesifik juga dapat digunakan untuk menguji metode yang dilindungi - yang dapat berarti bahwa refactor dari sudut pandang kode produksi merupakan perubahan besar dari sudut pandang kode uji. Memindahkan beberapa baris ke dalam atau keluar dari metode yang dilindungi mungkin tidak memiliki efek samping produksi, tetapi hancurkan tes.

Jika saya menggunakan " kait uji " atau kode kompilasi khusus uji atau kondisional lainnya, mungkin sulit untuk memastikan bahwa tes tidak rusak karena ketergantungan yang rapuh pada logika internal.

Jadi untuk mencegah agar tes tidak digabungkan dengan detail internal SUT yang intim, mungkin membantu untuk:

  • Gunakan bertopik daripada tiruan, jika memungkinkan. Untuk info lebih lanjut lihat blog Fabio Periera tentang tes tautologis , dan blog saya tentang tes tautologis .
  • Jika menggunakan tiruan, hindari memverifikasi urutan metode yang dipanggil, kecuali itu penting.
  • Cobalah untuk menghindari memverifikasi keadaan internal SUT Anda - gunakan API eksternal jika memungkinkan.
  • Cobalah untuk menghindari logika khusus tes dalam kode produksi
  • Cobalah untuk menghindari penggunaan subclass khusus-tes.

Semua poin di atas adalah contoh kopling kotak putih yang digunakan dalam pengujian. Jadi untuk benar-benar menghindari tes melanggar refactoring, gunakan pengujian kotak hitam SUT.

Penafian: Untuk tujuan membahas refactoring di sini, saya menggunakan kata ini sedikit lebih luas untuk memasukkan perubahan implementasi internal tanpa efek eksternal yang terlihat. Beberapa puritan mungkin tidak setuju dan merujuk secara eksklusif ke buku Martin Fowler dan Kent Beck Refactoring - yang menggambarkan operasi atom refactoring.

Dalam praktiknya, kami cenderung mengambil langkah-langkah tanpa-melanggar yang sedikit lebih besar daripada operasi atom yang dijelaskan di sana, dan khususnya perubahan yang membuat kode produksi berperilaku identik dari luar mungkin tidak meninggalkan tes yang lulus. Tapi saya pikir itu adil untuk memasukkan "algoritma pengganti untuk algoritma lain yang memiliki perilaku identik" sebagai refactor, dan saya pikir Fowler setuju. Martin Fowler sendiri mengatakan bahwa refactoring dapat membatalkan tes:

Saat Anda menulis tes mockist, Anda menguji panggilan keluar SUT untuk memastikannya berbicara dengan baik kepada pemasoknya. Tes klasik hanya peduli tentang kondisi akhir - bukan bagaimana status itu diturunkan. Tes Mockist dengan demikian lebih digabungkan dengan implementasi metode. Mengubah sifat panggilan ke kolaborator biasanya menyebabkan tes mockist rusak.

[...]

Menggabungkan ke implementasi juga mengganggu refactoring, karena perubahan implementasi lebih mungkin untuk memecahkan tes daripada dengan pengujian klasik.

Fowler - Mengolok-olok bukan bertopik

perfeksionis
sumber
Fowler benar-benar menulis buku tentang Refactoring; dan buku yang paling resmi tentang pengujian Unit (Pola Tes Unit oleh Gerard Meszaros) ada dalam seri "tanda tangan" Fowler, jadi ketika dia mengatakan refactoring dapat memecahkan tes, dia mungkin benar.
perfeksionis
5

Jika tes Anda rusak saat Anda refactoring, maka Anda tidak, menurut definisi, refactoring, yang merupakan "mengubah struktur program Anda tanpa mengubah perilaku program Anda".

Terkadang Anda DO perlu mengubah perilaku tes Anda. Mungkin Anda perlu menggabungkan dua metode bersama (katakanlah, ikat () dan dengarkan () pada kelas soket TCP yang mendengarkan), jadi Anda memiliki bagian lain dari kode Anda yang mencoba dan gagal menggunakan API yang sekarang diubah. Tapi itu bukan refactoring!

Frank Shearar
sumber
Bagaimana jika dia hanya mengubah nama metode yang diuji oleh tes? Tes akan gagal kecuali Anda menamainya kembali dalam tes juga. Di sini dia tidak mengubah perilaku program.
Oscar Mederos
2
Dalam hal ini tesnya juga sedang di refactored. Anda perlu berhati-hati: pertama-tama Anda mengubah nama metode, kemudian Anda menjalankan tes Anda. Seharusnya gagal karena alasan yang benar (tidak dapat dikompilasi (C #), Anda mendapatkan pengecualian MessageNotUnderstood (Smalltalk), sepertinya tidak ada yang terjadi (pola makan nol Objective-C))). Kemudian Anda mengubah tes Anda, mengetahui bahwa Anda belum sengaja memperkenalkan bug apa pun. "Jika tes Anda gagal" berarti "jika tes Anda gagal setelah Anda menyelesaikan refactoring", dengan kata lain. Coba simpan bongkahan kecil!
Frank Shearar
1
Tes Unit secara inheren digabungkan dengan struktur kode. Misalnya, Fowler memiliki banyak di refactoring.com/catalog yang akan memengaruhi tes unit (misalnya, metode sembunyikan, metode inline, ganti kode kesalahan dengan pengecualian, dan sebagainya).
Kristian H
Salah. Menggabungkan dua metode bersama-sama jelas merupakan refactoring yang memiliki nama resmi (misalnya refactoring metode inline sesuai dengan definisi) dan itu akan memecah tes metode yang digarisbawahi - beberapa kasus uji sekarang harus ditulis ulang / diuji dengan cara lain. Saya tidak harus mengubah perilaku suatu program untuk memecahkan tes unit, yang perlu saya lakukan adalah merestrukturisasi internal yang memiliki tes unit digabungkan dengan mereka. Selama perilaku suatu program tidak berubah, ini masih sesuai dengan definisi refactoring.
KolA
Saya menulis di atas dengan asumsi tes yang ditulis dengan baik: jika Anda menguji implementasi Anda - jika struktur tes mencerminkan internal kode yang diuji, tentu saja. Dalam hal ini, uji kontrak unit, bukan implementasinya.
Frank Shearar
4

Saya pikir masalah dengan pertanyaan ini, adalah bahwa orang yang berbeda mengambil kata 'refactoring' secara berbeda. Saya pikir yang terbaik adalah dengan hati-hati mendefinisikan beberapa hal yang mungkin Anda maksudkan:

>  Keep the API the same, but change how the API is implemented internally
>  Change the API

Seperti yang sudah dicatat oleh satu orang lain, jika Anda menjaga API tetap sama, dan semua tes regresi Anda beroperasi pada API publik, Anda seharusnya tidak memiliki masalah. Refactoring seharusnya tidak menimbulkan masalah sama sekali. Setiap tes yang gagal BAIK berarti kode lama Anda memiliki bug dan tes Anda buruk, atau kode baru Anda memiliki bug.

Tapi itu cukup jelas. Jadi, Anda MUNGKIN bermaksud dengan refactoring, bahwa Anda mengubah API.

Jadi izinkan saya menjawab bagaimana mendekati itu!

  • Pertama buat API BARU, yang melakukan apa yang Anda inginkan perilaku API BARU Anda. Jika kebetulan bahwa API baru ini memiliki nama yang sama dengan API TUA, maka saya menambahkan nama _NEW ke nama API baru.

    int DoSomethingInterestingAPI ();

menjadi:

int DoSomethingInterestingAPI_NEW( int takes_more_arguments );
int DoSomethingInterestingAPI_OLD();
int DoSomethingInterestingAPI() { DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API);

OK - pada tahap ini - semua tes regresi Anda masih lulus - menggunakan nama DoSomethingInterestingAPI ().

NEXT, buka kode Anda dan ubah semua panggilan ke DoSomethingInterestingAPI () ke varian yang sesuai dari DoSomethingInterestingAPI_NEW (). Ini termasuk memperbarui / menulis ulang bagian apa pun dari tes regresi Anda yang perlu diubah untuk menggunakan API baru.

NEXT, tandai DoSomethingInterestingAPI_OLD () sebagai [[usang ()]]. Tetap gunakan API yang sudah tidak digunakan lagi selama Anda mau (sampai Anda telah memperbarui semua kode yang mungkin bergantung dengan aman).

Dengan pendekatan ini, setiap kegagalan dalam tes regresi Anda hanyalah bug dalam tes regresi itu atau mengidentifikasi bug dalam kode Anda - persis seperti yang Anda inginkan. Proses bertahap untuk merevisi API dengan secara eksplisit membuat versi _NEW dan _OLD dari API ini memungkinkan Anda untuk memiliki beberapa bit dari kode baru dan lama yang hidup berdampingan untuk sementara waktu.

Lewis Pringle
sumber
Saya suka jawaban ini karena membuatnya jelas bahwa Tes Unit untuk SUT sama dengan klien eksternal untuk Api yang dipublikasikan. Apa yang Anda meresepkan sangat mirip dengan protokol SemVer untuk mengelola perpustakaan / komponen yang diterbitkan untuk menghindari 'neraka ketergantungan'. Namun ini datang pada biaya waktu dan fleksibilitas, memperkirakan pendekatan ini untuk antarmuka publik setiap unit mikro berarti mengekstrapolasi biaya juga. Pendekatan yang lebih fleksibel adalah dengan memisahkan tes dari implementasi sebanyak mungkin yaitu pengujian integrasi atau DSL terpisah untuk menggambarkan input dan output tes
KolA
1

Saya menganggap tes unit Anda adalah granularity yang saya sebut "bodoh" :) yaitu, mereka menguji hal-hal kecil mutlak dari setiap kelas dan fungsi. Langkah menjauh dari alat pembuat kode dan menulis tes yang berlaku untuk permukaan yang lebih besar, maka Anda dapat refactor internal sebanyak yang Anda inginkan, mengetahui bahwa antarmuka ke aplikasi Anda belum berubah, dan tes Anda masih berfungsi.

Jika Anda ingin memiliki unit test yang menguji masing-masing dan setiap metode, maka Anda harus melakukan refactor secara bersamaan.

gbjbaanb
sumber
1
Jawaban paling berguna yang benar-benar menjawab pertanyaan - jangan membangun cakupan tes Anda di atas fondasi trivia internal yang goyah, atau berharap itu akan terus berantakan - namun yang paling tidak disukai karena TDD menentukan untuk melakukan hal yang sebaliknya. Ini adalah apa yang Anda dapatkan untuk menunjukkan kebenaran yang tidak nyaman tentang pendekatan yang terlalu hyped.
KolA
1

menjaga suite pengujian tetap sinkron dengan basis kode selama dan setelah refactoring

Yang menyulitkan adalah menyambung . Setiap tes datang dengan beberapa tingkat penggabungan ke detail implementasi tetapi tes unit (terlepas dari apakah itu TDD atau tidak) sangat buruk dalam hal itu karena mengganggu internal: lebih banyak tes unit sama dengan lebih banyak kode digabungkan ke unit yaitu metode tanda tangan / antarmuka publik lainnya unit - setidaknya.

"Unit" menurut definisi adalah rincian implementasi tingkat rendah, antarmuka unit dapat dan harus diubah / dipisah / digabung dan jika tidak bermutasi saat sistem berkembang. Kelimpahan tes unit sebenarnya dapat menghambat evolusi ini lebih dari itu membantu.

Bagaimana menghindari tes yang gagal saat melakukan refactoring? Hindari sambungan. Dalam praktiknya itu berarti menghindari sebanyak mungkin unit test dan lebih memilih tes level / integrasi yang lebih tinggi daripada detail implementasi. Ingat juga bahwa tidak ada peluru perak, tes masih harus berpasangan dengan sesuatu pada tingkat tertentu, tetapi idealnya itu adalah antarmuka yang secara eksplisit diversi menggunakan Semantic Versioning yaitu biasanya di tingkat api / aplikasi yang dipublikasikan (Anda tidak ingin melakukan SemVer untuk setiap unit dalam solusi Anda).

Kola
sumber
0

Tes Anda terlalu erat untuk implementasi dan bukan persyaratan.

pertimbangkan menulis tes Anda dengan komentar seperti ini:

//given something
...test code...
//and something else
...test code...
//when something happens
...test code...
//then the state should be...
...test code...

dengan cara ini Anda tidak dapat memperbaiki arti dari tes.

mcintyre321
sumber