Bagaimana pengujian unit orang dengan Entity Framework 6, haruskah Anda repot-repot?

170

Saya baru memulai dengan pengujian Unit dan TDD secara umum. Saya telah mencoba-coba sebelumnya tetapi sekarang saya bertekad untuk menambahkannya ke alur kerja saya dan menulis perangkat lunak yang lebih baik.

Saya mengajukan pertanyaan kemarin termasuk jenis ini, tetapi tampaknya menjadi pertanyaan sendiri. Saya telah duduk untuk mulai menerapkan kelas layanan yang akan saya gunakan untuk mengabstraksi logika bisnis dari pengontrol dan peta ke model tertentu dan interaksi data menggunakan EF6.

Masalahnya adalah saya sudah memblokir sendiri karena saya tidak ingin mengambil abstrak EF di repositori (masih akan tersedia di luar layanan untuk pertanyaan tertentu, dll) dan ingin menguji layanan saya (Konteks EF akan digunakan) .

Di sini saya kira pertanyaannya, apakah ada gunanya melakukan ini? Jika demikian, bagaimana orang melakukannya di alam liar mengingat abstraksi yang bocor yang disebabkan oleh IQueryable dan banyak posting hebat oleh Ladislav Mrnka mengenai masalah unit testing tidak langsung karena perbedaan penyedia Linq ketika bekerja dengan memori. implementasi sesuai dengan database tertentu.

Kode yang ingin saya uji tampaknya cukup sederhana. (ini hanya kode dummy untuk mencoba dan memahami apa yang saya lakukan, saya ingin mendorong pembuatan menggunakan TDD)

Konteks

public interface IContext
{
    IDbSet<Product> Products { get; set; }
    IDbSet<Category> Categories { get; set; }
    int SaveChanges();
}

public class DataContext : DbContext, IContext
{
    public IDbSet<Product> Products { get; set; }
    public IDbSet<Category> Categories { get; set; }

    public DataContext(string connectionString)
                : base(connectionString)
    {

    }
}

Layanan

public class ProductService : IProductService
{
    private IContext _context;

    public ProductService(IContext dbContext)
    {
        _context = dbContext;
    }

    public IEnumerable<Product> GetAll()
    {
        var query = from p in _context.Products
                    select p;

        return query;
    }
}

Saat ini saya sedang berpikir untuk melakukan beberapa hal:

  1. Mengejek EF Konteks dengan pendekatan seperti ini - Mengejek EF Ketika Menguji Unit atau langsung menggunakan kerangka pengejek pada antarmuka seperti moq - menghilangkan rasa sakit yang mungkin dilewati oleh unit test tetapi tidak harus bekerja ujung ke ujung dan mendukungnya dengan tes Integrasi?
  2. Mungkin menggunakan sesuatu seperti Upaya untuk mengejek EF - Saya belum pernah menggunakannya dan tidak yakin apakah ada orang lain yang menggunakannya di alam liar?
  3. Tidak repot-repot menguji apa pun yang hanya memanggil kembali ke EF - jadi pada dasarnya metode layanan yang memanggil EF secara langsung (getAll dll) bukan unit yang diuji tetapi hanya integrasi yang diuji?

Adakah yang benar-benar melakukan ini di luar sana tanpa Repo dan berhasil?

Modika
sumber
Hai Modika, saya sedang memikirkan hal ini baru-baru ini (karena pertanyaan ini: stackoverflow.com/questions/25977388/... ) Di dalamnya saya mencoba menjelaskan sedikit lebih formal bagaimana saya bekerja saat ini, tetapi saya ingin mendengar bagaimana kamu melakukannya.
samy
Hai @samy, cara kami memutuskan untuk melakukannya bukan menguji unit apa pun yang menyentuh EF secara langsung. Kueri diuji tetapi sebagai uji integrasi, bukan tes unit. Mengejek EF terasa sedikit kotor, tetapi proyek ini kecil-kecilan sehingga dampak kinerja dari banyak tes yang menghantam database tidak terlalu memprihatinkan sehingga kami bisa sedikit lebih pragmatis tentang hal itu. Saya masih belum 100% yakin apa pendekatan terbaik untuk benar-benar jujur ​​kepada Anda, pada titik tertentu Anda akan mencapai EF (dan DB Anda) dan unit testing tidak terasa benar di sini.
Modika

Jawaban:

186

