Bagaimana Cara Menghindari Tes Unit Rapuh?

24

Kami telah menulis hampir 3.000 tes - data telah dikodekan dengan keras, sangat sedikit penggunaan kembali kode. Metodologi ini sudah mulai menggigit kita. Saat sistem berubah, kami mendapati diri kami menghabiskan lebih banyak waktu memperbaiki tes yang rusak. Kami memiliki tes unit, integrasi dan fungsional.

Apa yang saya cari adalah cara yang pasti untuk menulis tes yang dapat dikelola dan dikelola.

Kerangka kerja

Chuck Conway
sumber
Ini jauh lebih cocok untuk Programmer.StackExchange, IMO ...
IAbstract
BDD
Robbie Dee

Jawaban:

21

Jangan menganggapnya sebagai "tes unit rusak", karena tidak.

Itu adalah spesifikasi, yang tidak lagi didukung oleh program Anda.

Jangan menganggapnya sebagai "memperbaiki tes", tetapi sebagai "mendefinisikan persyaratan baru".

Tes harus menentukan aplikasi Anda terlebih dahulu, bukan sebaliknya.

Anda tidak bisa mengatakan Anda memiliki implementasi yang berfungsi sampai Anda tahu itu berhasil. Anda tidak bisa mengatakan itu berfungsi sampai Anda mengujinya.

Beberapa catatan lain yang mungkin memandu Anda:

  1. Tes dan kelas yang diuji harus pendek dan sederhana . Setiap tes hanya harus memeriksa bagian fungsionalitas yang kohesif. Artinya, tidak peduli tentang hal-hal yang sudah diperiksa oleh tes lain.
  2. Tes, dan objek Anda, harus digabungkan secara longgar, dengan cara bahwa jika Anda mengubah objek, Anda hanya mengubah grafik ketergantungannya ke bawah, dan objek lain yang menggunakan objek itu tidak terpengaruh olehnya.
  3. Anda mungkin membuat dan menguji hal-hal yang salah . Apakah objek Anda dibuat untuk antarmuka yang mudah, atau implementasi yang mudah? Jika ini kasus terakhir, Anda akan menemukan diri Anda mengubah banyak kode yang menggunakan antarmuka implementasi lama.
  4. Dalam kasus terbaik, patuhi prinsip Tanggung Jawab Tunggal. Dalam kasus yang lebih buruk, patuhi prinsip Segregasi Antarmuka. Lihat Prinsip SOLID .
Yam Marcovic
sumber
5
+1 untukDon't think of it as "fixing the tests", but as "defining new requirements".
StuperUser
2
+1 Tes harus menentukan aplikasi Anda terlebih dahulu, bukan sebaliknya
treecoder
11

Apa yang Anda gambarkan mungkin sebenarnya bukan hal yang buruk, tetapi petunjuk untuk masalah yang lebih dalam yang ditemukan oleh tes Anda

Saat sistem berubah, kami mendapati diri kami menghabiskan lebih banyak waktu memperbaiki tes yang rusak. Kami memiliki tes unit, integrasi dan fungsional.

Jika Anda dapat mengubah kode Anda, dan tes Anda tidak akan pecah, itu akan mencurigakan bagi saya. Perbedaan antara perubahan yang sah dan bug hanya fakta bahwa itu diminta, apa yang diminta (diasumsikan TDD) ditentukan oleh pengujian Anda.

data telah dikodekan dengan keras.

Data kode yang sulit dalam tes adalah hal yang baik. Tes berfungsi sebagai pemalsuan, bukan sebagai bukti. Jika terlalu banyak perhitungan, tes Anda mungkin berupa tautologi. Sebagai contoh:

assert sum([1,2,3]) == 6
assert sum([1,2,3]) == 1 + 2 + 3
assert sum([1,2,3]) == reduce(operator.add, [1,2,3])

Semakin tinggi abstraksi, semakin dekat Anda dengan algoritma, dan dengan itu, semakin dekat untuk membandingkan implementasi acutal dengan dirinya sendiri.

penggunaan kembali kode yang sangat sedikit

Penggunaan kembali kode yang terbaik dalam tes adalah imho 'Cek', seperti pada jUnits assertThat, karena mereka menjaga tes sederhana. Selain itu, jika pengujian dapat dilakukan refactored untuk membagikan kode, kode sebenarnya yang diuji kemungkinan juga bisa , sehingga mengurangi tes menjadi tes yang menguji basis refactored.

