İnsanlar Entity Framework 6 ile nasıl birim test yapıyorlar?


170

Genel olarak Birim testleri ve TDD ile başlıyorum. Daha önce dabbledim ama şimdi bunu iş akışıma eklemeye ve daha iyi yazılım yazmaya kararlıyım.

Dün buna böyle bir soru sordum, ama kendi başına bir soru gibi görünüyor. İş mantığını denetleyicilerden soyutlamak ve EF6 kullanarak belirli modellere ve veri etkileşimlerine eşlemek için kullanacağım bir hizmet sınıfı uygulamaya başlamak için oturdum.

Sorun, EF'i bir depoda soyutlamak istemediğim için (zaten belirli sorgular için hizmetlerin dışında hala kullanılabilir olacak) ve hizmetlerimi test etmek istediğimden (EF Bağlamı kullanılacak) .

Sanırım soru şu, bunu yapmanın bir anlamı var mı? Eğer öyleyse, insanlar IQueryable'ın neden olduğu sızıntılı soyutlamalar ve Ladislav Mrnka'nın birçok harika mesajı ışığında vahşi doğada nasıl yapıyorlar birim test konusundaki bir bellekte çalışırken Linq sağlayıcılarındaki farklılıklar nedeniyle nasıl düzgün olmadığı konusunda belirli bir veritabanına uygulandığı şekliyle uygulama.

Test etmek istediğim kod oldukça basit görünüyor. (Bu sadece ne yaptığımı anlamak ve denemek için kukla kod, TDD kullanarak oluşturma sürücü istiyorum)

bağlam

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)
    {

    }
}

Hizmet

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;
    }
}

Şu anda birkaç şey yapmanın zihnindeyim:

  1. Alaycı EF Bağlamı böyle bir şeyle alay - Birim Testinde Alaycı EF benzer bir bağlamla - Alaycı veya doğrudan moq gibi arayüzde alaycı bir çerçeve kullanıldığında - birim testlerinin geçebileceği ama mutlaka sonuna kadar çalışıp Entegrasyon testleri ile desteklenmediği için alaycı bir çerçeve kullanıldığında?
  2. Belki EF ile alay etmek için Çaba gibi bir şey kullanıyorum - hiç kullanmadım ve başkalarının vahşi doğada kullanıp kullanmadığından emin değilim?
  3. EF'yi geri çağıran herhangi bir şeyi test etmekten rahatsız olmayın - bu yüzden EF'i doğrudan çağıran hizmet yöntemleri (getAll vb.) Birim testinden değil, yalnızca entegrasyon testinden mi geçiyor?

Dışarıda kimse bunu bir Repo olmadan orada yapıyor ve başarılı mı?


Hey Modika, son zamanlarda bunu düşünüyordum (bu soru yüzünden: stackoverflow.com/questions/25977388/… ) İçinde şu anda nasıl çalıştığımı biraz daha resmi olarak anlatmaya çalışıyorum, ancak nasıl olduğunu duymak isterim sen yapıyorsun.
samy