Ini adalah topik yang saya sangat tertarik. Ada banyak puritan yang mengatakan bahwa Anda tidak boleh menguji teknologi seperti EF dan NHibernate. Mereka benar, mereka sudah diuji dengan sangat ketat dan seperti jawaban sebelumnya menyatakan bahwa sering kali tidak ada gunanya menghabiskan banyak waktu menguji apa yang tidak Anda miliki.

Namun, Anda memiliki basis data di bawahnya!Di sinilah pendekatan ini menurut saya rusak, Anda tidak perlu menguji bahwa EF / NH melakukan pekerjaan mereka dengan benar. Anda perlu menguji apakah pemetaan / implementasi Anda berfungsi dengan basis data Anda. Menurut pendapat saya ini adalah salah satu bagian terpenting dari suatu sistem yang dapat Anda uji.

Namun secara tegas kami bergerak keluar dari domain pengujian unit dan ke pengujian integrasi tetapi prinsip-prinsipnya tetap sama.

Hal pertama yang perlu Anda lakukan adalah dapat mengejek DAL Anda sehingga BLL Anda dapat diuji secara independen dari EF dan SQL. Ini adalah unit test Anda. Selanjutnya Anda perlu merancang Tes Integrasi Anda untuk membuktikan DAL Anda, menurut saya ini sama pentingnya.

Ada beberapa hal yang perlu dipertimbangkan:

  1. Basis data Anda harus dalam kondisi yang diketahui dengan setiap tes. Sebagian besar sistem menggunakan cadangan atau membuat skrip untuk ini.
  2. Setiap tes harus dapat diulang
  3. Setiap tes harus berupa atom

Ada dua pendekatan utama untuk mengatur database Anda, yang pertama adalah menjalankan skrip DB UnitTest. Ini memastikan bahwa basis data unit test Anda akan selalu dalam kondisi yang sama di awal setiap tes (Anda dapat mengatur ulang ini atau menjalankan setiap tes dalam transaksi untuk memastikan ini).

Pilihan Anda yang lain adalah apa yang saya lakukan, jalankan pengaturan khusus untuk setiap tes individu. Saya percaya ini adalah pendekatan terbaik karena dua alasan utama:

  • Basis data Anda lebih sederhana, Anda tidak perlu seluruh skema untuk setiap tes
  • Setiap pengujian lebih aman, jika Anda mengubah satu nilai dalam skrip buat Anda, itu tidak membatalkan lusinan tes lainnya.

Sayangnya kompromi Anda di sini adalah kecepatan. Diperlukan waktu untuk menjalankan semua tes ini, untuk menjalankan semua skrip pengaturan / penghancuran ini.

Satu poin terakhir, bisa sangat sulit untuk menulis sejumlah besar SQL untuk menguji ORM Anda. Di sinilah saya mengambil pendekatan yang sangat jahat (kaum puritan di sini tidak akan setuju dengan saya). Saya menggunakan ORM saya untuk membuat tes saya! Daripada memiliki skrip yang terpisah untuk setiap tes DAL di sistem saya, saya memiliki fase pengaturan tes yang menciptakan objek, melampirkannya ke konteks dan menyimpannya. Saya kemudian menjalankan tes saya.

Ini jauh dari solusi ideal namun dalam praktiknya saya merasa BANYAK lebih mudah untuk dikelola (terutama ketika Anda memiliki beberapa ribu tes), jika tidak, Anda membuat banyak skrip Kepraktisan atas kemurnian.

Saya pasti akan melihat kembali jawaban ini dalam beberapa tahun (bulan / hari) dan tidak setuju dengan diri saya karena pendekatan saya telah berubah - namun ini adalah pendekatan saya saat ini.

Untuk mencoba dan meringkas semua yang saya katakan di atas, ini adalah tes integrasi DB khas saya:

[Test]
public void LoadUser()
{
  this.RunTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    return user.UserID;
  }, id => // the ID of the entity we need to load
  {
     var user = LoadMyUser(id); // load the entity
     Assert.AreEqual("Mr", user.Title); // test your properties
     Assert.AreEqual("Joe", user.Firstname);
     Assert.AreEqual("Bloggs", user.Lastname);
  }
}

Hal utama yang perlu diperhatikan di sini adalah bahwa sesi dari kedua loop sepenuhnya independen. Dalam implementasi RunTest Anda, Anda harus memastikan bahwa konteksnya dilakukan dan dihancurkan dan data Anda hanya dapat berasal dari database Anda untuk bagian kedua.

Sunting 13/10/2014