keppla
sumber
Saya ingin tahu di mana downvoter tidak setuju.
keppla
keppla - Saya bukan downvoter, tetapi umumnya, tergantung di mana saya berada di dalam model, saya lebih suka menguji interaksi objek daripada menguji data pada tingkat unit. Menguji data bekerja lebih baik pada tingkat integrasi.
Ritch Melton
@ keppla Saya memiliki kelas yang merutekan pesanan ke saluran yang berbeda jika total itemnya mengandung item terbatas tertentu. Saya membuat pesanan palsu isilah dengan 4 item, dua di antaranya adalah yang dibatasi. Sejauh barang-barang terlarang ditambahkan, tes ini unik. Tetapi langkah-langkah untuk membuat pesanan palsu, dan menambahkan dua item biasa adalah pengaturan yang sama yang digunakan tes lain yang menguji alur kerja item yang tidak dibatasi. Dalam hal ini bersama dengan item jika pesanan perlu memiliki pengaturan data pelanggan dan pengaturan alamat dll bukankah ini kasus baik menggunakan kembali bantuan setup. Mengapa hanya menegaskan penggunaan kembali?
Asif Shiraz
6

Saya juga punya masalah ini. Pendekatan saya yang lebih baik adalah sebagai berikut:

  1. Jangan menulis unit test kecuali itu satu-satunya cara yang baik untuk menguji sesuatu.

    Saya sepenuhnya siap untuk mengakui bahwa unit test memiliki biaya diagnosis dan waktu perbaikan terendah. Ini membuat mereka alat yang berharga. Masalahnya adalah, dengan jarak tempuh Anda yang jelas-mungkin-bervariasi, bahwa tes unit seringkali terlalu kecil untuk pantas biaya mempertahankan massa kode. Saya menulis contoh di bagian bawah, lihatlah.

  2. Gunakan pernyataan di mana pun mereka setara dengan tes unit untuk komponen itu. Pernyataan memiliki properti bagus yang selalu diverifikasi di seluruh debug build. Jadi, alih-alih menguji batasan kelas "Karyawan" di unit tes terpisah, Anda secara efektif menguji kelas Karyawan melalui setiap kasus uji dalam sistem. Pernyataan juga memiliki properti bagus yang tidak menambah massa kode sebanyak tes unit (yang akhirnya membutuhkan perancah / mengejek / apa pun).

    Sebelum seseorang membunuh saya: produksi tidak boleh macet karena asersi. Sebagai gantinya, mereka harus masuk pada level "Kesalahan".

    Sebagai peringatan bagi seseorang yang belum memikirkannya, jangan nyatakan apa pun tentang input pengguna atau jaringan. Ini adalah kesalahan besar ™.

    Di basis kode terbaru saya, saya telah dengan bijaksana menghapus unit test di mana pun saya melihat peluang yang jelas untuk pernyataan. Ini secara signifikan menurunkan biaya perawatan secara keseluruhan dan membuat saya menjadi orang yang jauh lebih bahagia.

  3. Pilih pengujian sistem / integrasi, implementasikan untuk semua aliran utama dan pengalaman pengguna Anda. Kasus sudut mungkin tidak perlu ada di sini. Tes sistem memverifikasi perilaku di sisi pengguna dengan menjalankan semua komponen. Karena itu, tes sistem tentu lebih lambat, jadi tulis yang penting (tidak lebih, tidak kurang) dan Anda akan menangkap masalah yang paling penting. Tes sistem memiliki overhead perawatan yang sangat rendah.

    Penting untuk diingat bahwa, karena Anda menggunakan pernyataan, setiap pengujian sistem akan menjalankan beberapa ratus "unit test" secara bersamaan. Anda juga agak yakin bahwa yang paling penting dijalankan berulang kali.

  4. Tulis API yang kuat yang dapat diuji secara fungsional. Tes fungsional canggung dan (mari kita hadapi) agak tidak berarti jika API Anda membuatnya terlalu sulit untuk memverifikasi komponen yang berfungsi sendiri. Desain API yang baik a) membuat langkah-langkah pengujian menjadi mudah dan b) menghasilkan pernyataan yang jelas dan berharga.

    Pengujian fungsional adalah hal yang paling sulit untuk dilakukan dengan benar, terutama ketika Anda memiliki komponen yang berkomunikasi satu-ke-banyak atau (bahkan lebih buruk, oh tuhan) banyak-ke-banyak di seluruh hambatan proses. Semakin banyak input dan output yang terpasang pada satu komponen, semakin sulit pengujian fungsionalnya, karena Anda harus mengisolasi salah satunya untuk benar-benar menguji fungsionalitasnya.


