Memilih strategi untuk pengujian

Seperti yang dibahas dalam Gambaran Umum, keputusan dasar yang perlu Anda buat adalah apakah pengujian Anda akan melibatkan sistem database produksi Anda - seperti yang dilakukan aplikasi Anda - atau apakah pengujian Anda akan berjalan terhadap pengujian ganda, yang menggantikan sistem database produksi Anda.

Pengujian terhadap sumber daya eksternal nyata - daripada menggantinya dengan pengujian ganda - dapat melibatkan kesulitan berikut:

  1. Dalam banyak kasus, tidak mungkin atau praktis untuk menguji terhadap sumber daya eksternal yang sebenarnya. Misalnya, aplikasi Anda dapat berinteraksi dengan beberapa layanan yang tidak dapat dengan mudah diuji (karena pembatasan laju, atau kurangnya lingkungan pengujian).
  2. Bahkan ketika mungkin untuk melibatkan sumber daya eksternal nyata, ini mungkin terlalu lambat: menjalankan sejumlah besar pengujian terhadap layanan cloud dapat menyebabkan pengujian terlalu lama. Pengujian harus menjadi bagian dari alur kerja sehari-hari pengembang, jadi penting bahwa pengujian berjalan dengan cepat.
  3. Menjalankan pengujian terhadap sumber daya eksternal dapat melibatkan masalah isolasi, di mana pengujian mengganggu satu dengan yang lain. Misalnya, beberapa pengujian yang berjalan secara paralel terhadap database dapat memodifikasi data dan menyebabkan satu sama lain gagal dengan berbagai cara. Menggunakan test double menghindari hal ini, karena setiap pengujian dijalankan dengan sumber daya dalam memori tersendiri, dan dengan sendirinya terisolasi dari pengujian lain.

Namun, pengujian yang lulus terhadap pengujian ganda tidak menjamin bahwa program Anda berfungsi saat berjalan melawan sumber daya eksternal nyata. Misalnya, pengujian database ganda dapat melakukan perbandingan string peka huruf besar/kecil, sedangkan sistem database produksi melakukan perbandingan yang tidak peka huruf besar/kecil. Masalah tersebut hanya terungkap ketika pengujian dijalankan terhadap database produksi nyata Anda, menjadikan pengujian ini bagian penting dari strategi pengujian apa pun.

Pengujian terhadap database mungkin lebih mudah daripada yang terlihat

Karena kesulitan di atas dengan pengujian terhadap database nyata, pengembang sering didesak untuk menggunakan pengujian ganda terlebih dahulu, dan memiliki rangkaian pengujian yang kuat yang dapat sering mereka jalankan di komputer mereka; pengujian yang melibatkan database, sebaliknya, seharusnya dijalankan jauh lebih jarang, dan dalam banyak kasus juga memberikan cakupan yang jauh lebih sedikit. Kami menyarankan untuk memberikan lebih banyak pemikiran pada opsi yang terakhir, dan kami menyarankan bahwa database mungkin sebenarnya jauh kurang terpengaruh oleh masalah di atas daripada yang orang cenderung pikirkan.

  1. Sebagian besar database saat ini dapat dengan mudah diinstal pada komputer pengembang. Teknologi berbasis kontainer seperti Docker dapat membuat ini sangat mudah, dan pustaka seperti Testcontainers dapat membantu mengotomatiskan siklus hidup database kontainer dalam pengujian Anda. Teknologi seperti GitHub Workspaces dan Dev Container menyiapkan seluruh lingkungan pengembangan Untuk Anda (termasuk database). Saat menggunakan SQL Server, Dimungkinkan juga untuk menguji terhadap LocalDB di Windows, atau dengan mudah menyiapkan gambar Docker di Linux.
  2. Pengujian terhadap database lokal - dengan himpunan data pengujian yang wajar - biasanya sangat cepat: komunikasi benar-benar lokal, dan data pengujian biasanya di-buffer dalam memori di sisi database. EF Core sendiri berisi lebih dari 30.000 pengujian terhadap SQL Server saja; ini selesai dengan andal dalam beberapa menit, dijalankan di CI pada setiap penerapan tunggal, dan sangat sering dijalankan oleh pengembang secara lokal. Beberapa pengembang beralih ke database dalam memori ("palsu") dalam keyakinan bahwa ini diperlukan untuk kecepatan - ini hampir tidak pernah benar-benar terjadi.
  3. Isolasi memang merupakan rintangan saat menjalankan pengujian terhadap database nyata, karena pengujian dapat memodifikasi data dan mengganggu satu sama lain. Namun, ada berbagai teknik untuk memberikan isolasi dalam skenario pengujian database; kami berkonsentrasi pada ini dalam Pengujian terhadap sistem database produksi Anda).