Saya memang mengatakan bahwa saya mungkin akan merevisi model ini selama beberapa bulan mendatang. Sementara saya sebagian besar mendukung pendekatan yang saya anjurkan di atas, saya sedikit memperbarui mekanisme pengujian saya. Saya sekarang cenderung membuat entitas di dalam TestSetup dan TestTearDown.

[SetUp]
public void Setup()
{
  this.SetupTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    this.UserID =  user.UserID;
  });
}

[TearDown]
public void TearDown()
{
   this.TearDownDatabase();
}

Kemudian uji setiap properti secara individual

[Test]
public void TestTitle()
{
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Mr", user.Title);
}

[Test]
public void TestFirstname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Joe", user.Firstname);
}

[Test]
public void TestLastname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Bloggs", user.Lastname);
}

Ada beberapa alasan untuk pendekatan ini:

  • Tidak ada panggilan database tambahan (satu setup, satu teardown)
  • Tes jauh lebih terperinci, setiap tes memverifikasi satu properti
  • Setup / TearDown logic dihapus dari metode Test itu sendiri

Saya merasa ini membuat kelas tes lebih sederhana dan tes lebih rinci (pernyataan tunggal baik )

Edit 5/3/2015

Revisi lain tentang pendekatan ini. Sementara pengaturan tingkat kelas sangat membantu untuk pengujian seperti memuat properti, mereka kurang berguna di mana pengaturan yang berbeda diperlukan. Dalam hal ini pengaturan kelas baru untuk setiap kasus adalah berlebihan.

Untuk membantu dengan ini saya sekarang cenderung memiliki dua kelas dasar SetupPerTestdan SingleSetup. Dua kelas ini mengekspos kerangka kerja seperti yang dipersyaratkan.

Di SingleSetupkami memiliki mekanisme yang sangat mirip seperti yang dijelaskan dalam edit pertama saya. Contohnya adalah

public TestProperties : SingleSetup
{
  public int UserID {get;set;}

  public override DoSetup(ISession session)
  {
    var user = new User("Joe", "Bloggs");
    session.Save(user);
    this.UserID = user.UserID;
  }

  [Test]
  public void TestLastname()
  {
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Bloggs", user.Lastname);
  }

  [Test]
  public void TestFirstname()
  {
       var user = LoadMyUser(this.UserID);
       Assert.AreEqual("Joe", user.Firstname);
  }
}

Namun referensi yang memastikan bahwa hanya entites yang benar dimuat dapat menggunakan pendekatan SetupPerTest

public TestProperties : SetupPerTest
{
   [Test]
   public void EnsureCorrectReferenceIsLoaded()
   {
      int friendID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriend();
         session.Save(user);
         friendID = user.Friends.Single().FriendID;
      } () =>
      {
         var user = GetUser();
         Assert.AreEqual(friendID, user.Friends.Single().FriendID);
      });
   }
   [Test]
   public void EnsureOnlyCorrectFriendsAreLoaded()
   {
      int userID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriends(2);
         var user2 = CreateUserWithFriends(5);
         session.Save(user);
         session.Save(user2);
         userID = user.UserID;
      } () =>
      {
         var user = GetUser(userID);
         Assert.AreEqual(2, user.Friends.Count());
      });
   }
}

Secara ringkas kedua pendekatan ini bekerja tergantung pada apa yang Anda coba uji.

Liath
sumber
2
Berikut adalah pendekatan berbeda untuk pengujian integrasi. TL; DR - Gunakan aplikasi itu sendiri untuk mengatur data uji, kembalikan transaksi per tes.
Gert Arnold
3
@Liath, respons yang bagus. Anda telah mengkonfirmasi kecurigaan saya tentang pengujian EF. Pertanyaan saya adalah ini; contoh Anda adalah untuk kasus yang sangat konkret, yang baik-baik saja. Namun, seperti yang Anda catat, Anda mungkin perlu menguji ratusan entitas. Sesuai dengan prinsip KERING (Jangan Ulangi Diri Sendiri) bagaimana Anda mengukur solusi Anda, tanpa mengulangi pola kode dasar yang sama setiap kali?
Jeffrey A. Gochin
4
Saya harus tidak setuju dengan ini karena itu sepenuhnya menghindari masalah. Pengujian unit adalah tentang pengujian logika fungsi. Dalam contoh OP, logika memiliki ketergantungan pada penyimpanan data. Anda benar ketika mengatakan tidak menguji EF, tapi bukan itu masalahnya. Masalahnya adalah menguji kode Anda secara terpisah dari datastore. Menguji pemetaan Anda adalah topik yang sangat berbeda. Untuk menguji apakah logika berinteraksi dengan data dengan benar, Anda harus dapat mengontrol toko.
Sinaesthetic
7
Tidak ada seorang pun di pagar tentang apakah Anda harus unit menguji Entity Framework dengan sendirinya. Yang terjadi adalah Anda perlu menguji beberapa metode yang melakukan beberapa hal dan juga membuat panggilan EF ke database. Tujuannya adalah untuk mengejek EF sehingga Anda dapat menguji metode ini tanpa memerlukan database di server build Anda.
The Muffin Man
4
Saya sangat suka perjalanannya. Terima kasih telah menambahkan suntingan dari waktu ke waktu - ini seperti membaca kontrol sumber dan memahami bagaimana pemikiran Anda telah berkembang. Saya sangat menghargai perbedaan fungsional (dengan EF) dan unit (mengejek EF) juga.
Tom Leys
21