Pada masalah "jangan tulis unit test," saya akan memberikan contoh:

TEST(exception_thrown_on_null)
{
    InternalDataStructureType sink;
    ASSERT_THROWS(sink.consumeFrom(NULL), std::logic_error);
    try {
        sink.consumeFrom(NULL);
    } catch (const std::logic_error& e) {
        ASSERT(e.what() == "You must not pass NULL as a parameter!");
    }
}

Penulis tes ini telah menambahkan tujuh baris yang tidak berkontribusi sama sekali pada verifikasi produk akhir. Pengguna seharusnya tidak pernah melihat hal ini terjadi, baik karena a) tidak seorang pun boleh lewat NULL di sana (jadi tuliskan pernyataan), lalu) atau b) kasing NULL harus menyebabkan beberapa perilaku yang berbeda. Jika case (b), tulis tes yang benar-benar memverifikasi perilaku itu.

Filosofi saya adalah bahwa kita tidak boleh menguji artefak implementasi. Kami hanya harus menguji apa pun yang dapat dianggap sebagai hasil aktual. Kalau tidak, tidak ada cara untuk menghindari penulisan dua kali massa dasar kode antara tes unit (yang memaksa implementasi tertentu) dan implementasi itu sendiri.

Penting untuk dicatat, di sini, bahwa ada kandidat yang baik untuk tes unit. Bahkan, bahkan ada beberapa situasi di mana tes unit adalah satu-satunya cara yang memadai untuk memverifikasi sesuatu dan di mana itu bernilai tinggi untuk menulis dan mempertahankan tes tersebut. Dari bagian atas kepala saya, daftar ini termasuk algoritma nontrivial, wadah data yang terbuka di API, dan kode yang sangat dioptimalkan yang muncul "rumit" (alias "orang berikutnya mungkin akan mengacaukannya.").

Saran khusus saya kepada Anda, maka: Mulai hapus pengujian unit secara bijaksana saat mereka rusak, tanyakan pada diri Anda pertanyaan, "apakah ini output, atau saya membuang-buang kode?" Anda mungkin akan berhasil mengurangi jumlah hal yang menghabiskan waktu Anda.

Andres Jaan Tack
sumber
3
Memilih tes sistem / integrasi - Ini sangat buruk. Sistem Anda sampai pada titik di mana ia menggunakan tes (lambat!) Ini untuk menguji hal-hal yang dapat ditangkap dengan cepat di tingkat unit dan dibutuhkan berjam-jam untuk menjalankannya karena Anda memiliki begitu banyak tes yang serupa dan lambat.
Ritch Melton
1
@RitchMelton Sepenuhnya terpisah dari diskusi, sepertinya Anda memerlukan server CI baru. CI seharusnya tidak berperilaku seperti itu.
Andres Jaan Tack
1
Program mogok (yang adalah apa yang dilakukan asersi) tidak boleh mematikan test runner (CI) Anda. Itu sebabnya Anda memiliki pelari ujian; jadi sesuatu dapat mendeteksi dan melaporkan kegagalan tersebut.
Andres Jaan Tack
1
Pernyataan gaya 'Tegas' hanya debug yang saya kenal (bukan tes pernyataan) memunculkan dialog yang menggantung CI karena sedang menunggu interaksi pengembang.
Ritch Melton
1
Ah, itu akan menjelaskan banyak hal tentang ketidaksepakatan kita. :) Saya mengacu pada pernyataan gaya-C. Saya baru saja memperhatikan bahwa ini adalah pertanyaan .NET. cplusplus.com/reference/clibrary/cassert/assert
Andres Jaan Tack
5

Sepertinya pengujian unit Anda bekerja seperti pesona. Ini adalah yang baik hal yang begitu rapuh untuk perubahan, karena itu semacam seluruh titik. Perubahan kecil dalam tes pemutusan kode sehingga Anda dapat menghilangkan kemungkinan kesalahan di seluruh program Anda.

Namun, perlu diingat bahwa Anda hanya benar-benar perlu menguji kondisi yang akan membuat metode Anda gagal atau memberikan hasil yang tidak terduga. Ini akan membuat pengujian unit Anda lebih rentan untuk "pecah" jika ada masalah asli daripada hal-hal sepele.