Merhaba @samy, bunu yapmaya karar verdiğimiz yol, EF'e doğrudan dokunan hiçbir şeyi birim test etmek değildi. Sorgular test edildi, ancak birim testler değil, entegrasyon testi olarak. Alaycı EF biraz kirli hissediyor, ancak bu proje küçüktü, bu yüzden bir veritabanına isabet eden testlerin bir sürü performansının etkisi gerçekten endişe verici değildi, bu yüzden biraz daha pragmatik olabiliriz. Hala en iyi yaklaşımın sizin için tamamen doğru olacağından% 100 emin değilim, bir noktada EF'e (ve DB'nize) vuracaksınız ve birim testleri burada bana doğru gelmiyor.
Modika

Yanıtlar:


186

Bu çok ilgilendiğim bir konu. EF ve NHibernate gibi teknolojileri test etmemeniz gerektiğini söyleyen pek çok safkan var. Haklılar, zaten çok sıkı bir şekilde test ediliyorlar ve önceki bir cevabın belirttiği gibi, sahip olmadığınız şeyleri test etmek için büyük miktarda zaman harcamanız genellikle anlamsız.

Ancak, altında veritabanı sahibi! Bence bu yaklaşım çöküyor, EF / NH'nin işlerini doğru bir şekilde yaptığını test etmenize gerek yok. Eşlemelerinizin / uygulamalarınızın veritabanınızla çalışıp çalışmadığını test etmeniz gerekir. Bence bu, test edebileceğiniz bir sistemin en önemli kısımlarından biridir.

Kesin olarak, birim test alanından ve entegrasyon testine geçiyoruz ancak prensipler aynı kalıyor.

Yapmanız gereken ilk şey, BLL'nizin EF ve SQL'den bağımsız olarak test edilebilmesi için DAL'nizi taklit etmek. Bunlar birim testleriniz. Daha sonra Entegrasyon Testlerinizi tasarlamanız gerekiyor DAL'nizi kanıtlamak , bence bunlar her şey kadar önemli.

Dikkate alınması gereken birkaç nokta var:

  1. Her testte veritabanınızın bilinen bir durumda olması gerekir. Çoğu sistem bunun için bir yedekleme kullanır veya komut dosyaları oluşturur.
  2. Her test tekrarlanabilir olmalıdır
  3. Her test atomik olmalıdır

Veritabanınızı ayarlamak için iki temel yaklaşım vardır, ilki bir UnitTest oluşturma DB komut dosyası çalıştırmaktır. Bu, birim test veritabanınızın her testin başında her zaman aynı durumda olmasını sağlar (bunu sağlamak için bunu sıfırlayabilir veya her işlemi bir işlemde çalıştırabilirsiniz).

Diğer seçeneğiniz benim yaptığım şeydir, her bir test için özel kurulumlar çalıştırın. Bunun iki ana nedenden dolayı en iyi yaklaşım olduğuna inanıyorum:

  • Veritabanınız daha basit, her test için tam bir şemaya ihtiyacınız yok
  • Her test daha güvenlidir, oluşturma komut dosyanızdaki bir değeri değiştirirseniz düzinelerce başka testi geçersiz kılmaz.

Maalesef buradaki uzlaşmanız hızdır. Tüm bu testleri çalıştırmak, tüm bu kurulum / yıkma komut dosyalarını çalıştırmak zaman alır.

Son bir nokta, ORM'nizi test etmek için bu kadar büyük miktarda SQL yazmak çok zor olabilir. Burası çok kötü bir yaklaşım benimsediğim yer (buradaki püristler benimle aynı fikirde değiller). Testimi oluşturmak için ORM'mi kullanıyorum! Sistemimdeki her bir DAL testi için ayrı bir komut dosyasına sahip olmak yerine, nesneleri oluşturan, onları bağlama ekleyen ve kaydeden bir test kurulum aşaması var. Daha sonra testimi yaparım.

Bu ideal çözümden uzaktır, ancak pratikte (özellikle birkaç bin testiniz olduğunda) yönetilmesi çok daha kolay olduğunu düşünüyorum, aksi takdirde çok sayıda komut dosyası oluşturuyorsunuz. Saflık üzerinde pratiklik.

Şüphesiz birkaç yıl içinde (ay / gün) bu cevaba bakacağım ve yaklaşımlarım değiştikçe kendime katılmayacağım - ancak bu benim şu anki yaklaşımım.

Yukarıda söylediğim her şeyi denemek ve özetlemek için bu benim tipik DB entegrasyon testi:

[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);
  }
}

Burada dikkat edilmesi gereken en önemli şey, iki döngünün oturumlarının tamamen bağımsız olmasıdır. RunTest'i uygularken bağlamın işlendiğinden ve yok edildiğinden ve verilerinizin veritabanınızdan yalnızca ikinci bölüm için gelebildiğinden emin olmalısınız.

Düzenle 13/10/2014

Önümüzdeki aylarda bu modeli muhtemelen gözden geçireceğimi söylemiştim. Yukarıda savunduğum yaklaşımın yanında dururken test mekanizmamı biraz güncelledim. Şimdi TestSetup ve TestTearDown içinde varlıkları oluşturma eğilimindedir.

[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();
}

Ardından her mülkü ayrı ayrı test edin

[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);
}

Bu yaklaşımın birkaç nedeni vardır:

  • Ek veritabanı çağrısı yoktur (bir kurulum, bir söküm)
  • Testler çok daha ayrıntılıdır, her test bir özelliği doğrular
  • Setup / TearDown mantığı Test yöntemlerinin kendisinden kaldırılır

Bunun test sınıfını basitleştirdiğini ve testleri daha ayrıntılı hale getirdiğini hissediyorum ( tek varsayımlar iyidir )

Düzenle 5/3/2015

Bu yaklaşım üzerinde bir başka revizyon. Sınıf düzeyi kurulumlar, yükleme özellikleri gibi testler için çok yararlı olsa da, farklı kurulumların gerekli olduğu yerlerde daha az kullanışlıdır. Bu durumda, her vaka için yeni bir sınıf oluşturmak aşırıdır.

Buna yardımcı olmak için şimdi iki temel sınıfım var SetupPerTestve SingleSetup. Bu iki sınıf, çerçeveyi gerektiği gibi ortaya koyar.

Gelen SingleSetupilk düzenleme anlatıldığı gibi bir çok benzer bir mekanizma var. Bir örnek

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);
  }
}

Ancak, yalnızca doğru girişlerin yüklenmesini sağlayan başvurular SetupPerTest yaklaşımını kullanabilir

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());
      });
   }
}

Özet olarak, her iki yaklaşım da test etmeye çalıştığınız şeye bağlı olarak çalışır.