Umpan Balik Pengalaman Usaha di sini

Setelah banyak membaca saya telah menggunakan Usaha dalam pengujian saya: selama tes, Konteks dibuat oleh pabrik yang mengembalikan versi dalam memori, yang memungkinkan saya menguji papan tulis kosong setiap kali. Di luar pengujian, pabrik diselesaikan ke salah satu yang mengembalikan seluruh Konteks.

Namun saya merasa bahwa pengujian terhadap fitur tiruan lengkap dari database cenderung menyeret tes ke bawah; Anda sadar Anda harus berhati-hati dalam mengatur sejumlah besar dependensi untuk menguji satu bagian dari sistem. Anda juga cenderung melayang ke arah menyusun tes bersama yang mungkin tidak terkait, hanya karena hanya ada satu objek besar yang menangani semuanya. Jika Anda tidak memperhatikan, Anda mungkin akan melakukan pengujian integrasi alih-alih pengujian unit

Saya lebih suka pengujian terhadap sesuatu yang lebih abstrak daripada DBContext besar tetapi saya tidak bisa menemukan titik manis antara tes bermakna dan tes telanjang. Kapur hingga pengalaman saya.

Jadi saya menemukan Usaha menarik; jika Anda perlu menjalankannya, itu adalah alat yang baik untuk segera memulai dan mendapatkan hasil. Namun saya berpikir bahwa sesuatu yang sedikit lebih elegan dan abstrak harus menjadi langkah selanjutnya dan itulah yang akan saya selidiki selanjutnya. Mengunggulkan posting ini untuk melihat kemana selanjutnya :)

Sunting untuk ditambahkan : Upaya memang butuh waktu untuk melakukan pemanasan, jadi Anda sedang mencari kira-kira. 5 detik saat pengujian dimulai. Ini mungkin menjadi masalah bagi Anda jika Anda perlu ruang uji Anda menjadi sangat efisien.


Diedit untuk klarifikasi:

Saya menggunakan Upaya untuk menguji aplikasi layanan web. Setiap pesan M yang masuk dirutekan ke IHandlerOf<M>via Windsor. Castle.Windsor menyelesaikan IHandlerOf<M>yang memulihkan dependensi komponen. Salah satu dari dependensi ini adalahDataContextFactory , yang memungkinkan pawang meminta pabrik

Dalam pengujian saya, saya instantiate komponen IHandlerOf secara langsung, mengejek semua sub-komponen SUT dan menangani upaya yang dibungkus DataContextFactoryke handler.

Itu berarti bahwa saya tidak menguji unit dalam arti yang ketat, karena DB terkena tes saya. Namun seperti yang saya katakan di atas, biarkan saya mulai bekerja dan saya bisa dengan cepat menguji beberapa poin dalam aplikasi

samy
sumber
Terima kasih atas masukannya, apa yang dapat saya lakukan karena saya harus menjalankan proyek ini karena ini adalah pekerjaan pembayaran yang bonafide dimulai dengan beberapa repo dan lihat bagaimana saya maju, tetapi Usaha ini sangat menarik. Tidak tertarik pada lapisan apa Anda telah menggunakan upaya dalam aplikasi Anda?
Modika
2
hanya jika Usaha telah mendukung transaksi dengan benar
Sedat Kapanoglu
dan upaya memiliki bug untuk string dengan csv loader, ketika kami menggunakan '' bukan null dalam string.
Sam
13