Meskipun menurut saya Anda sangat mendesain ulang program. Dalam hal ini, lakukan apa pun yang Anda perlukan dan hapus tes lama dan ganti dengan yang baru sesudahnya. Perbaikan unit test hanya bermanfaat jika Anda tidak memperbaikinya karena perubahan radikal dalam program Anda. Kalau tidak, Anda mungkin mendapati bahwa Anda mendedikasikan terlalu banyak waktu untuk menulis ulang tes agar dapat diterapkan di bagian kode program yang baru Anda tulis.

Neil
sumber
3

Saya yakin orang lain akan memiliki banyak masukan, tetapi dalam pengalaman saya, ini adalah beberapa hal penting yang akan membantu Anda:

  1. Gunakan pabrik objek pengujian untuk membangun struktur data input, sehingga Anda tidak perlu menduplikasi logika itu. Mungkin melihat ke perpustakaan penolong seperti AutoFixture untuk mengurangi kode yang diperlukan untuk pengaturan tes.
  2. Untuk setiap kelas tes, sentralisasi pembuatan SUT, sehingga akan mudah untuk berubah ketika segala sesuatunya di-refactored.
  3. Ingat, kode pengujian itu sama pentingnya dengan kode produksi. Itu juga harus di-refactored, jika Anda mendapati diri Anda mengulangi, jika kode itu terasa tidak dapat dipertahankan, dll, dll.
driis
sumber
Semakin banyak Anda menggunakan kembali kode di seluruh tes, semakin rapuh mereka, karena sekarang mengubah satu tes dapat merusak yang lain. Itu mungkin biaya yang masuk akal, sebagai imbalan untuk pemeliharaan - saya tidak masuk ke argumen itu di sini - tetapi untuk berpendapat bahwa poin 1 dan 2 membuat tes kurang rapuh (yang menjadi pertanyaan) adalah salah.
pdr
@driis - Benar, kode tes memiliki idiom yang berbeda dari kode yang sedang berjalan. Menyembunyikan sesuatu dengan refactoring kode 'umum' dan menggunakan hal-hal seperti wadah IoC hanya menutupi masalah desain yang diekspos oleh tes Anda.
Ritch Melton
Sementara titik @pdr membuat kemungkinan valid untuk unit test, saya berpendapat bahwa untuk tes integrasi / sistem, mungkin berguna untuk berpikir dalam hal "menyiapkan aplikasi untuk tugas X". Itu mungkin melibatkan menavigasi ke tempat yang tepat, mengatur pengaturan run-time tertentu, membuka file data, dan sebagainya. Jika beberapa tes integrasi dimulai di tempat yang sama, refactoring kode itu untuk digunakan kembali di beberapa tes mungkin bukan hal yang buruk jika Anda memahami risiko dan keterbatasan pendekatan semacam itu.
CVn
2

Tangani tes seperti Anda melakukannya dengan kode sumber.

Kontrol versi, rilis pos pemeriksaan, pelacakan masalah, "kepemilikan fitur", estimasi perencanaan dan upaya, dll. Pernahkah ada yang melakukannya - Saya pikir ini adalah cara paling efisien untuk menangani masalah seperti yang Anda jelaskan.

agas
sumber
1

Anda pasti harus melihat pola tes XUnit Gerard Meszaros . Ini memiliki bagian yang hebat dengan banyak resep untuk menggunakan kembali kode tes Anda dan menghindari duplikasi.

Jika tes Anda rapuh, bisa juga Anda tidak cukup menggunakan tes ganda. Terutama, jika Anda membuat kembali seluruh grafik objek pada awal setiap unit test, bagian Arrange dalam tes Anda mungkin menjadi terlalu besar dan Anda mungkin sering menemukan diri Anda dalam situasi di mana Anda harus menulis ulang bagian Arrange dalam sejumlah besar tes hanya karena salah satu kelas yang paling sering Anda gunakan telah berubah. Mengolok-olok dan bertopik dapat membantu Anda di sini dengan mengurangi jumlah objek yang Anda harus rehydrate untuk memiliki konteks pengujian yang relevan.

Mengambil detail yang tidak penting dari pengaturan pengujian Anda melalui mengejek dan bertopik dan menerapkan pola pengujian untuk menggunakan kembali kode harus mengurangi kerapuhan mereka secara signifikan.

guillaume31
sumber