2
İşte entegrasyon testine farklı bir yaklaşım. TL; DR - Test verilerini ayarlamak, test başına bir işlemi geri almak için uygulamanın kendisini kullanın.
Gert Arnold

3
@ Liath, büyük tepki. EF testiyle ilgili şüphelerimi onayladınız. Sorum şu; örneğiniz gayet somut bir dava için. Ancak, belirttiğiniz gibi, yüzlerce varlığı test etmeniz gerekebilir. KURU prensibi (Kendinizi Tekrarlamayın) uyarınca, her seferinde aynı temel kod desenini tekrar etmeden çözümünüzü nasıl ölçeklendirirsiniz?
Jeffrey A. Gochin

4
Buna katılmıyorum çünkü bu konuyu tamamen ortadan kaldırıyor. Birim testi, işlevin mantığını test etmekle ilgilidir. OP örneğinde, mantığın bir veri deposuna bağımlılığı vardır. EF'yi test etmemeyi söylediğinde haklısın, ama sorun bu değil. Sorun kodunuzu veri deposundan ayrı olarak test etmektir. Eşlemenizi test etmek tamamen farklı bir konu imo. Mantığın verilerle doğru bir şekilde etkileşime girdiğini test etmek için mağazayı kontrol edebilmeniz gerekir.
Sinaesthetic

7
Entity Framework'ü tek başına birim test edip etmemeniz gerektiğine dair kimse yok. Olan şey, bazı şeyler yapan ve ayrıca veritabanına EF çağrısı yapmak için bazı yöntemleri test etmeniz gerektiğidir. Amaç EF'i taklit etmek, böylece bu yöntemi yapı sunucunuzda veritabanı gerektirmeden test edebilirsiniz.
Muffin Man

4
Yolculuğu gerçekten çok seviyorum. Zaman içinde düzenlemeler eklediğiniz için teşekkür ederiz. Kaynak kontrolünü okumak ve düşüncelerinizin nasıl geliştiğini anlamak gibidir. Fonksiyonel (EF ile) ve birim (alaycı EF) ayrımını da çok takdir ediyorum.
Tom Leys

21

Çaba Deneyimi Geribildirimi

Çok fazla okumadan sonra Çaba kullanıyorum Testlerimde : Testler sırasında Bağlam, her seferinde boş bir sayfaya karşı test etmeme izin veren bir bellek sürümü döndüren bir fabrika tarafından inşa edildi. Testlerin dışında fabrika, tüm Bağlamı döndüren bir çözüme dönüştürülür.

Ancak ben veritabanı tam özellikli bir sahte karşı test testleri sürüklemek eğilimindedir bir duygu var; sistemin bir bölümünü test etmek için bir grup bağımlılık kurmanız gerektiğinin farkındasınız. Ayrıca, her şeyi işleyen tek bir büyük nesne olduğu için, ilişkili olmayan testleri birlikte düzenlemeye yöneliyorsunuz. Dikkat etmiyorsanız, birim testi yerine entegrasyon testi yaptığınızı görebilirsiniz

Büyük bir DBContext yerine daha soyut bir şeye karşı test etmeyi tercih ederdim ama anlamlı testler ve çıplak kemik testleri arasındaki tatlı noktayı bulamadım. Tecrübem için tebeşirle.

Bu yüzden Çaba'yı ilginç buluyorum; Eğer yere koşmak gerekiyorsa hızlı bir şekilde başlamak ve sonuç almak için iyi bir araçtır. Ancak biraz daha zarif ve soyut bir şey bir sonraki adım olması gerektiğini düşünüyorum ve ben bir sonraki araştırma yapacağım budur. Bir sonraki nereye gideceğini görmek için bu gönderiyi favorilere ekleyerek :)

Eklemek için düzenleyin : Çaba ısınmak biraz zaman alır, bu yüzden yakl. Test başlangıcında 5 saniye. Test takımınızın çok verimli olması gerekiyorsa, bu sizin için bir sorun olabilir.


Açıklama için düzenlendi:

Webservice uygulamasını test etmek için Çaba kullandım. Girilen her mesaj M, IHandlerOf<M>Windsor aracılığıyla yönlendirilir . IHandlerOf<M>Windsor, bileşenin bağımlılıklarını ortadan kaldıran çözümü çözer . Bu bağımlılıklardan biriDataContextFactory , işleyicinin fabrika istemesini sağlayan

Testlerimde IHandlerOf bileşenini doğrudan başlatırım, SUT'un tüm alt bileşenlerini alay eder ve Çaba sarfını DataContextFactoryişleyiciye işler .

DB testlerim tarafından vurulduğundan, katı bir şekilde test yapmam anlamına gelir. Ancak yukarıda söylediğim gibi çalışmama izin verin ve hızlı bir şekilde uygulamadaki bazı noktaları test edebilirim