Jika Anda ingin menyatukan kode uji maka Anda perlu mengisolasi kode yang ingin Anda uji (dalam hal ini layanan Anda) dari sumber daya eksternal (mis. Database). Anda mungkin dapat melakukan ini dengan semacam penyedia EF di memori , namun cara yang jauh lebih umum adalah dengan mengabstraksikan implementasi EF Anda misalnya dengan semacam pola penyimpanan. Tanpa isolasi ini, setiap tes yang Anda tulis akan menjadi tes integrasi, bukan tes unit.

Adapun pengujian kode EF - Saya menulis tes integrasi otomatis untuk repositori saya yang menulis berbagai baris ke database selama inisialisasi mereka, dan kemudian memanggil implementasi repositori saya untuk memastikan bahwa mereka berperilaku seperti yang diharapkan (misalnya memastikan bahwa hasilnya disaring dengan benar, atau bahwa mereka diurutkan dalam urutan yang benar).

Ini adalah tes integrasi bukan tes unit, karena tes bergantung pada adanya koneksi database yang ada, dan bahwa database target sudah memiliki skema terbaru yang diinstal.

Justin
sumber
Terima kasih @justin saya tahu tentang pola Repositori, tetapi membaca hal-hal seperti ayende.com/blog/4784/… dan lostechies.com/jimmybogard/2009/09/11/wither-the- repositori antara lain telah membuat saya berpikir bahwa saya tidak Saya ingin lapisan abstraksi ini, tetapi sekali lagi ini berbicara lebih banyak tentang pendekatan Query juga yang menjadi sangat membingungkan.
Modika
7
@Modika Ayende telah memilih implementasi yang buruk dari pola repositori untuk dikritik, dan sebagai hasilnya adalah 100% benar - ini direkayasa berlebihan dan tidak menawarkan manfaat apa pun. Implementasi yang baik mengisolasi bagian-bagian yang dapat diuji unit dari kode Anda dari implementasi DAL. Menggunakan NHibernate dan EF secara langsung membuat kode sulit (jika bukan tidak mungkin) untuk unit test dan mengarah pada basis kode monolitik yang kaku. Saya masih agak skeptis terhadap pola repositori, namun saya 100% yakin bahwa Anda perlu mengisolasi implementasi DAL Anda dan repositori adalah hal terbaik yang saya temukan sejauh ini.
Justin
2
@Modika Baca artikel kedua lagi. "Aku tidak ingin lapisan abstraksi ini" bukan apa yang dia katakan. Plus, baca tentang pola Repositori asli dari Fowler ( martinfowler.com/eaaCatalog/repository.html ) atau DDD ( dddcommunity.org/resources/ddd_terms ). Jangan percaya penentang tanpa sepenuhnya memahami konsep asli. Apa yang sebenarnya mereka kritik adalah penyalahgunaan pola baru-baru ini, bukan pola itu sendiri (walaupun mereka mungkin tidak tahu ini).
guillaume31
1
@ guillaume31 saya tidak menentang pola repositori (saya memahaminya) saya hanya mencoba mencari tahu apakah saya memerlukannya untuk abstrak apa yang sudah merupakan abstraksi pada tingkat itu, dan jika saya dapat menghilangkannya dan menguji terhadap EF secara langsung dengan mengejek dan menggunakannya dalam pengujian saya di lapisan yang lebih tinggi di aplikasi saya. Selain itu, jika saya tidak menggunakan repo saya mendapat manfaat dari set fitur extended EF, dengan repo saya mungkin tidak mendapatkannya.
Modika
Setelah saya mengisolasi DAL dengan repositori saya perlu cara "Mock" database (EF). Sejauh ini mengejek konteks dan berbagai ekstensi async (ToListAsync (), FirstOrDefaultAsync (), dll.) Telah mengakibatkan frustrasi bagi saya.
Kevin Burton
9

Jadi, inilah masalahnya, Entity Framework adalah implementasi sehingga terlepas dari kenyataan bahwa ia mengabstraksi kompleksitas interaksi basis data, berinteraksi secara langsung masih merupakan penggabungan yang ketat dan itulah mengapa membingungkan untuk diuji.

Pengujian unit adalah tentang menguji logika suatu fungsi dan masing-masing hasil potensial dalam isolasi dari setiap dependensi eksternal, yang dalam hal ini adalah penyimpanan data. Untuk melakukan itu, Anda harus dapat mengontrol perilaku penyimpanan data. Misalnya, jika Anda ingin menegaskan bahwa fungsi Anda mengembalikan false jika pengguna yang diambil tidak memenuhi beberapa kriteria, maka penyimpanan data [mocked] Anda harus dikonfigurasi untuk selalu mengembalikan pengguna yang gagal memenuhi kriteria, dan sebaliknya sebaliknya untuk pernyataan sebaliknya.

