Pengujian Unit .NET: Belanjakan di Muka untuk Menyimpan Nanti

Diterbitkan: 2022-03-11

Seringkali ada banyak kebingungan dan keraguan mengenai pengujian unit saat mendiskusikannya dengan pemangku kepentingan dan klien. Pengujian unit terkadang terdengar seperti flossing pada seorang anak, “Saya sudah menyikat gigi, mengapa saya harus melakukan ini?”

Menyarankan pengujian unit sering kali terdengar seperti pengeluaran yang tidak perlu bagi orang-orang yang menganggap metode pengujian mereka dan pengujian penerimaan pengguna cukup kuat.

Tetapi Tes Unit adalah alat yang sangat kuat dan lebih sederhana dari yang Anda kira. Pada artikel ini, kita akan melihat pengujian unit dan alat apa yang tersedia di DotNet seperti Microsoft.VisualStudio.TestTools dan Moq .

Kami akan mencoba membangun perpustakaan kelas sederhana yang akan menghitung suku ke-n dalam deret Fibonacci. Untuk melakukan itu, kita akan ingin membuat kelas untuk menghitung barisan Fibonacci yang bergantung pada kelas matematika khusus yang menambahkan angka bersama-sama. Kemudian, kita dapat menggunakan .NET Testing Framework untuk memastikan program kita berjalan seperti yang diharapkan.

Apa itu Pengujian Unit?