Giriş için teşekkürler, ben bir bonafide ödeme işi olduğu gibi bu projeyi çalıştırmak için ne yapabilirim bazı depoları ile başlamak ve nasıl almak görmek, ama çaba çok ilginç. Uygulamalarınızda hangi katmanı kullanarak çaba harcadınız?
Modika

2
Efor ancak işlemleri düzgün bir şekilde desteklemişse
Sedat Kapanoglu

ve çaba, dizelerde null yerine '' kullandığımızda csv yükleyicili dizeler için bir hata var.
Sam

13

Eğer isterseniz birim test kodu sonra dış kaynaklardan (bu durumda servis olarak) (örneğin veritabanları) testine istediğiniz kodunuzu izole etmek gerekir. Muhtemelen bunu bir çeşit bellek içi EF sağlayıcısı ile yapabilirsiniz , ancak çok daha yaygın bir yol, EF uygulamanızı örneğin bir tür havuz deseniyle soyutlamaktır. Bu izolasyon olmadan yazdığınız testler birim testleri değil, entegrasyon testleri olacaktır.

EF kodunu sınamaya gelince - Başlatma sırasında veritabanına çeşitli satırlar yazan havuzlarım için otomatik tümleştirme testleri yazıyorum ve sonra beklediğim gibi davrandıklarından emin olmak için havuz uygulamalarını çağırıyorum (örneğin, sonuçların doğru bir şekilde filtrelendiğinden emin olun veya doğru sırayla sıralanması).

Bunlar, bir veritabanı bağlantısının mevcut olmasına ve hedef veritabanının en son güncel şemaya sahip olduğundan, birim testleri değil, entegrasyon testleridir.


Teşekkürler @justin Depo desenini biliyorum, ancak ayende.com/blog/4784/… ve lostechies.com/jimmybogard/2009/09/11/wither-the-repository gibi şeyleri okumak beni Bu soyutlama katmanını istemiyorum, ama yine de bunlar çok kafa karıştırıcı hale gelen bir Sorgu yaklaşımı hakkında daha fazla konuşuyor.
Modika

7
@Modika Ayende, eleştirmek için veri havuzu modelinin zayıf bir uygulamasını seçti ve sonuç olarak% 100 doğru - aşırı tasarlanmış ve herhangi bir fayda sunmuyor. İyi bir uygulama, kodunuzun birim test edilebilir bölümlerini DAL uygulamasından yalıtır. NHibernate ve EF kullanımı kodun doğrudan birim testini zorlaştırmasını (imkansız değilse) zorlaştırır ve katı bir monolitik kod tabanına yol açar. Havuz desenine hala biraz şüpheci yaklaşıyorum, ancak% 100 DAL uygulamanızı bir şekilde izole etmeniz gerektiğine ikna oldum ve depo şimdiye kadar bulduğum en iyi şey.
Justin

2
@Modika İkinci makaleyi tekrar okuyun. “Bu soyutlama katmanını istemiyorum” diyor. Artı, Fowler (orijinal Depo deseni hakkında okuyun martinfowler.com/eaaCatalog/repository.html ) veya DDD ( dddcommunity.org/resources/ddd_terms ). Orijinal konsepti tam olarak anlamadan muhaliflere inanmayın. Gerçekten eleştirdikleri, desenin kendisi değil, desenin son zamanlarda yanlış kullanımıdır (muhtemelen bunu bilmiyor olsalar da).
guillaume31

1
@ guillaume31 ben depo desen karşı değilim (anlıyorum) ben sadece o düzeyde zaten bir soyutlama ne soyutlamak için ihtiyacım olup olmadığını anlamaya çalışıyorum, ve ben atlamak ve doğrudan alay ile EF karşı test ve uygulamamda daha yüksek bir katmandaki testlerimde kullanıyorum. Ayrıca, bir repo kullanmazsanız EF genişletilmiş özellik kümesi yararlanır, bir repo ile bunu elde olmayabilir.
Modika

Ben bir depo ile DAL izole ettikten sonra bir yere veritabanı (EF) "Mock" gerekir. Şimdiye kadar bağlam ve çeşitli zaman uyumsuz uzantıları (ToListAsync (), FirstOrDefaultAsync () vb.) Alay etmek benim için hayal kırıklığına neden oldu.
Kevin Burton

9

İşte önemli olan şey, Entity Framework, veritabanı etkileşiminin karmaşıklığını soyutlamasına rağmen, doğrudan etkileşim hala sıkı bağlantıdır ve bu yüzden test etmek kafa karıştırıcıdır.