Dengan mengatakan itu, dan menerima kenyataan bahwa EF adalah sebuah implementasi, saya mungkin akan menyukai gagasan untuk mengabstraksi repositori. Tampak agak berlebihan? Bukan, karena Anda memecahkan masalah yang mengisolasi kode Anda dari implementasi data.

Dalam DDD, repositori hanya mengembalikan akar agregat, bukan DAO. Dengan begitu, konsumen repositori tidak pernah harus tahu tentang implementasi data (sebagaimana seharusnya) dan kita dapat menggunakannya sebagai contoh cara mengatasi masalah ini. Dalam hal ini, objek yang dihasilkan oleh EF adalah DAO dan karenanya, harus disembunyikan dari aplikasi Anda. Ini manfaat lain dari repositori yang Anda tetapkan. Anda dapat mendefinisikan objek bisnis sebagai jenis pengembaliannya alih-alih objek EF. Sekarang yang dilakukan repo adalah menyembunyikan panggilan ke EF dan memetakan respons EF terhadap objek bisnis yang ditentukan dalam tanda tangan repo. Sekarang Anda dapat menggunakan repo itu di tempat ketergantungan DbContext yang Anda suntikkan ke dalam kelas Anda dan akibatnya, sekarang Anda dapat mengejek antarmuka itu untuk memberi Anda kontrol yang Anda butuhkan untuk menguji kode Anda secara terpisah.

Ini sedikit lebih banyak pekerjaan dan banyak ibu jari mereka di itu, tetapi itu memecahkan masalah nyata. Ada penyedia di-memori yang disebutkan dalam jawaban berbeda yang bisa menjadi pilihan (saya belum mencobanya), dan keberadaannya adalah bukti perlunya latihan.

Saya benar-benar tidak setuju dengan jawaban teratas karena menghindari masalah sebenarnya yang mengisolasi kode Anda dan kemudian bersinggungan dengan pengujian pemetaan Anda. Dengan segala cara, uji pemetaan Anda jika Anda mau, tetapi atasi masalah yang sebenarnya di sini dan dapatkan cakupan kode nyata.

Sinaesthetic
sumber
8

Saya tidak akan membuat kode tes yang bukan milik saya. Apa yang Anda uji di sini, bahwa kompiler MSFT berfungsi?

Yang mengatakan, untuk membuat kode ini dapat diuji, Anda hampir HARUS membuat lapisan akses data Anda terpisah dari kode logika bisnis Anda. Apa yang saya lakukan adalah mengambil semua barang EF saya dan memasukkannya ke dalam (atau beberapa) kelas DAO atau DAL yang juga memiliki antarmuka yang sesuai. Kemudian saya menulis layanan saya yang akan menyuntikkan objek DAO atau DAL sebagai dependensi (lebih disukai injeksi konstruktor) sebagai antarmuka. Sekarang bagian yang perlu diuji (kode Anda) dapat dengan mudah diuji dengan mengejek antarmuka DAO dan memasukkannya ke dalam instance layanan Anda di dalam unit test Anda.

//this is testable just inject a mock of IProductDAO during unit testing
public class ProductService : IProductService
{
    private IProductDAO _productDAO;

    public ProductService(IProductDAO productDAO)
    {
        _productDAO = productDAO;
    }

    public List<Product> GetAllProducts()
    {
        return _productDAO.GetAll();
    }

    ...
}

Saya akan menganggap Lapisan Akses Data langsung sebagai bagian dari pengujian integrasi, bukan pengujian unit. Saya telah melihat orang-orang menjalankan verifikasi pada berapa banyak perjalanan ke database hibernate membuat sebelumnya, tetapi mereka berada di proyek yang melibatkan miliaran catatan di datastore mereka dan perjalanan ekstra itu sangat penting.