Hal di atas tidak dimaksudkan untuk meremehkan test double atau untuk berargumen terhadap penggunaannya. Untuk satu hal, pengujian ganda diperlukan untuk beberapa skenario yang tidak dapat diuji sebaliknya, seperti mensimulasikan kegagalan database. Namun, dalam pengalaman kami, pengguna sering malu-malu menguji terhadap database mereka karena alasan di atas, percaya itu lambat, sulit atau tidak dapat diandalkan, ketika itu belum tentu terjadi. Pengujian terhadap sistem database produksi Anda bertujuan untuk mengatasi hal ini, memberikan panduan dan sampel untuk menulis pengujian yang cepat dan terisolasi terhadap database Anda.

Berbagai jenis uji ganda

Uji ganda adalah istilah luas yang mencakup pendekatan yang sangat berbeda. Bagian ini mencakup beberapa teknik umum yang melibatkan pengujian ganda untuk menguji aplikasi EF Core:

  1. Gunakan SQLite (mode dalam memori) sebagai database palsu, menggantikan sistem database produksi Anda.
  2. Gunakan penyedia dalam memori EF Core sebagai database palsu, menggantikan sistem database produksi Anda.
  3. Mock atau membuat tiruan atau menonaktifkan DbContext dan DbSet.
  4. Perkenalkan lapisan repositori antara EF Core dan kode aplikasi Anda, serta lakukan mocking atau buat stub untuk lapisan tersebut.

Di bawah ini, kita akan menjelajahi apa arti setiap metode, dan membandingkannya dengan yang lain. Sebaiknya baca melalui berbagai metode untuk mendapatkan pemahaman penuh tentang masing-masing metode. Jika Anda telah memutuskan untuk menulis pengujian yang tidak melibatkan sistem database produksi Anda, maka lapisan repositori adalah satu-satunya pendekatan yang memungkinkan stubbing/mocking lapisan data yang komprehensif dan andal. Namun, pendekatan tersebut memiliki biaya yang signifikan dalam hal implementasi dan pemeliharaan.

SQLite sebagai database palsu

Salah satu pendekatan pengujian yang mungkin adalah menukar database produksi Anda (e.g. SQL Server) dengan SQLite, secara efektif menggunakannya sebagai pengujian "palsu". Selain kemudahan penyiapan, SQLite memiliki fitur database dalam memori yang sangat berguna untuk pengujian: setiap pengujian secara alami diisolasi dalam database dalam memorinya sendiri, dan tidak ada file aktual yang perlu dikelola.

Namun, sebelum melakukan ini, penting untuk dipahami bahwa di EF Core, penyedia database yang berbeda bertingkah berbeda - EF Core tidak mencoba mengabstraksi setiap aspek sistem database yang mendasar. Pada dasarnya, ini berarti bahwa pengujian terhadap SQLite tidak menjamin hasil yang sama seperti terhadap SQL Server, atau database lainnya. Berikut adalah beberapa contoh kemungkinan perbedaan perilaku:

  • Kueri LINQ yang sama dapat mengembalikan hasil yang berbeda pada penyedia yang berbeda. Misalnya, SQL Server melakukan perbandingan string yang tidak peka huruf besar/kecil secara default, sedangkan SQLite peka huruf besar/kecil. Ini dapat membuat pengujian Anda lulus terhadap SQLite di mana mereka akan gagal melawan SQL Server (atau sebaliknya).
  • Beberapa kueri yang berfungsi di SQL Server tidak didukung di SQLite, karena dukungan SQL yang tepat dalam dua database ini berbeda.
  • Jika kueri Anda kebetulan menggunakan metode khusus penyedia seperti SQL Server EF.Functions.DateDiffDay, kueri tersebut akan gagal pada SQLite, dan tidak dapat diuji.
  • SQL mentah mungkin berfungsi, atau mungkin gagal atau mengembalikan hasil yang berbeda, tergantung pada apa yang sedang dilakukan. Dialek SQL berbeda dalam banyak cara di seluruh database.

Dibandingkan dengan menjalankan pengujian terhadap sistem database produksi Anda, relatif mudah untuk memulai dengan SQLite, dan begitu banyak pengguna melakukannya. Sayangnya, batasan di atas cenderung pada akhirnya menjadi bermasalah saat menguji aplikasi EF Core, bahkan jika mereka tampaknya tidak berada di awal. Akibatnya, kami merekomendasikan menulis pengujian Anda terhadap database nyata Anda, atau jika menggunakan pengujian duplikat adalah keharusan mutlak, mempertimbangkan biaya pola repositori seperti yang dibahas di bawah ini.