Birim testi, bir fonksiyonun mantığını ve potansiyel sonuçlarının her birini, bu durumda veri deposu olan herhangi bir dış bağımlılıktan izole olarak test etmekle ilgilidir. Bunu yapmak için, veri deposunun davranışını kontrol edebilmeniz gerekir. Örneğin, getirilen kullanıcı bazı ölçütleri karşılamıyorsa işlevinizin false döndürdüğünü iddia etmek istiyorsanız, [alay edilmiş] veri deponuz her zaman ölçütleri karşılamayan bir kullanıcıyı döndürecek şekilde yapılandırılmalıdır. tam tersi iddia için.

Bununla birlikte ve EF'in bir uygulama olduğu gerçeğini kabul ederek, muhtemelen bir deposu soyutlama fikrini tercih ederim. Biraz gereksiz görünüyor mu? Bu değil, çünkü kodunuzu veri uygulamasından izole eden bir sorunu çözüyorsunuz.

DDD'de depolar, DAO yerine yalnızca toplu kökleri döndürür. Bu şekilde, deponun tüketicisi asla veri uygulaması hakkında bilmek zorunda değildir (olması gerektiği gibi) ve bunu bu sorunun nasıl çözüleceğinin bir örneği olarak kullanabiliriz. Bu durumda, EF tarafından oluşturulan nesne bir DAO'dur ve bu nedenle uygulamanızdan gizlenmelidir. Bu, tanımladığınız deponun bir başka yararıdır. Bir işletme nesnesini, EF nesnesi yerine dönüş türü olarak tanımlayabilirsiniz. Reponun yaptığı şey EF çağrılarını gizlemek ve EF yanıtını depo imzasında tanımlanan iş nesnesine eşlemek. Şimdi, sınıflarınıza enjekte ettiğiniz DbContext bağımlılığı yerine bu repoyu kullanabilirsiniz ve sonuç olarak, kodunuzu tek tek test etmek için ihtiyacınız olan kontrolü size vermek için bu arayüzü alay edebilirsiniz.

Biraz daha fazla iş var ve birçoğu burnunu başpartıyor, ancak gerçek bir sorunu çözüyor. Bir seçenek olabilen farklı bir cevapta bahsedilen bir bellek içi sağlayıcı var (denemedim) ve onun varlığı pratik ihtiyacının kanıtıdır.

En iyi yanıta tamamen katılmıyorum çünkü kodunuzu izole eden gerçek sorunu ortadan kaldırıyor ve daha sonra eşlemenizi test etme konusunda teğet oluyor. Elbette isterseniz eşlemenizi test edin, ancak asıl sorunu burada ele alın ve gerçek bir kod kapsamı alın.


8

Sahip olmadığım birim test kodunu vermem. Burada ne test ediyorsunuz, MSFT derleyicisinin çalıştığını?

Bununla birlikte, bu kodu test edilebilir yapmak için, veri erişim katmanınızı iş mantığı kodunuzdan ayrı hale getirmek zorundasınız. Yaptığım şey, tüm EF şeylerimi alıp karşılık gelen bir arayüze sahip bir (veya çoklu) DAO veya DAL sınıfına koymak. Daha sonra DAO veya DAL nesnesine arayüz olarak atıf yapılan bir bağımlılık (tercihen yapıcı enjeksiyonu) enjekte edilecek olan servisimi yazıyorum. Şimdi test edilmesi gereken parça (kodunuz) DAO arayüzünü takıp birim testinizdeki servis örneğinize enjekte ederek kolayca test edilebilir.

//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();
    }

    ...
}

Canlı Veri Erişim Katmanlarının birim testinin değil, entegrasyon testinin bir parçası olduğunu düşünürüm. Ben daha önce hazırda bekletme veritabanı kaç gezileri üzerinde doğrulama çalıştırmak adamlar gördüm, ama onlar kendi veri deposunda milyarlarca kayıt içeren bir proje vardı ve bu ekstra gezileri gerçekten önemli.


1
Cevabınız için teşekkürler, ancak bunun farkı EF'nin iç kısımlarını bu seviyede sakladığınız bir Depo demek olabilir mi? I soyut hala IContext arayüzü ile yapıyor olabilir rağmen, EF soyut istemiyorum? Ben bu konuda
yeniyim

3
@Modika A Repo da gayet iyi. İstediğiniz desen. "EF'i gerçekten soyutlamak istemiyorum" Test edilebilir kod ister misiniz?
Jonathan Henson

1
@Modika, endişelerinizi ayırmazsanız HERHANGİ BİR test edilebilir kodunuz olmayacak. Veri Erişimi ve İş Mantığı, iyi korunabilir testleri çıkarmak için ayrı katmanlarda olmalıdır ZORUNLU.
Jonathan Henson

2
Ben sadece aslında IDbSets repo's ve UOW bağlamı olarak EF bir depo soyutlama sarmak için nesessary hissetmedim, bu yanıltıcı olabilir gibi benim soru biraz güncelleyecektir. Sorun herhangi bir soyutlama ile geliyor ve ana nokta tam olarak ne test benim queiries aynı sınırlarda (linq-to-entities vs linq-to-objeler) çalışmayacak çünkü ben sadece benim hizmet yapar test eğer Bu biraz savurgan görünüyor ya da ben burada iyi miyim?
Modika