Jonathan Henson
sumber
1
Terima kasih atas jawabannya, tetapi apa bedanya dengan mengatakan Repositori di mana Anda menyembunyikan internal EF di belakangnya pada level ini? Saya tidak benar-benar ingin mengabstraksi EF, walaupun saya mungkin masih melakukannya dengan antarmuka IContext? Saya baru dalam hal ini, bersikaplah lembut :)
Modika
3
@Modika A Repo juga baik-baik saja. Pola apa pun yang Anda inginkan. "Saya benar-benar tidak ingin mengabstraksi EF" Apakah Anda ingin kode yang dapat diuji atau tidak?
Jonathan Henson
1
@Modika poin saya adalah Anda tidak akan memiliki kode APA PUN yang dapat diuji jika Anda tidak memisahkan masalah Anda. Akses Data dan Logika Bisnis HARUS berada di lapisan terpisah untuk melakukan tes yang dapat dikelola dengan baik.
Jonathan Henson
2
saya hanya merasa tidak perlu membungkus EF dalam abstraksi repositori karena pada dasarnya IDbSet adalah repo dan konteksnya UOW, saya akan memperbarui pertanyaan saya sedikit karena mungkin menyesatkan. Masalahnya datang dengan abstraksi dan titik utama adalah apa sebenarnya yang saya uji karena antrian saya tidak akan berjalan dalam batas yang sama (linq-to-entitas vs linq-to-objects) jadi jika saya hanya menguji bahwa layanan saya membuat panggilan yang tampaknya agak boros atau apakah saya kaya di sini?
Modika
1
, Sementara saya setuju dengan poin umum Anda, DbContext adalah unit kerja dan IDbSets pasti beberapa untuk implementasi repositori, dan saya bukan satu-satunya yang berpikir begitu. Saya dapat mengejek EF, dan pada beberapa lapisan saya perlu menjalankan tes integrasi, apakah itu penting jika saya melakukannya di Repositori atau lebih jauh dalam Layanan? Dipasangkan dengan erat ke DB sebenarnya bukan masalah, saya yakin itu terjadi tetapi saya tidak akan merencanakan sesuatu yang mungkin tidak terjadi.
Modika
8

Saya telah meraba-raba suatu waktu untuk mencapai pertimbangan ini:

1- Jika aplikasi saya mengakses database, mengapa tes tidak boleh? Bagaimana jika ada yang salah dengan akses data? Tes harus mengetahuinya sebelumnya dan mengingatkan diri sendiri tentang masalahnya.

2- Pola Repositori agak sulit dan memakan waktu.

Jadi saya datang dengan pendekatan ini, yang menurut saya bukan yang terbaik, tetapi memenuhi harapan saya:

Use TransactionScope in the tests methods to avoid changes in the database.

Untuk melakukannya perlu:

1- Pasang EntityFramework ke dalam Proyek Uji. 2- Masukkan string koneksi ke file app.config dari Proyek Uji. 3- Referensi Sistem dll. Transaksi dalam Proyek Uji.

Efek samping yang unik adalah bahwa identitas benih akan bertambah ketika mencoba memasukkan, bahkan ketika transaksi dibatalkan. Tetapi karena tes dilakukan terhadap database pengembangan, ini seharusnya tidak menjadi masalah.

Kode sampel:

