Saya telah membaca beberapa jawaban untuk pertanyaan di sepanjang baris yang sama seperti "Bagaimana Anda menjaga tes unit Anda bekerja saat refactoring?". Dalam kasus saya, skenarionya sedikit berbeda karena saya diberi proyek untuk meninjau dan menyesuaikan dengan beberapa standar yang kami miliki, saat ini tidak ada tes sama sekali untuk proyek!
Saya telah mengidentifikasi beberapa hal yang saya pikir bisa dilakukan dengan lebih baik seperti TIDAK mencampur kode jenis DAO dalam lapisan layanan.
Sebelum refactoring, sepertinya ide yang baik untuk menulis tes untuk kode yang ada. Masalahnya bagi saya adalah ketika saya melakukan refactor maka tes-tes itu akan rusak ketika saya mengubah di mana logika tertentu dilakukan dan tes akan ditulis dengan struktur sebelumnya dalam pikiran (mocked dependency, dll.)
Dalam kasus saya, apa cara terbaik untuk melanjutkan? Saya tergoda untuk menulis tes di sekitar kode refactored tapi saya sadar ada risiko saya dapat memperbaiki hal-hal yang salah yang dapat mengubah perilaku yang diinginkan.
Apakah ini refactor atau redesign, saya senang pengertian saya tentang istilah-istilah itu diperbaiki, saat ini saya sedang mengerjakan definisi berikut untuk refactoring "Dengan refactoring, menurut definisi, Anda tidak mengubah apa yang dilakukan perangkat lunak Anda, Anda mengubah cara kerjanya. " Jadi saya tidak mengubah perangkat lunak apa yang akan saya ubah bagaimana / di mana ia melakukannya.
Sama saya bisa melihat argumen bahwa jika saya mengubah tanda tangan metode yang dapat dianggap desain ulang.
Ini contoh singkatnya
MyDocumentService.java
(arus)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
DataResultSet rs = documentDAO.findAllDocuments();
List<Document> documents = new ArrayList<>();
for(DataObject do: rs.getRows()) {
//get row data create new document add it to
//documents list
}
return documents;
}
}
MyDocumentService.java
(refactored / didesain ulang apa pun)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
//Code dealing with DataResultSet moved back up to DAO
//DAO now returns a List<Document> instead of a DataResultSet
return documentDAO.findAllDocuments();
}
}
sumber
Jawaban:
Anda sedang mencari tes yang memeriksa regresi . yaitu melanggar beberapa perilaku yang ada. Saya akan mulai dengan mengidentifikasi pada tingkat apa perilaku itu akan tetap sama, dan bahwa antarmuka yang mendorong perilaku itu akan tetap sama, dan mulai melakukan tes pada saat itu.
Anda sekarang memiliki beberapa tes yang akan menyatakan bahwa apa pun yang Anda lakukan di bawah level ini, perilaku Anda tetap sama.
Anda benar mempertanyakan bagaimana tes dan kode dapat tetap sinkron. Jika antarmuka Anda ke komponen tetap sama, maka Anda dapat menulis tes di sekitar ini dan menyatakan kondisi yang sama untuk kedua implementasi (saat Anda membuat implementasi baru). Jika tidak, maka Anda harus menerima bahwa tes untuk komponen yang redundan adalah tes yang berlebihan.
sumber
Praktik yang disarankan adalah memulai dengan menulis "tes pin-down" yang menguji perilaku kode saat ini, mungkin termasuk bug, tetapi tanpa mengharuskan Anda untuk masuk ke dalam kegilaan membedakan apakah perilaku tertentu yang melanggar dokumen persyaratan adalah bug, solusi untuk sesuatu yang tidak Anda sadari, atau merupakan perubahan persyaratan yang tidak didokumentasikan.
Masuk akal jika tes pin-down ini dilakukan pada level tinggi, yaitu integrasi daripada tes unit, sehingga tes tersebut akan tetap berfungsi saat Anda mulai melakukan refactoring.
Tetapi beberapa refactoring mungkin diperlukan untuk membuat kode dapat diuji - hanya berhati-hatilah untuk tetap pada refactoring "aman". Misalnya, dalam hampir semua kasus, metode yang bersifat pribadi dapat dipublikasikan tanpa melanggar apa pun.
sumber
Saya sarankan - jika Anda belum - membaca baik Bekerja Efektif Dengan Kode Legacy maupun Refactoring - Meningkatkan Desain Kode yang Ada .
Saya tidak perlu melihat ini sebagai masalah: Tulis tes, ubah struktur kode Anda, dan kemudian sesuaikan struktur tes juga . Ini akan memberi Anda umpan balik langsung apakah struktur baru Anda sebenarnya lebih baik daripada yang lama, karena jika ya, tes yang disesuaikan akan lebih mudah untuk ditulis (dan dengan demikian mengubah tes harus relatif mudah, menurunkan risiko memiliki yang baru diperkenalkan bug lulus tes).
Juga, seperti yang telah ditulis orang lain: Jangan menulis tes terlalu rinci (setidaknya tidak di awal). Cobalah untuk tetap berada pada tingkat abstraksi yang tinggi (dengan demikian tes Anda mungkin akan lebih baik ditandai sebagai regresi atau bahkan tes integrasi).
sumber
Jangan menulis tes unit ketat di mana Anda mengejek semua dependensi. Beberapa orang akan memberi tahu Anda ini bukan tes unit nyata. Abaikan mereka. Tes ini bermanfaat, dan itulah yang penting.
Mari kita lihat contoh Anda:
Tes Anda mungkin terlihat seperti ini:
Alih-alih mengejek DocumentPOR, mengejek dependensinya:
Sekarang, Anda dapat memindahkan logika dari
MyDocumentService
keDocumentDao
tanpa melanggar tes. Tes akan menunjukkan bahwa fungsinya sama (sejauh Anda telah mengujinya).sumber
Seperti yang Anda katakan, jika Anda mengubah perilaku maka itu adalah transformasi dan bukan refactor. Pada tingkat apa Anda mengubah perilaku adalah apa yang membuat perbedaan.
Jika tidak ada tes formal di level tertinggi maka cobalah dan temukan satu set persyaratan yang klien (kode panggilan atau manusia) yang perlu tetap sama setelah Anda mendesain ulang agar kode Anda dianggap berfungsi. Itu adalah daftar kasus uji yang perlu Anda laksanakan.
Untuk menjawab pertanyaan Anda tentang perubahan implementasi yang membutuhkan perubahan test case, saya sarankan Anda melihat Detroit (klasik) vs London (mockist) TDD. Martin Fowler membicarakan hal ini dalam artikelnya yang hebat Mocks bukan bertopik tetapi banyak orang memiliki pendapat. Jika Anda mulai di level tertinggi, di mana eksternal Anda tidak bisa berubah, dan turun, maka persyaratannya harus tetap stabil sampai Anda mencapai level yang benar-benar perlu diubah.
Tanpa tes apa pun, ini akan sulit, dan Anda mungkin ingin mempertimbangkan menjalankan klien melalui jalur kode ganda (dan mencatat perbedaannya) sampai Anda dapat memastikan kode baru Anda melakukan apa yang perlu dilakukan.
sumber
Ini pendekatan saya. Ini memiliki biaya dalam hal waktu karena merupakan tes refactor dalam 4 fase.
Apa yang akan saya paparkan mungkin cocok dengan komponen yang lebih rumit daripada yang terpapar dalam contoh pertanyaan.
Bagaimanapun strategi tersebut valid untuk setiap kandidat komponen yang dinormalisasi dengan antarmuka (DAO, Layanan, Pengendali, ...).
1. Antarmuka
Mari kita kumpulkan semua metode publik dari MyDocumentService dan mari kita satukan semuanya menjadi sebuah antarmuka. Misalnya. Jika sudah ada, gunakan yang itu daripada mengatur yang baru .
Kemudian kami memaksa MyDocumentService untuk mengimplementasikan antarmuka baru ini.
Sejauh ini bagus. Tidak ada perubahan besar yang dilakukan, kami menghormati kontrak saat ini dan behaivos tetap tidak tersentuh.
2. Tes unit kode warisan
Di sini kita memiliki kerja keras. Untuk mengatur test suite. Kita harus menetapkan sebanyak mungkin kasus: kasus sukses dan juga kasus kesalahan. Yang terakhir ini untuk kebaikan kualitas hasil.
Sekarang, alih-alih menguji MyDocumentService kita akan menggunakan antarmuka sebagai kontrak yang akan diuji.
Saya tidak akan masuk ke rincian, jadi maafkan saya Jika kode saya terlihat terlalu sederhana atau terlalu agnostik
Tahap ini membutuhkan waktu lebih lama daripada yang lain dalam pendekatan ini. Dan itu yang paling penting karena akan menetapkan titik referensi untuk perbandingan di masa depan.
Catatan: Karena tidak ada perubahan besar yang dibuat dan behaivor tetap tidak tersentuh. Saya sarankan untuk melakukan tag di sini ke SCM. Tag atau cabang tidak masalah. Lakukan saja versi.
Kami menginginkannya untuk rollbacks, perbandingan versi dan mungkin untuk eksekusi paralel dari kode lama dan yang baru.
3. Refactoring
Refactor akan diimplementasikan menjadi komponen baru. Kami tidak akan melakukan perubahan pada kode yang ada. Langkah pertama semudah melakukan copy & paste MyDocumentService dan ganti namanya menjadi CustomDocumentService (misalnya).
Kelas baru terus menerapkan DocumentService . Lalu pergi dan refactorize getAllDocuments () . (Mari kita mulai dengan satu. Pin-refactor)
Mungkin memerlukan beberapa perubahan pada antarmuka / metode DAO. Jika demikian, jangan ubah kode yang ada. Terapkan metode Anda sendiri di antarmuka DAO. Kode lama Annotate sebagai Usang dan Anda akan tahu nanti apa yang harus dihapus.
Sangat penting untuk tidak merusak / mengubah implementasi yang ada. Kami ingin menjalankan kedua layanan secara paralel dan kemudian membandingkan hasilnya.
4. Memperbarui DocumentServiceTestSuite
Ok, sekarang bagian yang lebih mudah. Untuk menambah tes komponen baru.
Sekarang kita memiliki oldResult dan newResult keduanya divalidasi secara independen tetapi kita juga dapat membandingkan satu sama lain. Validasi terakhir ini bersifat opsional dan tergantung pada hasilnya. Mungkin itu tidak sebanding.
Mungkin tidak membuat terlalu banyak seity untuk membandingkan dua koleksi dengan cara ini, tetapi akan berlaku untuk objek jenis lain (pojos, entitas model data, DTO, Pembungkus, tipe asli ...)
Catatan
Saya tidak akan berani memberi tahu bagaimana melakukan tes unit atau cara menggunakan lib mock. Saya tidak berani mengatakan bagaimana Anda harus melakukan refactor. Yang ingin saya lakukan adalah menyarankan strategi global. Cara membawanya tergantung pada Anda. Anda tahu persis bagaimana kode itu, kompleksitasnya dan apakah strategi seperti itu patut dicoba. Fakta seperti waktu dan sumber daya penting di sini. Juga penting apa yang Anda harapkan dari tes ini di masa depan.
Saya telah memulai contoh saya dengan Layanan dan saya akan mengikuti dengan DAO dan seterusnya. Masuk jauh ke tingkat ketergantungan. Kurang lebih bisa digambarkan sebagai strategi dari atas ke bawah . Namun untuk perubahan kecil / refaktor ( seperti yang terlihat pada contoh tur ), bottom up akan melakukan tugas dengan lebih mudah. Karena ruang lingkup perubahannya sedikit.
Akhirnya, terserah Anda untuk menghapus kode yang sudah usang dan untuk mengalihkan dependensi lama ke yang baru.
Hapus juga tes yang sudah usang dan pekerjaan sudah selesai. Jika Anda memberi versi solusi lama dengan pengujiannya, Anda dapat memeriksa dan membandingkan satu sama lain kapan saja.
Karena begitu banyak pekerjaan, Anda memiliki kode lama diuji, divalidasi, dan versi. Dan kode baru, diuji, divalidasi, dan siap diversi.
sumber
tl; dr Jangan menulis unit test. Tulis tes pada tingkat yang lebih tepat.
Mengingat definisi kerja Anda tentang refactoring:
ada spektrum yang sangat luas. Di satu sisi adalah perubahan mandiri untuk metode tertentu, mungkin menggunakan algoritma yang lebih efisien. Di ujung lain porting ke bahasa lain.
Apa pun tingkat refactoring / desain ulang yang dilakukan, penting untuk memiliki tes yang beroperasi pada tingkat itu atau lebih tinggi.
Tes otomatis sering diklasifikasikan berdasarkan level sebagai:
Tes unit - Komponen individual (kelas, metode)
Tes integrasi - Interaksi antar komponen
Tes sistem - Aplikasi lengkap
Tulis tingkat tes yang dapat bertahan pada refactoring yang pada dasarnya tidak tersentuh.
Berpikir:
sumber
Jangan buang waktu menulis tes yang menghubungkan pada titik-titik di mana Anda dapat mengantisipasi bahwa antarmuka akan berubah dengan cara yang tidak sepele. Ini sering merupakan tanda bahwa Anda mencoba untuk menguji unit-unit kelas yang sifatnya 'kolaboratif' - yang nilainya tidak dalam apa yang mereka lakukan sendiri, tetapi dalam cara mereka berinteraksi dengan sejumlah kelas yang terkait erat untuk menghasilkan perilaku yang berharga . Ini yang perilaku yang Anda ingin tes, yang berarti bahwa Anda ingin menguji pada tingkat yang lebih tinggi. Pengujian di bawah level ini seringkali membutuhkan banyak ejekan yang jelek, dan tes yang dihasilkan dapat menjadi hambatan bagi pengembangan daripada bantuan untuk mempertahankan perilaku.
Jangan terlalu terpaku pada apakah Anda membuat refactor, mendesain ulang, atau apa pun. Anda dapat membuat perubahan yang pada tingkat yang lebih rendah merupakan desain ulang sejumlah komponen, tetapi pada tingkat integrasi yang lebih tinggi hanya berarti sebuah refactor. Intinya adalah untuk menjadi jelas tentang perilaku apa yang bernilai bagi Anda, dan mempertahankan perilaku itu saat Anda pergi.
Mungkin bermanfaat untuk dipertimbangkan saat Anda menulis tes - bisakah saya dengan mudah menjelaskan kepada QA, pemilik produk, atau pengguna, tes apa yang sebenarnya diuji? Jika sepertinya menggambarkan tes akan terlalu esoteris dan teknis, mungkin Anda menguji pada tingkat yang salah. Tes pada poin / level yang 'masuk akal', dan jangan gabungkan kode Anda dengan tes di setiap level.
sumber
Tugas pertama Anda adalah mencoba membuat "tanda tangan metode ideal" untuk pengujian Anda. Berusaha keras untuk menjadikannya fungsi murni . Ini harus independen dari kode yang sebenarnya sedang diuji; itu adalah lapisan adaptor kecil. Tulis kode Anda ke lapisan adaptor ini. Sekarang ketika Anda memperbaiki kode Anda, Anda hanya perlu mengubah lapisan adaptor. Ini adalah contoh sederhana:
Tesnya bagus, tetapi kode yang diuji memiliki API yang buruk. Saya dapat melakukan refactor tanpa mengubah tes hanya dengan memperbarui lapisan adaptor saya:
Contoh ini tampaknya cukup jelas untuk dilakukan sesuai prinsip Jangan Ulangi Diri Sendiri, tetapi mungkin tidak begitu jelas dalam kasus lain. Keuntungan melampaui KERING - keuntungan sebenarnya adalah decoupling dari tes dari kode yang diuji.
Tentu saja, teknik ini mungkin tidak disarankan dalam semua situasi. Misalnya, tidak akan ada alasan untuk menulis adaptor untuk POCO / POJO karena mereka tidak benar-benar memiliki API yang dapat berubah secara independen dari kode pengujian. Juga jika Anda menulis sejumlah kecil tes, lapisan adaptor yang relatif besar mungkin akan sia-sia.
sumber