1
, Genel noktaları kabul ederken, DbContext bir iş birimidir ve IDbSets depo uygulaması için kesinlikle bazıları ve ben bunu düşünen tek kişi değilim. EF ile alay edebilirim ve bazı katmanlarda entegrasyon testlerini yürütmem gerekecek, bunu bir Havuzda mı yoksa bir Hizmette mi daha fazla yaparsam gerçekten önemli mi? Bir DB sıkıca bağlı olmak gerçekten bir endişe değil, eminim olur ama ben oluşmayacak bir şey için planlamak için gitmiyorum.
Modika

8

Bir zamanlar bu düşüncelere ulaşmak için uğraştım:

1- Uygulamam veritabanına erişirse, neden test yapılmasın? Veri erişiminde bir sorun varsa ne olur? Testler önceden bilmeli ve sorun hakkında kendimi uyarmalıyım.

2- Havuz Kalıbı biraz zor ve zaman alıcıdır.

Bu yaklaşımı buldum, en iyisi olduğunu düşünmüyorum ama beklentilerimi yerine getirdim:

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

Bunu yapmak için gerekli:

1- EntityFramework'u Test Projesine kurun. 2- Bağlantı dizesini Test Projesi'nin app.config dosyasına yerleştirin. 3- Test Projesinde dll Sistemi Referans İşlemleri.

Benzersiz yan etkisi, kimlik çekilmeye, işlem iptal edildiğinde bile eklemeye çalışırken artacaktır. Ancak testler bir geliştirme veritabanına karşı yapıldığından, bu sorun olmamalıdır.

Basit kod:

[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();
        }
    }
}

1
Aslında bu çözümü çok seviyorum. Uygulaması çok basit ve daha gerçekçi test senaryoları. Teşekkürler!
slopapa

1
EF 6 ile DbContext.Database.BeginTransaction kullanırsınız, değil mi?
SwissCoder

5

Kısacası hayır diyebilirim, meyve suyu, bir servis yöntemini model verilerini alan tek bir satırla test etmek için sıkmaya değmez. Tecrübelerime göre, TDD'de yeni olan insanlar kesinlikle her şeyi test etmek istiyorlar. Bir cepheyi 3. taraf çerçevesine soyutlamanın eski kestanesi, böylece kukla verileri enjekte edebilmeniz için piç yaptığınız / genişlettiğiniz çerçeveler API'sinin bir kopyasını oluşturabilirsiniz. Herkesin ne kadar birim testinin en iyi olduğuna dair farklı bir görüşü vardır. Bu günlerde daha pragmatik olma eğilimindeyim ve kendime testimin nihai ürüne gerçekten değer katıp katmadığını ve ne pahasına olduğunu soruyorum.


1
Pragmatikliğe evet. Ünite testlerinizin kalitesinin orijinal kodunuzun kalitesinden daha düşük olduğunu iddia ediyorum. Elbette kodlama uygulamanızı geliştirmek ve aynı zamanda sürdürülebilirliği artırmak için TDD kullanmanın değeri vardır, ancak TDD'nin değeri azalabilir. Tüm testlerimizi veri tabanına karşı yürütüyoruz, çünkü EF ve tabloların kullanımının sağlam olduğuna dair bize güven veriyor. Testlerin yapılması daha uzun sürer, ancak daha güvenilirdir.
Savage

3

Hakkında yorumlanan ve kısaca tartışılan bir yaklaşımı paylaşmak istiyorum, ancak şu anda EF tabanlı hizmetleri birim test etmek için kullandığım gerçek bir örnek göstermek istiyorum .

İlk olarak, EF Core'un bellek içi sağlayıcısını kullanmak isterdim, ancak bu EF 6 ile ilgili. Yine - bu özellikle EF tabanlı kodu çok fazla tören yapmadan test etmeye yardımcı olmak içindir .

İşte bir model hazırlarken sahip olduğum hedefler:

  • Takımdaki diğer geliştiricilerin anlaması basit olmalı
  • EF kodunu mümkün olan en düşük seviyede izole etmelidir
  • Tuhaf çoklu sorumluluk arayüzleri ("genel" veya "tipik" veri havuzu düzeni gibi) oluşturmayı içermemelidir
  • Birim testinde yapılandırılması ve ayarlanması kolay olmalıdır

Önceki ifadelere katılıyorum, EF hala bir uygulama detayı ve "saf" bir birim testi yapmak için soyutlamak gerekir gibi hissediyorum. İdeal olarak, EF kodunun kendisinin çalışmasını sağlamak istediğimi de kabul ediyorum - ancak bu bir sanal alan veritabanı, bellek içi sağlayıcı vb. İçerir. Yaklaşımım her iki sorunu da çözer - EF bağımlı kodu ve oluşturmak EF kodunuzu özel olarak test etmek için entegrasyon testleri.