[TestClass]
public class NameValueTest
{
    [TestMethod]
    public void Edit()
    {
        NameValueController controller = new NameValueController();

        using(var ts = new TransactionScope()) {
            Assert.IsNotNull(controller.Edit(new Models.NameValue()
            {
                NameValueId = 1,
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }

    [TestMethod]
    public void Create()
    {
        NameValueController controller = new NameValueController();

        using (var ts = new TransactionScope())
        {
            Assert.IsNotNull(controller.Create(new Models.NameValue()
            {
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }
}
Marquinho Peli
sumber
1
Sebenarnya, saya sangat menyukai solusi ini. Super sederhana untuk mengimplementasikan dan skenario pengujian yang lebih realistis. Terima kasih!
slopapa
1
dengan EF 6, Anda akan menggunakan DbContext.Database.BeginTransaction, bukan?
SwissCoder
5

Singkatnya saya akan mengatakan tidak, jus tidak layak diperas untuk menguji metode layanan dengan satu baris yang mengambil data model. Dalam pengalaman saya, orang-orang yang baru mengenal TDD ingin menguji sepenuhnya segalanya. Berangan tua dari abstrak fasad ke kerangka kerja pihak ke-3 supaya Anda bisa membuat tiruan dari kerangka kerja yang digunakan bastardise / luas sehingga Anda bisa menyuntikkan data dummy memiliki nilai kecil dalam pikiran saya. Setiap orang memiliki pandangan berbeda tentang seberapa banyak pengujian unit terbaik. Saya cenderung lebih pragmatis akhir-akhir ini dan bertanya pada diri sendiri apakah pengujian saya benar-benar menambah nilai pada produk akhir, dan berapa biayanya.

Silahkan masuk
sumber
1
Ya untuk pragmatisme. Saya masih berpendapat bahwa kualitas pengujian unit Anda lebih rendah daripada kualitas kode asli Anda. Tentu saja ada nilai dalam menggunakan TDD untuk meningkatkan praktik pengkodean Anda, dan juga untuk meningkatkan pemeliharaan, tetapi TDD dapat memiliki nilai yang semakin berkurang. Kami menjalankan semua pengujian kami terhadap basis data, karena memberi kami keyakinan bahwa penggunaan EF dan tabel itu sendiri adalah baik. Tes memang membutuhkan waktu lebih lama untuk dijalankan, tetapi lebih dapat diandalkan.
Savage
3

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.

kamranicus
sumber
Terima kasih atas jawabannya, ini adalah deskripsi yang bagus tentang Pola QueryObject menggunakan Mediator, dan sesuatu yang saya mulai mendorong dalam proyek saya juga. Saya mungkin harus memperbarui pertanyaan tetapi saya tidak lagi menguji unit EF, abstraksinya terlalu bocor (SqlLite mungkin baik-baik saja) jadi saya hanya menguji hal-hal integrasi saya yang meminta database dan unit menguji aturan bisnis dan logika lainnya.
Modika
3

Ada Upaya yang merupakan penyedia basis data kerangka entitas memori. Saya belum benar-benar mencobanya ... Haa baru saja melihat ini disebutkan dalam pertanyaan!

Atau Anda bisa beralih ke EntityFrameworkCore yang memiliki built-in penyedia basis data memori.

https://blog.goyello.com/2016/07/14/save-time-mocking-use-your-real-entity-framework-dbcontext-in-unit-tests/

https://github.com/tamasflamich/effort

Saya menggunakan pabrik untuk mendapatkan konteks, jadi saya bisa membuat konteks dekat dengan penggunaannya. Ini tampaknya bekerja secara lokal di studio visual tetapi tidak di server saya membangun TeamCity, belum yakin mengapa.

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");
andrew pate
sumber
Hai andrew, masalah ini tidak pernah mendapatkan konteks, Anda dapat membuat konteks yang kami lakukan, mengabstraksi konteks dan membuatnya dibangun oleh pabrik. Masalah terbesar adalah konsistensi dari apa yang ada dalam memori vs apa yang dilakukan Linq4Entities, mereka tidak sama yang dapat menyebabkan tes menyesatkan. Saat ini, kami hanya melakukan integrasi basis data, mungkin bukan proses terbaik untuk semua orang.
Modika
Bantuan Moq ini berfungsi ( codeproject.com/Tips/1045590/... ) jika Anda memiliki konteks untuk mengejek. Jika Anda mendukung konteks mocked-out dengan daftar itu tidak akan berperilaku seperti konteks yang didukung oleh database sql.
andrew pate
2

Saya suka memisahkan filter saya dari bagian lain dari kode dan mengujinya saat saya garis besar di blog saya di sini http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

Yang sedang berkata, logika filter yang diuji tidak identik dengan logika filter yang dijalankan ketika program dijalankan karena terjemahan antara ekspresi LINQ dan bahasa permintaan yang mendasarinya, seperti T-SQL. Namun, ini memungkinkan saya untuk memvalidasi logika filter. Saya tidak terlalu khawatir tentang terjemahan yang terjadi dan hal-hal seperti sensitivitas huruf dan penanganan nol hingga saya menguji integrasi antara lapisan.

Grax32
sumber
0

Penting untuk menguji apa yang Anda harapkan dilakukan oleh kerangka kerja entitas (yaitu memvalidasi harapan Anda). Salah satu cara untuk melakukan ini yang telah saya gunakan dengan sukses, adalah menggunakan moq seperti yang ditunjukkan dalam contoh ini (terlalu lama untuk menyalin jawaban ini):

https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

Namun hati-hati ... Konteks SQL tidak dijamin untuk mengembalikan barang-barang dalam urutan tertentu kecuali Anda memiliki "OrderBy" yang sesuai dalam permintaan linq Anda, jadi mungkin untuk menulis hal-hal yang lulus ketika Anda menguji menggunakan daftar dalam-memori ( linq-to-entitas) tetapi gagal di lingkungan uat / live Anda ketika (linq-to-sql) digunakan.

andrew pate
sumber