Tes unit rapuh karena kebutuhan untuk mengejek yang berlebihan

21

Saya telah berjuang dengan masalah yang semakin menjengkelkan terkait tes unit kami yang kami terapkan di tim saya. Kami berusaha menambahkan unit test ke dalam kode legacy yang tidak dirancang dengan baik dan sementara kami belum mengalami kesulitan dengan penambahan tes yang sebenarnya, kami mulai bergumul dengan bagaimana hasil tes tersebut.

Sebagai contoh masalah, katakanlah Anda memiliki metode yang memanggil 5 metode lain sebagai bagian dari pelaksanaannya. Tes untuk metode ini mungkin untuk mengkonfirmasi bahwa suatu perilaku terjadi sebagai hasil dari salah satu dari 5 metode lain yang dipanggil. Jadi, karena unit test harus gagal karena satu alasan dan hanya satu alasan, Anda ingin menghilangkan masalah potensial yang disebabkan oleh memanggil 4 metode lainnya dan mengejeknya. Besar! Tes unit dijalankan, metode yang diejek diabaikan (dan perilakunya dapat dikonfirmasi sebagai bagian dari pengujian unit lainnya), dan verifikasi berfungsi.

Tapi ada masalah baru - unit test memiliki pengetahuan mendalam tentang bagaimana Anda mengkonfirmasi bahwa perilaku dan setiap tanda tangan berubah ke salah satu dari 4 metode lain di masa depan, atau metode baru yang perlu ditambahkan ke 'metode induk', akan mengakibatkan harus mengubah unit test untuk menghindari kemungkinan kegagalan.

Tentu saja masalahnya dapat dikurangi sedikit dengan hanya memiliki lebih banyak metode menyelesaikan perilaku lebih sedikit tapi saya berharap mungkin ada solusi yang lebih elegan yang tersedia.

Berikut ini adalah contoh unit test yang menangkap masalah.

Sebagai catatan singkat, 'MergeTests' adalah kelas pengujian unit yang mewarisi dari kelas yang kami uji dan menimpa perilaku yang diperlukan. Ini adalah 'pola' yang kami gunakan dalam pengujian kami untuk memungkinkan kami mengesampingkan panggilan ke kelas / dependensi eksternal.

[TestMethod]
public void VerifyMergeStopsSpinner()
{
    var mockViewModel = new Mock<MergeTests> { CallBase = true };
    var mockMergeInfo = new MergeInfo(Mock.Of<IClaim>(), Mock.Of<IClaim>(), It.IsAny<bool>());

    mockViewModel.Setup(m => m.ClaimView).Returns(Mock.Of<IClaimView>);
    mockViewModel.Setup(
        m =>
        m.TryMergeClaims(It.IsAny<Func<bool>>(), It.IsAny<IClaim>(), It.IsAny<IClaim>(), It.IsAny<bool>(),
                         It.IsAny<bool>()));
    mockViewModel.Setup(m => m.GetSourceClaimAndTargetClaimByMergeState(It.IsAny<MergeState>())).Returns(mockMergeInfo);
    mockViewModel.Setup(m => m.SwitchToOverviewTab());
    mockViewModel.Setup(m => m.IncrementSaveRequiredNotification());
    mockViewModel.Setup(m => m.OnValidateAndSaveAll(It.IsAny<object>()));
    mockViewModel.Setup(m => m.ProcessPendingActions(It.IsAny<string>()));

    mockViewModel.Object.OnMerge(It.IsAny<MergeState>());    

    mockViewModel.Verify(mvm => mvm.StopSpinner(), Times.Once());
}

Bagaimana Anda semua berurusan dengan ini atau tidak ada cara 'sederhana' yang hebat untuk menanganinya?

Pembaruan - Saya menghargai umpan balik semua orang. Sayangnya, dan itu tidak mengherankan, sepertinya tidak ada solusi, pola, atau praktik yang dapat diikuti seseorang dalam pengujian unit jika kode yang diuji buruk. Saya menandai jawaban yang paling tepat menangkap kebenaran sederhana ini.

PremiumTier
sumber
Wow, saya hanya melihat setup tiruan, tidak ada instantiation SUT atau apa pun, apakah Anda menguji implementasi aktual di sini? Siapa yang seharusnya memanggil StopSpinner? OnMerge? Anda harus mengejek setiap dependensi yang mungkin dipanggil tetapi tidak dengan sendirinya ..
Joppe
Agak sulit dilihat, tetapi Mock <MergeTests> adalah SUT. Kami menetapkan bendera CallBase untuk memastikan metode 'OnMerge' dijalankan pada objek aktual, tetapi mengejek metode yang disebut oleh 'OnMerge' yang dapat menyebabkan pengujian gagal karena masalah ketergantungan dll. Tujuan dari tes ini adalah baris terakhir - untuk memverifikasi kami menghentikan pemintal dalam kasus ini.
PremiumTier
MergeTests terdengar seperti kelas instrumen lainnya, bukan sesuatu yang hidup dalam produksi karenanya kebingungan.
Joppe
1
Sama sekali terlepas dari masalah Anda yang lain, tampaknya salah bagi saya bahwa SUT Anda adalah Mock <MergeTests>. Mengapa Anda menguji Mock? Mengapa Anda tidak menguji kelas MergeTests sendiri?
Eric King

Jawaban:

18
  1. Perbaiki kode agar dirancang dengan lebih baik. Jika tes Anda memiliki masalah ini, maka kode Anda akan memiliki masalah yang lebih buruk ketika Anda mencoba mengubah sesuatu.

  2. Jika Anda tidak bisa, maka mungkin Anda harus kurang ideal. Uji terhadap kondisi sebelum dan sesudah metode. Siapa yang peduli jika Anda menggunakan 5 metode lainnya? Mereka mungkin memiliki tes unit mereka sendiri memperjelas (er) apa yang menyebabkan kegagalan ketika tes gagal.