Bunu başarabilmemin yolu, EF kodunu özel Sorgu ve Komut sınıflarına kapsüllemekti . Fikir basit: herhangi bir EF kodunu bir sınıfa sarın ve başlangıçta kullanacak olan sınıflardaki bir arayüze bağlı olun. Çözmem gereken temel sorun, sınıflara çok sayıda bağımlılık eklemek ve testlerimde çok fazla kod ayarlamaktan kaçınmaktı.

Yararlı, basit bir kütüphane burada devreye girer: Mediatr . Basit işlem içi mesajlaşmaya izin verir ve bunu kodu uygulayan işleyicilerin "isteklerini" ayırarak yapar. Bunun "ne" yi "nasıl" dan ayırmanın ek bir yararı vardır. Örneğin, EF kodunu küçük parçalara kapsülleyerek uygulamaları başka bir sağlayıcıyla veya tamamen farklı bir mekanizma ile değiştirmenize izin verir, çünkü yaptığınız tek şey bir eylem gerçekleştirmek için bir istek göndermektir.

Bağımlılık enjeksiyonunu kullanarak (çerçeveli veya çerçevesiz - tercihiniz), aracıyı kolayca alay edebilir ve birim kod EF testini etkinleştirmek için istek / yanıt mekanizmalarını kontrol edebiliriz.

İlk olarak, test etmemiz gereken iş mantığı olan bir hizmetimiz olduğunu varsayalım:

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
    // ...    
  }
}

Bu yaklaşımın faydasını görmeye başlıyor musunuz? Sadece EF ile ilgili tüm kodları açıklayıcı sınıflara açıkça dahil etmekle kalmaz, aynı zamanda bu isteğin "nasıl" ele alındığına dair uygulama kaygısını kaldırarak genişletilebilirliğe izin verirsiniz - bu sınıf ilgili nesnelerin EF, MongoDB'den gelip gelmediğini umursamaz, veya bir metin dosyası.

Şimdi istek ve işleyici için MediatR üzerinden:

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();
  }
}

Gördüğünüz gibi, soyutlama basit ve kapsüllenmiştir. Ayrıca kesinlikle test edilebilir, çünkü bir entegrasyon testinde bu sınıfı ayrı ayrı test edebilirsiniz - burada karışık iş endişeleri yoktur.

Peki, özellik hizmetimizin birim testi neye benziyor? Bu çok basit. Bu durumda, alay etmek için Moq kullanıyorum (sizi mutlu eden her şeyi kullanın):

[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
  }

}

İhtiyacımız olan tek şeyin tek bir kurulum olduğunu ve ekstra bir şey yapılandırmamıza gerek olmadığını görebilirsiniz - bu çok basit bir birim testidir. Açık olalım: Bu, Mediatr gibi bir şey yapmadan tamamen mümkündür (sadece bir arayüz uygular ve testler için alay edersiniz IGetRelevantDbObjectsQuery), ancak pratikte birçok özellik ve sorgu / komut içeren büyük bir kod tabanı için, kapsüllemeyi seviyorum ve doğuştan gelen DI desteği Mediatr sunuyor.

Bu sınıfları nasıl organize ettiğimi merak ediyorsanız, oldukça basit:

- MyProject
  - Features
    - MyFeature
      - Queries
      - Commands
      - Services
      - DependencyConfig.cs (Ninject feature modules)

Özellik dilimlerine göre düzenleme noktanın yanındadır, ancak bu, tüm ilgili / bağımlı kodu bir arada ve kolayca keşfedilebilir tutar. En önemlisi, Komut / Sorgu Ayırma ilkesini izleyerek Sorgularla Komutları ayırırım .

Bu, tüm kriterlerimi karşılıyor: Düşük tören, anlaşılması kolay ve ekstra gizli faydalar var. Örneğin, kaydetme değişikliklerini nasıl ele alırsınız? Artık bir rol arayüzü kullanarak Db Bağlamınızı basitleştirebilirsiniz (IUnitOfWork.SaveChangesAsync()) ve tek rol arayüzüne yapılan sahte aramalar veya RequestHandlers'ınızda taahhütte bulunmayı / geri almayı kapsülleyebilirsiniz - ancak bunu yapmayı sürdürdüğü sürece bunu yapmak size kalmıştır. Örneğin, sadece bir EF nesnesini geçireceğiniz tek bir genel istek / işleyici oluşturmak için cazip davrandım ve onu kaydeder / günceller / kaldırır - ama niyetinizin ne olduğunu sormanız ve eğer isterseniz işleyiciyi başka bir depolama sağlayıcısı / uygulaması ile değiştirin, muhtemelen ne yapmak istediğinizi gösteren açık komutlar / sorgular oluşturmalısınız. Çoğu zaman, tek bir hizmet veya özellik belirli bir şeye ihtiyaç duyar - buna ihtiyaç duymadan genel şeyler oluşturmayın.

