Saya ingin membagikan pendekatan yang dikomentari dan dibahas secara singkat tetapi menunjukkan contoh aktual yang saat ini saya gunakan untuk membantu unit menguji layanan berbasis EF.
Pertama, saya ingin menggunakan penyedia di-memori dari EF Core, tetapi ini tentang EF 6. Selanjutnya, untuk sistem penyimpanan lain seperti RavenDB, saya juga akan menjadi pendukung pengujian melalui penyedia basis data di-memori. Lagi - ini khusus untuk membantu menguji kode berbasis EF tanpa banyak upacara .
Berikut adalah tujuan yang saya miliki ketika membuat sebuah pola:
- Itu harus sederhana untuk dipahami oleh pengembang lain dalam tim
- Itu harus mengisolasi kode EF di tingkat sedekat mungkin
- Itu tidak boleh melibatkan menciptakan antarmuka multi-tanggung jawab aneh (seperti pola repositori "generik" atau "khas")
- Harus mudah dikonfigurasikan dan diatur dalam unit test
Saya setuju dengan pernyataan sebelumnya bahwa EF masih merupakan detail implementasi dan tidak apa-apa untuk merasa seperti Anda perlu abstrak untuk melakukan tes unit "murni". Saya juga setuju bahwa idealnya, saya ingin memastikan kode EF itu sendiri berfungsi - tetapi ini melibatkan basis data kotak pasir, penyedia di-memori, dll. Pendekatan saya menyelesaikan kedua masalah - Anda dapat dengan aman menguji unit kode yang tergantung pada EF dan membuat tes integrasi untuk menguji kode EF Anda secara khusus.
Cara saya mencapainya adalah dengan hanya merangkum kode EF ke dalam kelas Query dan Command khusus. Idenya sederhana: hanya bungkus kode EF di kelas dan bergantung pada antarmuka di kelas yang semula akan menggunakannya. Masalah utama yang saya perlu selesaikan adalah menghindari menambahkan banyak dependensi ke kelas dan mengatur banyak kode dalam pengujian saya.
Di sinilah perpustakaan yang berguna dan sederhana masuk: Mediatr . Ini memungkinkan untuk pesan dalam proses yang sederhana dan melakukannya dengan memisahkan "permintaan" dari penangan yang mengimplementasikan kode. Ini memiliki manfaat tambahan memisahkan "apa" dari "bagaimana". Misalnya, dengan mengenkapsulasi kode EF ke dalam potongan-potongan kecil, Anda dapat mengganti implementasinya dengan penyedia lain atau mekanisme yang sama sekali berbeda, karena yang Anda lakukan hanyalah mengirim permintaan untuk melakukan tindakan.
Menggunakan injeksi ketergantungan (dengan atau tanpa kerangka kerja - preferensi Anda), kami dapat dengan mudah mengejek mediator dan mengontrol mekanisme permintaan / respons untuk memungkinkan unit menguji kode EF.
Pertama, katakanlah kita memiliki layanan yang memiliki logika bisnis yang perlu kita uji:
public class FeatureService {
private readonly IMediator _mediator;
public FeatureService(IMediator mediator) {
_mediator = mediator;
}
public async Task ComplexBusinessLogic() {
// retrieve relevant objects
var results = await _mediator.Send(new GetRelevantDbObjectsQuery());
// normally, this would have looked like...
// var results = _myDbContext.DbObjects.Where(x => foo).ToList();
// perform business logic
// ...
}
}
Apakah Anda mulai melihat manfaat dari pendekatan ini? Anda tidak hanya merangkum semua kode terkait-EF secara eksplisit ke dalam kelas deskriptif, Anda juga memungkinkan perpanjangan dengan menghilangkan keprihatinan implementasi "bagaimana" permintaan ini ditangani - kelas ini tidak peduli jika objek yang relevan berasal dari EF, MongoDB, atau file teks.
Sekarang untuk permintaan dan penangan, melalui MediatR:
public class GetRelevantDbObjectsQuery : IRequest<DbObject[]> {
// no input needed for this particular request,
// but you would simply add plain properties here if needed
}
public class GetRelevantDbObjectsEFQueryHandler : IRequestHandler<GetRelevantDbObjectsQuery, DbObject[]> {
private readonly IDbContext _db;
public GetRelevantDbObjectsEFQueryHandler(IDbContext db) {
_db = db;
}
public DbObject[] Handle(GetRelevantDbObjectsQuery message) {
return _db.DbObjects.Where(foo => bar).ToList();
}
}
Seperti yang Anda lihat, abstraksi itu sederhana dan dienkapsulasi. Ini juga benar-benar dapat diuji karena dalam tes integrasi, Anda dapat menguji kelas ini secara individual - tidak ada masalah bisnis yang bercampur aduk di sini.
Jadi, seperti apa uji unit dari layanan fitur kami? Sederhana saja. Dalam hal ini, saya menggunakan Moq untuk melakukan ejekan (menggunakan apa pun yang membuat Anda bahagia):
[TestClass]
public class FeatureServiceTests {
// mock of Mediator to handle request/responses
private Mock<IMediator> _mediator;
// subject under test
private FeatureService _sut;
[TestInitialize]
public void Setup() {
// set up Mediator mock
_mediator = new Mock<IMediator>(MockBehavior.Strict);
// inject mock as dependency
_sut = new FeatureService(_mediator.Object);
}
[TestCleanup]
public void Teardown() {
// ensure we have called or expected all calls to Mediator
_mediator.VerifyAll();
}
[TestMethod]
public void ComplexBusinessLogic_Does_What_I_Expect() {
var dbObjects = new List<DbObject>() {
// set up any test objects
new DbObject() { }
};
// arrange
// setup Mediator to return our fake objects when it receives a message to perform our query
// in practice, I find it better to create an extension method that encapsulates this setup here
_mediator.Setup(x => x.Send(It.IsAny<GetRelevantDbObjectsQuery>(), default(CancellationToken)).ReturnsAsync(dbObjects.ToArray()).Callback(
(GetRelevantDbObjectsQuery message, CancellationToken token) => {
// using Moq Callback functionality, you can make assertions
// on expected request being passed in
Assert.IsNotNull(message);
});
// act
_sut.ComplexBusinessLogic();
// assertions
}
}
Anda dapat melihat semua yang kami butuhkan adalah pengaturan tunggal dan kami bahkan tidak perlu mengkonfigurasi apa pun ekstra - ini adalah tes unit yang sangat sederhana. Mari kita perjelas: Ini sangat mungkin untuk dilakukan tanpa sesuatu seperti Mediatr (Anda hanya akan mengimplementasikan antarmuka dan mengejeknya untuk tes, misalnya IGetRelevantDbObjectsQuery
), tetapi dalam praktiknya untuk basis kode besar dengan banyak fitur dan pertanyaan / perintah, saya suka enkapsulasi dan bawaan DI mendukung penawaran Mediatr.
Jika Anda bertanya-tanya bagaimana saya mengatur kelas-kelas ini, itu sangat sederhana:
- MyProject
- Features
- MyFeature
- Queries
- Commands
- Services
- DependencyConfig.cs (Ninject feature modules)
Pengorganisasian dengan irisan fitur tidak penting, tetapi ini menjaga semua kode yang relevan / tergantung bersama dan mudah ditemukan. Yang paling penting, saya memisahkan Query vs Commands - mengikuti prinsip Command / Query Separation .
Ini memenuhi semua kriteria saya: upacara rendah, mudah dimengerti, dan ada manfaat tambahan tersembunyi. Misalnya, bagaimana Anda menangani perubahan penyimpanan? Sekarang Anda dapat menyederhanakan Konteks Db Anda dengan menggunakan antarmuka peran (IUnitOfWork.SaveChangesAsync()
) dan mengejek panggilan ke antarmuka peran tunggal atau Anda bisa merangkum melakukan / memutar kembali ke dalam RequestHandlers Anda - namun Anda lebih suka melakukannya terserah Anda, selama itu bisa dipertahankan. Misalnya, saya tergoda untuk membuat satu permintaan / penangan generik tunggal di mana Anda baru saja melewati objek EF dan itu akan menyimpan / memperbarui / menghapusnya - tetapi Anda harus bertanya apa maksud Anda dan ingat bahwa jika Anda ingin menukar handler dengan penyedia / implementasi penyimpanan lain, Anda mungkin harus membuat perintah / kueri eksplisit yang mewakili apa yang ingin Anda lakukan. Lebih sering daripada tidak, satu layanan atau fitur akan memerlukan sesuatu yang spesifik - jangan membuat barang-barang umum sebelum Anda membutuhkannya.
Tentu saja ada peringatan untuk pola ini - Anda bisa pergi terlalu jauh dengan mekanisme pub / sub sederhana. Saya telah membatasi implementasi saya hanya untuk mengabstraksi kode terkait EF, tetapi pengembang petualang dapat mulai menggunakan MediatR untuk berlebihan dan mengirim pesan untuk segalanya - sesuatu yang harus ditangkap oleh praktik peninjauan kode yang baik dan tinjauan sejawat. Itu masalah proses, bukan masalah dengan MediatR, jadi cukup sadari bagaimana Anda menggunakan pola ini.
Anda menginginkan contoh nyata tentang bagaimana orang menguji unit / mengejek EF dan ini adalah pendekatan yang berhasil bagi kami dalam proyek kami - dan tim sangat senang dengan betapa mudahnya untuk mengadopsi. Saya harap ini membantu! Seperti halnya semua hal dalam pemrograman, ada beberapa pendekatan dan semuanya tergantung pada apa yang ingin Anda capai. Saya menghargai kesederhanaan, kemudahan penggunaan, perawatan, dan kemampuan menemukan - dan solusi ini memenuhi semua tuntutan itu.