Untuk informasi tentang cara menggunakan SQLite untuk pengujian, lihat bagian ini.

Dalam memori sebagai database palsu

Sebagai alternatif untuk SQLite, EF Core juga dilengkapi dengan penyedia penyimpanan sementara di memori. Meskipun penyedia ini awalnya dirancang untuk mendukung pengujian internal EF Core itu sendiri, beberapa pengembang menggunakannya sebagai palsu database saat menguji aplikasi EF Core. Melakukannya sangat tidak disarankan: sebagai database palsu, dalam memori memiliki masalah yang sama dengan SQLite (lihat di atas), tetapi selain itu memiliki batasan tambahan berikut:

  • Penyedia dalam memori umumnya mendukung lebih sedikit jenis kueri daripada penyedia SQLite, karena bukan database relasional. Lebih banyak kueri akan gagal atau berprilaku berbeda dibandingkan dengan database produksi Anda.
  • Transaksi tidak didukung.
  • SQL mentah benar-benar tidak didukung. Bandingkan ini dengan SQLite, di mana dimungkinkan untuk menggunakan SQL mentah, selama SQL bekerja dengan cara yang sama pada SQLite dan database produksi Anda.
  • Penyedia in-memory belum dioptimalkan untuk performa, dan umumnya beroperasi lebih lambat daripada SQLite dalam mode in-memory (atau bahkan dibandingkan dengan sistem basis data produksi Anda).

Singkatnya, in-memory memiliki semua kelemahan SQLite, ditambah beberapa kekurangan lainnya, dan tidak menawarkan keuntungan apapun sebagai imbalannya. Jika Anda mencari database palsu dalam memori yang sederhana, gunakan SQLite alih-alih penyedia dalam memori; tetapi pertimbangkan untuk menggunakan pola repositori sebagai gantinya seperti yang dijelaskan di bawah ini.

Untuk informasi tentang cara menggunakan pengujian in-memory, lihat bagian ini.

Mocking atau stubbing DbContext dan DbSet

Pendekatan ini biasanya menggunakan kerangka kerja pemodelan untuk membuat duplikat pengujian dari DbContext dan DbSet, dan melakukan pengujian terhadap duplikat tersebut. Meniru DbContext dapat menjadi pendekatan yang baik untuk menguji berbagai fungsionalitas non-kueri, seperti panggilan ke Add atau SaveChanges(), memungkinkan Anda memverifikasi bahwa kode Anda memanggilnya dalam skenario penulisan.

Namun, meniru fungsionalitas kueri dengan benar tidak mungkin dilakukan, karena kueri diekspresikan melalui operator LINQ, yang merupakan panggilan metode ekstensi statis di atas IQueryable. Akibatnya, ketika beberapa orang berbicara tentang "memanipulasi DbSet", yang mereka maksud sebenarnya adalah bahwa mereka membuat DbSet yang didukung oleh kumpulan data dalam memori, kemudian mengevaluasi operator kueri terhadap kumpulan tersebut dalam memori, persis seperti IEnumerable yang sederhana. Alih-alih tiruan, ini sebenarnya semacam palsu, di mana koleksi dalam memori menggantikan database nyata.

Karena hanya DbSet itu sendiri yang dipalsukan dan kueri dievaluasi dalam memori, pendekatan ini akhirnya sangat mirip dengan menggunakan penyedia dalam memori EF Core: kedua teknik menjalankan operator kueri di .NET melalui koleksi dalam memori. Akibatnya, teknik ini juga menderita kelemahan yang sama: kueri akan berperilaku berbeda (misalnya terkait sensitivitas huruf besar/kecil) atau hanya akan gagal (misalnya karena metode khusus penyedia), SQL mentah tidak akan berfungsi dan transaksi paling banter akan diabaikan. Akibatnya, teknik ini umumnya harus dihindari untuk menguji kode kueri apa pun.

Pola repositori

Pendekatan di atas mencoba untuk menukar penyedia database produksi EF Core dengan penyedia pengujian palsu, atau membuat DbSet yang didukung oleh koleksi dalam memori. Teknik ini serupa karena mereka masih mengevaluasi kueri LINQ program - baik dalam SQLite atau dalam memori - dan ini pada akhirnya adalah sumber kesulitan yang diuraikan di atas: kueri yang dirancang untuk dijalankan terhadap database produksi tertentu tidak dapat dijalankan dengan andal di tempat lain tanpa masalah.

