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.
sumber
Jawaban:
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.
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.
sumber
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.
sumber
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.
sumber