Orada ders bu kalıba uyarılar - basit bir pub / sub mekanizması ile çok ileri gidebilir. Uygulamamı yalnızca EF ile ilgili kodu soyutlamakla sınırlandırdım, ancak maceracı geliştiriciler MediatR'ı denize girmek ve her şeyi mesajlaşmak için kullanmaya başlayabilir - iyi bir kod inceleme uygulamaları ve akran incelemeleri yakalamalı. Bu bir süreç meselesi, MediatR ile ilgili bir sorun değil, bu yüzden bu kalıbı nasıl kullandığınıza dikkat edin.

İnsanların EF'yi nasıl test ettikleri / alay ettikleri konusunda somut bir örnek istediniz ve bu bizim projemizde bizim için başarılı bir şekilde çalışan bir yaklaşım - ve ekip benimsemenin ne kadar kolay olduğu konusunda çok mutlu. Umarım bu yardımcı olur! Programlamadaki her şeyde olduğu gibi, çoklu yaklaşımlar vardır ve hepsi neyi başarmak istediğinize bağlıdır. Sadeliğe, kullanım kolaylığına, sürdürülebilirliğe ve keşfedilebilirliğe değer veriyorum - ve bu çözüm tüm bu talepleri karşılıyor.


Cevabınız için teşekkürler, bir Arabulucu kullanarak QueryObject Deseni'nin harika bir açıklaması ve projelerimde de itmeye başladığım bir şey. Soruyu güncellemek zorunda kalabilirim ama artık birim test EF değilim, soyutlamalar çok sızıntılı (SqlLite olsa da iyi olabilir), bu yüzden sadece entegrasyon test veritabanı ve birim testi iş kuralları ve diğer mantık sorgu şeyler.
Modika

3

Bir bellek varlık çerçeve veritabanı sağlayıcısı olan Çaba var. Aslında denemedim ... Haa sadece bu soruda belirtildi fark!

Alternatif olarak, yerleşik bir bellek veritabanı sağlayıcısı olan EntityFrameworkCore'a geçebilirsiniz.

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

https://github.com/tamasflamich/effort

Bir bağlam elde etmek için bir fabrika kullandım, böylece kullanımı yakın bağlam oluşturabilirim. Bu, görsel stüdyoda yerel olarak çalışıyor gibi görünüyor, ancak TeamCity derleme sunucumda değil, neden olduğundan emin değilim.

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");

Merhaba Andrew, sorun asla bağlamı elde etmiyordu, yaptığımız bağlamı fabrikadan çıkarabilir, bağlamı soyutlayabilir ve fabrika tarafından inşa ettirebilirsiniz. En büyük sorun, bellekte olanın ve Linq4Entities'in yaptığı şeyin tutarlılığıydı, yanıltıcı testlere yol açabilecek aynı değillerdi. Şu anda, biz sadece entegrasyon testi veritabanı şeyler, olmayabilir herkes için en iyi süreç olabilir zihin.
Modika

Bu Moq yardımcısı , alay edecek bir içeriğiniz varsa çalışır ( codeproject.com/Tips/1045590/… ). Mock-out bağlamını bir listeyle destekliyorsanız, bir sql veritabanı tarafından desteklenen bir bağlam gibi davranmayacaktır.
andrew pate

2

Filtrelerimi kodun diğer bölümlerinden ayırmak ve bunları blogumda özetlediğim gibi test etmek istiyorum http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

Bununla birlikte, test edilen filtre mantığı, LINQ ifadesi ve T-SQL gibi temel sorgu dili arasındaki çeviri nedeniyle program çalıştırıldığında yürütülen filtre mantığı ile aynı değildir. Yine de bu, filtrenin mantığını doğrulamamı sağlıyor. Katmanlar arasındaki entegrasyonu test edene kadar gerçekleşen çeviriler ve büyük / küçük harf duyarlılığı ve boş işleme gibi şeyler hakkında çok fazla endişelenmiyorum.


0

Varlık çerçevesinin ne yapmasını beklediğinizi test etmeniz önemlidir (yani beklentilerinizi doğrulayın). Başarılı bir şekilde kullandığım bunu yapmanın bir yolu, bu örnekte gösterildiği gibi adedi kullanmaktır (bu cevaba kopyalamak için uzun süre):

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

Ancak dikkatli olun ... Linq sorgunuzda uygun bir "OrderBy" olmadığı sürece SQL bağlamının belirli bir sırayla bir şeyler döndürmesi garanti edilmez, bu nedenle bellek içi bir liste kullanarak test ettiğinizde geçen şeyleri yazmak mümkündür ( linq-to-entities), ancak (linq---sql) kullanıldığında uat / live ortamınızda başarısız olur.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.