Untuk pengujian yang tepat dan andal, pertimbangkan untuk memperkenalkan repository layer yang menjadi perantara antara kode aplikasi Anda dan EF Core. Implementasi produksi repositori berisi kueri LINQ aktual dan menjalankannya melalui EF Core. Dalam pengujian, abstraksi repositori secara langsung tersandung atau ditipu tanpa memerlukan kueri LINQ yang sebenarnya, secara efektif menghapus EF Core dari tumpukan pengujian Anda sama sekali dan memungkinkan pengujian untuk fokus pada kode aplikasi saja.

Diagram berikut membandingkan pendekatan palsu database (SQLite/in-memory) dengan pola repositori:

Perbandingan penyedia palsu dengan pola repositori

Karena kueri LINQ tidak lagi menjadi bagian dari pengujian, Anda dapat langsung memberikan hasil kueri ke aplikasi Anda. Dengan kata lain, pendekatan sebelumnya kira-kira memungkinkan mensimulasikan input kueri (misalnya mengganti tabel SQL Server dengan yang di memori), tetapi kemudian masih menjalankan operator kueri yang sebenarnya di memori. Pola repositori, sebaliknya, memungkinkan Anda untuk memalsukan output kueri secara langsung, memungkinkan pengujian unit yang jauh lebih kuat dan terfokus. Perhatikan bahwa agar ini berfungsi, repositori Anda tidak boleh mengekspos metode yang mengembalikan IQueryable, karena sekali lagi ini tidak bisa dipalsukan; IEnumerable harus dikembalikan.

Namun, karena pola repositori memerlukan setiap kueri LINQ (yang dapat diuji) untuk dienkapsulasi dalam metode yang mengembalikan IEnumerable, ini menambahkan lapisan arsitektur ekstra pada aplikasi Anda, dan dapat memerlukan biaya signifikan untuk diimplementasikan dan dipelihara. Biaya ini tidak boleh didiskon ketika membuat pilihan tentang cara menguji aplikasi, terutama mengingat bahwa pengujian terhadap database nyata masih mungkin diperlukan untuk kueri yang diekspos oleh repositori.

Perlu dicatat bahwa repositori memang memiliki keuntungan di luar hanya pengujian. Mereka memastikan semua kode akses data terkonsentrasi di satu tempat daripada tersebar di sekitar aplikasi, dan jika aplikasi Anda perlu mendukung lebih dari satu database, maka abstraksi repositori dapat sangat membantu untuk mengubah kueri di seluruh penyedia.

Untuk sampel yang memperlihatkan pengujian dengan repositori, lihat bagian ini.

Perbandingan keseluruhan

Tabel berikut ini menyediakan tampilan cepat dan komparatif dari berbagai teknik pengujian, dan menunjukkan fungsionalitas mana yang dapat diuji dengan pendekatan mana.

Fitur Dalam memori SQLite dalam memori Mock DbContext Pola repositori Pengujian terhadap database
Uji tipe ganda Palsu Palsu Palsu Tiruan/sandi Asli, tidak ada duplikasi
SQL mentah? Tidak Bergantung Tidak Ya Ya
Transaksi? Tidak (diabaikan) Ya Ya Ya Ya
Terjemahan khusus penyedia? Tidak Tidak Tidak Ya Ya
Perilaku pencarian yang akurat? Bergantung Bergantung Bergantung Ya Ya
Dapatkah menggunakan LINQ di mana saja dalam aplikasi? Ya Ya Ya Tidak* Ya

* Semua kueri LINQ database yang dapat diuji harus dienkapsulasi dalam metode repositori yang mengembalikan IEnumerable, agar dapat di-stub atau di-mock.

Ringkasan

  • Kami menyarankan agar pengembang memiliki cakupan pengujian yang baik tentang aplikasi mereka yang berjalan terhadap sistem database produksi mereka yang sebenarnya. Ini memberikan keyakinan bahwa aplikasi benar-benar bekerja dalam produksi, dan dengan desain yang tepat, pengujian dapat dijalankan dengan andal dan cepat. Karena pengujian ini diperlukan dalam hal apa pun, ada baiknya untuk memulai di sana, dan jika diperlukan, tambahkan pengujian menggunakan pengujian ganda nanti, sesuai kebutuhan.
  • Jika Anda telah memutuskan untuk menggunakan pengujian ganda, sebaiknya terapkan pola repositori, yang memungkinkan Anda untuk mencambuk atau mengejek lapisan akses data Anda di atas EF Core, daripada menggunakan penyedia EF Core palsu (Sqlite/in-memory) atau dengan menipu DbSet.
  • Jika pola repositori bukan opsi yang layak karena alasan tertentu, pertimbangkan untuk menggunakan database dalam memori SQLite.
  • Hindari penyedia memori untuk tujuan pengujian - langkah ini tidak disarankan dan hanya didukung untuk aplikasi lama.
  • Hindari meniru DbSet untuk tujuan kueri.