"unit test seharusnya hanya memiliki satu alasan untuk gagal" adalah pedoman yang baik, tetapi menurut pengalaman saya, tidak praktis. Tes yang sulit ditulis tidak ditulis. Tes rapuh tidak bisa dipercaya.

Telastyn
sumber
Saya setuju sepenuhnya dengan memperbaiki desain kode tetapi dalam dunia pengembangan yang kurang ideal untuk perusahaan besar dengan jadwal yang ketat, mungkin sulit untuk membuat 'pembayaran' hutang teknis yang dikeluarkan oleh tim sebelumnya atau keputusan buruk sama sekali. sekali. Untuk poin kedua Anda banyak mengejek bukan hanya karena kami ingin tes hanya gagal karena satu alasan - itu karena kode yang dieksekusi tidak dapat diizinkan untuk dieksekusi tanpa juga pertama-tama menangani sejumlah besar dependensi yang dibuat di dalam kode itu . Maaf untuk memindahkan posting tujuan yang satu itu.
PremiumTier
Jika desain yang lebih baik tidak realistis saya setuju dengan 'Siapa yang peduli jika Anda menggunakan 5 metode lainnya?' Verifikasi metode melakukan fungsi yang diperlukan, bukan bagaimana melakukan itu.
Kwebble
@Kwebble - Dipahami, namun tujuan dari pertanyaan ini adalah untuk menentukan apakah ada cara sederhana untuk memverifikasi perilaku untuk suatu metode ketika Anda juga harus mencemooh perilaku lain yang disebut dalam metode untuk menjalankan tes sama sekali. Saya ingin menghapus 'bagaimana', tetapi saya tidak tahu bagaimana :)
PremiumTier
Tidak ada peluru perak ajaib. Tidak ada "cara sederhana" untuk menguji kode yang buruk. Entah kode yang diuji harus direaktor ulang, atau kode uji itu sendiri, juga akan menjadi buruk. Entah tes akan menjadi miskin karena akan terlalu spesifik untuk rincian internal, seperti yang Anda berjalan ke dalam, atau sebagai btilly disarankan, Anda dapat menjalankan tes terhadap lingkungan kerja, tetapi kemudian tes akan jauh lebih lambat dan lebih kompleks. Either way, tes akan lebih sulit untuk menulis, lebih sulit untuk mempertahankan, dan cenderung negatif palsu.
Steven Doggart
8

Memecah metode besar menjadi metode kecil yang lebih fokus jelas merupakan praktik terbaik. Anda melihatnya sebagai rasa sakit dalam memverifikasi perilaku unit test, tetapi Anda juga mengalami rasa sakit dengan cara lain.

Yang mengatakan, itu adalah bid'ah tetapi saya pribadi penggemar menciptakan lingkungan pengujian sementara yang realistis. Artinya, daripada mengejek segala sesuatu yang tersembunyi di dalam metode-metode lain, pastikan ada yang mudah untuk mengatur lingkungan sementara (lengkap dengan database dan skema pribadi - SQLite dapat membantu di sini) yang memungkinkan Anda menjalankan semua hal itu. Tanggung jawab untuk mengetahui cara membangun / menghancurkan lingkungan uji hidup dengan kode yang mensyaratkan itu, sehingga ketika berubah, Anda tidak perlu mengubah semua kode uji unit yang bergantung pada keberadaannya.

Tapi aku catatan bahwa ini adalah bid'ah di bagian saya. Orang-orang yang sangat menyukai pengujian unit menganjurkan tes unit "murni" dan menyebut apa yang saya gambarkan sebagai "tes integrasi". Saya pribadi tidak khawatir tentang perbedaan itu.

btilly
sumber
3

Saya akan mempertimbangkan pelonggaran pada mock dan hanya merumuskan tes yang mungkin mencakup metode yang disebutnya.

Jangan menguji bagaimana , menguji apa . Ini hasil yang penting, termasuk sub-metode jika perlu.

Dari sudut lain Anda dapat merumuskan tes, membuatnya lulus dengan satu metode besar, refactor dan berakhir dengan pohon metode setelah refactoring. Anda tidak perlu menguji masing-masing dari mereka secara terpisah. Ini adalah hasil akhir yang diperhitungkan.

Jika sub metode menyulitkan untuk menguji beberapa aspek, pertimbangkan membaginya ke kelas yang terpisah sehingga Anda dapat mengejek mereka di sana dengan lebih bersih tanpa kelas Anda yang sedang diuji sangat padat / terkekang. Agak sulit untuk mengatakan apakah Anda benar-benar menguji implementasi konkret dalam contoh pengujian Anda.

Joppe
sumber
Masalahnya adalah kita harus mengejek 'bagaimana' untuk menguji 'apa'. Ini adalah batasan yang diberlakukan oleh desain kode. Saya tentu tidak ingin 'mengejek' bagaimana karena itulah yang membuat tes rapuh.
PremiumTier
Melihat nama metode, saya pikir kelas Anda yang diuji hanya mengambil terlalu banyak tanggung jawab. Bacalah prinsip tanggung jawab tunggal. Meminjam dari MVC mungkin sedikit membantu, kelas Anda tampaknya menangani masalah UI, infrastruktur, dan bisnis.
Joppe
Ya :( Itu akan menjadi kode warisan yang dirancang dengan buruk yang saya maksudkan. Kami sedang mengerjakan desain ulang dan refactor tapi kami merasa akan lebih baik untuk menempatkan sumber yang diuji terlebih dahulu.
PremiumTier