Pengujian unit memecah program menjadi kode terkecil, biasanya tingkat fungsi, dan memastikan bahwa fungsi mengembalikan nilai yang diharapkan. Dengan menggunakan kerangka pengujian unit, pengujian unit menjadi entitas terpisah yang kemudian dapat menjalankan pengujian otomatis pada program saat sedang dibangun.

 [TestClass] public class FibonacciTests { [TestMethod] //Check the first value we calculate public void Fibonacci_GetNthTerm_Input2_AssertResult1() { //Arrange int n = 2; //setup Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>(); mockMath .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>())) .Returns((int x, int y) => x + y); UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object); //Act int result = fibonacci.GetNthTerm(n); //Assert Assert.AreEqual(result, 1); } }

Pengujian unit sederhana menggunakan pengujian metodologi Arrange, Act, Assert bahwa perpustakaan matematika kami dapat dengan benar menambahkan 2 + 2.

Setelah pengujian unit disiapkan, jika perubahan dibuat pada kode, untuk memperhitungkan kondisi tambahan yang tidak diketahui saat program pertama kali dikembangkan, misalnya, pengujian unit akan menunjukkan apakah semua kasus cocok dengan nilai yang diharapkan keluaran oleh fungsi.

Pengujian unit bukanlah pengujian integrasi. Ini bukan pengujian ujung ke ujung. Meskipun keduanya merupakan metodologi yang kuat, keduanya harus bekerja sama dengan pengujian unit–bukan sebagai pengganti.

Manfaat dan Tujuan Pengujian Unit

Manfaat paling sulit dari pengujian unit untuk dipahami, tetapi yang paling penting, adalah kemampuan untuk menguji ulang kode yang diubah dengan cepat. Alasan mengapa hal itu bisa sangat sulit untuk dipahami adalah karena begitu banyak pengembang berpikir, "Saya tidak akan pernah menyentuh fungsi itu lagi," atau "Saya akan mengujinya kembali setelah selesai." Dan pemangku kepentingan berpikir dalam kerangka, "Jika bagian itu sudah ditulis, mengapa saya perlu mengujinya kembali?"

Sebagai seseorang yang berada di kedua sisi spektrum pembangunan, saya telah mengatakan kedua hal ini. Pengembang di dalam diri saya tahu mengapa kami harus mengujinya kembali.

Perubahan yang kita buat sehari-hari bisa berdampak besar. Sebagai contoh:

  • Apakah sakelar Anda dengan benar memperhitungkan nilai baru yang Anda masukkan?
  • Apakah Anda tahu berapa kali Anda menggunakan sakelar itu?
  • Apakah Anda memperhitungkan dengan benar perbandingan string yang tidak peka huruf besar-kecil?
  • Apakah Anda memeriksa nol dengan tepat?
  • Apakah pengecualian lemparan ditangani seperti yang Anda harapkan?

Pengujian unit mengambil pertanyaan-pertanyaan ini dan mengabadikannya dalam kode dan proses untuk memastikan pertanyaan-pertanyaan ini selalu dijawab. Pengujian unit dapat dijalankan sebelum pembangunan untuk memastikan bahwa Anda belum memperkenalkan bug baru. Karena pengujian unit dirancang untuk menjadi atomik, pengujian tersebut dijalankan dengan sangat cepat, biasanya kurang dari 10 milidetik per pengujian. Bahkan dalam aplikasi yang sangat besar, rangkaian pengujian lengkap dapat dilakukan dalam waktu kurang dari satu jam. Bisakah proses UAT Anda cocok dengan itu?

Contoh konvensi penamaan yang disiapkan untuk dengan mudah mencari kelas atau metode di dalam kelas yang akan diuji.
Selain Fibonacci_GetNthTerm_Input2_AssertResult1 yang merupakan proses pertama dan mencakup waktu penyiapan, semua pengujian unit berjalan di bawah 5 md. Konvensi penamaan saya di sini diatur untuk dengan mudah mencari kelas atau metode di dalam kelas yang ingin saya uji

Sebagai pengembang, mungkin ini terdengar seperti lebih banyak pekerjaan untuk Anda. Ya, Anda mendapatkan ketenangan pikiran bahwa kode yang Anda rilis bagus. Tetapi pengujian unit juga menawarkan Anda kesempatan untuk melihat di mana desain Anda lemah. Apakah Anda menulis tes unit yang sama untuk dua potong kode? Haruskah mereka berada di satu bagian kode saja?

Membuat kode Anda menjadi unit yang dapat diuji itu sendiri adalah cara bagi Anda untuk meningkatkan desain Anda. Dan untuk sebagian besar pengembang yang belum pernah menguji unit, atau tidak mengambil banyak waktu untuk mempertimbangkan desain sebelum pengkodean, Anda dapat menyadari seberapa besar peningkatan desain Anda dengan membuatnya siap untuk pengujian unit.

Apakah Unit Kode Anda Dapat Diuji?

Selain KERING, kami juga memiliki pertimbangan lain.

Apakah Metode atau Fungsi Anda Mencoba Melakukan Terlalu Banyak?

Jika Anda perlu menulis pengujian unit yang terlalu rumit yang berjalan lebih lama dari yang Anda harapkan, metode Anda mungkin terlalu rumit dan lebih cocok sebagai beberapa metode.

Apakah Anda Memanfaatkan Injeksi Ketergantungan dengan Benar?

Jika metode Anda yang sedang diuji memerlukan kelas atau fungsi lain, kami menyebutnya dependensi. Dalam pengujian unit, kami tidak peduli apa yang dilakukan ketergantungan di bawah tenda; untuk tujuan metode yang diuji, ini adalah kotak hitam. Ketergantungan memiliki serangkaian tes unit sendiri yang akan menentukan apakah perilakunya berfungsi dengan benar.

Sebagai penguji, Anda ingin mensimulasikan ketergantungan itu dan memberi tahu nilai apa yang akan dikembalikan dalam contoh tertentu. Ini akan memberi Anda kontrol lebih besar atas kasus pengujian Anda. Untuk melakukan ini, Anda perlu menyuntikkan versi dummy (atau seperti yang akan kita lihat nanti, tiruan) dari ketergantungan itu.

Apakah Komponen Anda Berinteraksi Satu Sama Lain Seperti yang Anda Harapkan?

Setelah Anda menyelesaikan dependensi dan injeksi ketergantungan Anda, Anda mungkin menemukan bahwa Anda telah memperkenalkan dependensi siklik dalam kode Anda. Jika Kelas A bergantung pada Kelas B, yang pada gilirannya bergantung pada Kelas A, Anda harus mempertimbangkan kembali desain Anda.

Keindahan Injeksi Ketergantungan

Mari kita perhatikan contoh Fibonacci kita. Bos Anda memberi tahu Anda bahwa mereka memiliki kelas baru yang lebih efisien dan akurat daripada operator penambahan saat ini yang tersedia di C#.

Meskipun contoh khusus ini sangat tidak mungkin di dunia nyata, kami melihat contoh analog di komponen lain, seperti otentikasi, pemetaan objek, dan hampir semua proses algoritmik. Untuk tujuan artikel ini, anggap saja fungsi add baru klien Anda adalah yang terbaru dan terhebat sejak komputer ditemukan.

Dengan demikian, bos Anda memberi Anda perpustakaan kotak hitam dengan satu kelas Math , dan di kelas itu, satu fungsi Add . Tugas Anda dalam menerapkan kalkulator Fibonacci kemungkinan akan terlihat seperti ini:

 public int GetNthTerm(int n) { Math math = new Math(); int nMinusTwoTerm = 1; int nMinusOneTerm = 1; int newTerm = 0; for (int i = 2; i < n; i++) { newTerm = math.Add(nMinusOneTerm, nMinusTwoTerm); nMinusTwoTerm = nMinusOneTerm; nMinusOneTerm = newTerm; } return newTerm; }

Ini tidak menghebohkan. Anda membuat instance kelas Math baru dan menggunakannya untuk menambahkan dua istilah sebelumnya untuk mendapatkan yang berikutnya. Anda menjalankan metode ini melalui tes baterai normal Anda, menghitung hingga 100 suku, menghitung suku ke 1000, suku ke 10.000, dan seterusnya hingga Anda merasa puas bahwa metodologi Anda berfungsi dengan baik. Kemudian suatu saat di masa depan, pengguna mengeluh bahwa istilah ke-501 tidak berfungsi seperti yang diharapkan. Anda menghabiskan malam melihat-lihat kode Anda dan mencoba mencari tahu mengapa kasus sudut ini tidak berfungsi. Anda mulai curiga bahwa kelas Math terbaru dan terhebat tidak sebagus yang dipikirkan bos Anda. Tapi itu adalah kotak hitam dan Anda tidak dapat benar-benar membuktikannya—Anda menemui jalan buntu secara internal.

Masalahnya di sini adalah bahwa ketergantungan Math tidak dimasukkan ke dalam kalkulator Fibonacci Anda. Oleh karena itu, dalam pengujian Anda, Anda selalu mengandalkan hasil yang ada, belum teruji, dan tidak diketahui dari Math untuk menguji Fibonacci. Jika ada masalah dengan Math , maka Fibonacci akan selalu salah (tanpa mengkodekan kasus khusus untuk suku ke-501).

Ide untuk memperbaiki masalah ini adalah dengan menyuntikkan kelas Math ke dalam kalkulator Fibonacci Anda. Tetapi lebih baik lagi, adalah membuat antarmuka untuk kelas Math yang mendefinisikan metode publik (dalam kasus kami, Add ) dan mengimplementasikan antarmuka pada kelas Math kami.

 public interface IMath { int Add(int x, int y); } public class Math : IMath { public int Add(int x, int y) { //super secret implementation here } } }

Daripada menyuntikkan kelas Math ke Fibonacci, kita bisa menyuntikkan antarmuka IMath ke Fibonacci. Manfaatnya di sini adalah kita bisa mendefinisikan kelas OurMath kita sendiri yang kita tahu akurat dan menguji kalkulator kita terhadap itu. Lebih baik lagi, dengan menggunakan Moq kita dapat dengan mudah mendefinisikan apa yang dikembalikan oleh Math.Add . Kita dapat menentukan sejumlah penjumlahan atau kita dapat memberitahu Math.Add untuk mengembalikan x + y.

 private IMath _math; public Fibonacci(IMath math) { _math = math; }

Suntikkan antarmuka IMath ke dalam kelas Fibonacci

 //setup Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>(); mockMath .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>())) .Returns((int x, int y) => x + y);

Menggunakan Moq untuk menentukan pengembalian Math.Add .

Sekarang kami memiliki metode yang dicoba dan benar (baik, jika operator + itu salah dalam C# kami memiliki masalah yang lebih besar) untuk menambahkan dua angka. Dengan menggunakan IMath , kami dapat mengkodekan unit test untuk istilah ke-501 kami dan melihat apakah kami melakukan kesalahan dalam implementasi kami atau jika kelas Math kustom membutuhkan sedikit lebih banyak pekerjaan.

Jangan Biarkan Metode Mencoba Melakukan Terlalu Banyak

Contoh ini juga menunjukkan gagasan tentang metode yang melakukan terlalu banyak. Tentu, penambahan adalah operasi yang cukup sederhana tanpa perlu banyak mengabstraksikan fungsinya dari metode GetNthTerm kami. Tetapi bagaimana jika operasinya sedikit lebih rumit? Alih-alih penambahan, mungkin itu adalah validasi model, memanggil pabrik untuk mendapatkan objek untuk dioperasikan, atau mengumpulkan data tambahan yang dibutuhkan dari repositori.

Sebagian besar pengembang akan mencoba untuk tetap berpegang pada gagasan bahwa satu metode memiliki satu tujuan. Dalam pengujian unit, kami mencoba untuk tetap berpegang pada prinsip bahwa pengujian unit harus diterapkan pada metode atom dan dengan memperkenalkan terlalu banyak operasi ke metode kami membuatnya tidak dapat diuji. Kita sering dapat membuat masalah di mana kita harus menulis begitu banyak tes untuk menguji fungsi kita dengan benar.

Setiap parameter yang kita tambahkan ke suatu metode meningkatkan jumlah pengujian yang harus kita tulis secara eksponensial sesuai dengan kompleksitas parameter. Jika Anda menambahkan boolean ke logika Anda, Anda perlu menggandakan jumlah tes untuk menulis karena Anda sekarang perlu memeriksa kasus benar dan salah bersama dengan tes Anda saat ini. Dalam hal validasi model, kompleksitas pengujian unit kami dapat meningkat dengan sangat cepat.

Diagram peningkatan pengujian yang diperlukan saat boolean ditambahkan ke logika.

Kita semua bersalah karena menambahkan sedikit tambahan ke suatu metode. Tetapi metode yang lebih besar dan lebih kompleks ini menciptakan kebutuhan akan terlalu banyak unit test. Dan dengan cepat menjadi jelas ketika Anda menulis unit test bahwa metode ini mencoba melakukan terlalu banyak. Jika Anda merasa mencoba menguji terlalu banyak kemungkinan hasil dari parameter input Anda, pertimbangkan fakta bahwa metode Anda perlu dipecah menjadi serangkaian yang lebih kecil.

Jangan Ulangi Diri Sendiri

Salah satu penyewa pemrograman favorit kami. Yang satu ini harus cukup lurus ke depan. Jika Anda mendapati diri Anda menulis tes yang sama lebih dari sekali, Anda telah memasukkan kode lebih dari sekali. Mungkin bermanfaat bagi Anda untuk memfaktorkan ulang yang berfungsi menjadi kelas umum yang dapat diakses oleh kedua instance yang Anda coba gunakan.

Alat Pengujian Unit Apa yang Tersedia?

DotNet menawarkan kami platform pengujian unit yang sangat kuat di luar kotak. Dengan menggunakan ini, Anda dapat menerapkan apa yang dikenal sebagai metodologi Arrange, Act, Assert. Anda mengatur pertimbangan awal Anda, bertindak berdasarkan kondisi itu dengan metode Anda yang sedang diuji, lalu menegaskan sesuatu telah terjadi. Anda dapat menegaskan apa saja, membuat alat ini semakin kuat. Anda dapat menyatakan bahwa suatu metode dipanggil beberapa kali, bahwa metode tersebut mengembalikan nilai tertentu, bahwa jenis pengecualian tertentu dilemparkan, atau apa pun yang dapat Anda pikirkan. Bagi mereka yang mencari kerangka kerja yang lebih maju, NUnit dan mitra Java-nya JUnit adalah opsi yang layak.

 [TestMethod] //Test To Verify Add Never Called on the First Term public void Fibonacci_GetNthTerm_Input0_AssertAddNeverCalled() { //Arrange int n = 0; //setup Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>(); mockMath .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>())) .Returns((int x, int y) => x + y); UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object); //Act int result = fibonacci.GetNthTerm(n); //Assert mockMath.Verify(r => r.Add(It.IsAny<int>(), It.IsAny<int>()), Times.Never); }

Menguji bahwa Metode Fibonacci kami menangani angka negatif dengan melemparkan pengecualian. Tes unit dapat memverifikasi bahwa pengecualian telah dilemparkan.

Untuk menangani injeksi ketergantungan, baik Ninject dan Unity ada di platform DotNet. Ada sedikit perbedaan antara keduanya, dan itu menjadi masalah jika Anda ingin mengelola konfigurasi dengan Sintaks Lancar atau Konfigurasi XML.

Untuk mensimulasikan dependensi, saya merekomendasikan Moq. Moq dapat menjadi tantangan untuk Anda dapatkan, tetapi intinya adalah Anda membuat versi tiruan dari dependensi Anda. Kemudian, Anda memberi tahu dependensi apa yang harus dikembalikan dalam kondisi tertentu. Misalnya, jika Anda memiliki metode bernama Square(int x) yang mengkuadratkan bilangan bulat, Anda dapat mengetahuinya ketika x = 2, mengembalikan 4. Anda juga dapat memerintahkannya untuk mengembalikan x^2 untuk bilangan bulat apa pun. Atau Anda dapat memintanya untuk mengembalikan 5 ketika x = 2. Mengapa Anda melakukan kasus terakhir? Jika metode di bawah peran tes adalah untuk memvalidasi jawaban dari ketergantungan, Anda mungkin ingin memaksa jawaban yang tidak valid untuk kembali untuk memastikan Anda menangkap bug dengan benar.

 [TestMethod] //Test To Verify Add Called Three times on the fifth Term public void Fibonacci_GetNthTerm_Input4_AssertAddCalledThreeTimes() { //Arrange int n = 4; //setup Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>(); mockMath .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>())) .Returns((int x, int y) => x + y); UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object); //Act int result = fibonacci.GetNthTerm(n); //Assert mockMath.Verify(r => r.Add(It.IsAny<int>(), It.IsAny<int>()), Times.Exactly(3)); }

Menggunakan Moq untuk memberi tahu antarmuka IMath yang diejek bagaimana menangani Add yang sedang diuji. Anda dapat mengatur kasus eksplisit dengan It.Is atau rentang dengan It.IsInRange .

Kerangka Kerja Pengujian Unit untuk DotNet

Kerangka Pengujian Unit Microsoft

Kerangka Pengujian Unit Microsoft adalah solusi pengujian unit out-of-the-box dari Microsoft dan disertakan dengan Visual Studio. Karena dilengkapi dengan VS, ia terintegrasi dengan baik dengannya. Saat Anda memulai sebuah proyek, Visual Studio akan menanyakan apakah Anda ingin membuat Perpustakaan Uji Unit di samping aplikasi Anda.

Kerangka Kerja Pengujian Unit Microsoft juga dilengkapi dengan sejumlah alat untuk membantu Anda menganalisis prosedur pengujian dengan lebih baik. Selain itu, karena dimiliki dan ditulis oleh Microsoft, ada beberapa perasaan stabilitas dalam keberadaannya di masa depan.

Tetapi ketika bekerja dengan alat Microsoft, Anda mendapatkan apa yang mereka berikan kepada Anda. Kerangka Kerja Pengujian Unit Microsoft dapat menjadi rumit untuk diintegrasikan.

NUnit

Keuntungan terbesar bagi saya dalam menggunakan NUnit adalah tes parameter. Dalam contoh Fibonacci kami di atas, kami dapat memasukkan sejumlah kasus uji dan memastikan hasil tersebut benar. Dan dalam kasus masalah ke-501, kami selalu dapat menambahkan set parameter baru untuk memastikan bahwa pengujian selalu dijalankan tanpa memerlukan metode pengujian baru.

Kelemahan utama NUnit adalah mengintegrasikannya ke dalam Visual Studio. Itu tidak memiliki lonceng dan peluit yang datang dengan versi Microsoft dan berarti Anda harus mengunduh perangkat Anda sendiri.

xUnit.Net

xUnit sangat populer di C# karena terintegrasi dengan baik dengan ekosistem .NET yang ada. Nuget memiliki banyak ekstensi xUnit yang tersedia. Ini juga terintegrasi dengan baik dengan Team Foundation Server, meskipun saya tidak yakin berapa banyak pengembang .NET yang masih menggunakan TFS pada berbagai implementasi Git.

Pada sisi negatifnya, banyak pengguna mengeluh bahwa dokumentasi xUnit agak kurang. Bagi pengguna baru untuk pengujian unit, ini dapat menyebabkan sakit kepala yang parah. Selain itu, ekstensibilitas dan kemampuan beradaptasi xUnit juga membuat kurva pembelajaran sedikit lebih curam daripada NUnit atau Kerangka Pengujian Unit Microsoft.

Desain/Pengembangan Berbasis Uji

Desain/pengembangan yang digerakkan oleh tes (TDD) adalah topik yang sedikit lebih maju yang layak mendapatkan postingannya sendiri. Namun, saya ingin memberikan pengantar.

Idenya adalah untuk memulai dengan pengujian unit Anda dan memberi tahu pengujian unit Anda apa yang benar. Kemudian, Anda dapat menulis kode Anda di sekitar tes tersebut. Secara teori, konsepnya terdengar sederhana, tetapi dalam praktiknya, sangat sulit untuk melatih otak Anda untuk berpikir mundur tentang aplikasinya. Tetapi pendekatan ini memiliki manfaat bawaan karena tidak diharuskan untuk menulis pengujian unit Anda setelah fakta. Ini menyebabkan lebih sedikit refactoring, penulisan ulang, dan kebingungan kelas.

TDD telah menjadi kata kunci dalam beberapa tahun terakhir tetapi adopsinya lambat. Sifat konseptualnya membingungkan para pemangku kepentingan yang membuatnya sulit untuk disetujui. Tetapi sebagai pengembang, saya mendorong Anda untuk menulis bahkan aplikasi kecil menggunakan pendekatan TDD untuk membiasakan diri dengan prosesnya.

Mengapa Anda Tidak Dapat Memiliki Terlalu Banyak Tes Unit

Pengujian unit adalah salah satu alat pengujian paling kuat yang dimiliki pengembang. Ini sama sekali tidak cukup untuk pengujian penuh aplikasi Anda, tetapi manfaatnya dalam pengujian regresi, desain kode, dan dokumentasi tujuan tidak tertandingi.

Tidak ada yang namanya menulis terlalu banyak unit test. Setiap kasus tepi dapat mengusulkan masalah besar di telepon dalam perangkat lunak Anda. Mengenang bug yang ditemukan sebagai pengujian unit dapat memastikan bug tersebut tidak menemukan cara untuk merayap kembali ke perangkat lunak Anda selama perubahan kode nanti. Meskipun Anda dapat menambahkan 10-20% ke anggaran awal proyek, Anda dapat menghemat lebih banyak dari itu dalam pelatihan, perbaikan bug, dan dokumentasi.

Anda dapat menemukan repositori Bitbucket yang digunakan dalam artikel ini di sini.