Üretim veritabanı sisteminizde test yapma

Bu sayfada, uygulamanın üretimde çalıştığı veritabanı sistemini içeren otomatikleştirilmiş testler yazma tekniklerini ele aacağız. Üretim veritabanı sisteminin test çiftleri tarafından değiştirildiği alternatif test yaklaşımları vardır; Daha fazla bilgi için teste genel bakış sayfasına bakın. Farklı veritabanı bir test çifti olarak kullanıldığından, üretimde kullanılandan (örneğin Sqlite) farklı bir veritabanına yönelik testlerin burada ele alınmadığını unutmayın; bu yaklaşım üretim veritabanı sisteminiz olmadan test etme bölümünde ele alınmıştır.

Gerçek bir veritabanı içeren testin ana engeli, paralel (hatta seri) çalışan testlerin birbiriyle karışmaması için doğru test yalıtımını sağlamaktır. Aşağıdakilerin tam örnek kodu burada görüntülenebilir.

Bahşiş

Bu sayfada xUnit teknikleri gösterilmektedir, ancak NUnit de dahil olmak üzere diğer test çerçevelerinde benzer kavramlar mevcuttur.

Veritabanı sisteminizi ayarlama

Günümüzde çoğu veritabanı sistemi hem CI ortamlarına hem de geliştirici makinelerine kolayca yüklenebilir. Veritabanını normal yükleme mekanizması aracılığıyla yüklemek sık sık kolay olsa da, çoğu büyük veritabanı için kullanıma hazır Docker görüntüleri kullanılabilir ve CI'de yüklemeyi özellikle kolaylaştırabilir. Geliştirici ortamı için GitHub Workspaces, Dev Container veritabanı da dahil olmak üzere tüm gerekli hizmetleri ve bağımlılıkları ayarlayabilir. Bunun için kuruluma ilk yatırım yapılması gerekir ancak bu işlem tamamlandıktan sonra bir çalışma testi ortamınız olur ve daha önemli şeylere odaklanabilirsiniz.

Bazı durumlarda veritabanlarının test için yararlı olabilecek özel bir sürümü veya sürümü vardır. SQL Server kullanırken LocalDB, neredeyse hiç kurulum olmadan testleri yerel olarak çalıştırmak, veritabanı örneğini isteğe bağlı olarak döndürmek ve daha az güçlü geliştirici makinelerinde kaynak tasarrufu yapmak için kullanılabilir. Ancak, LocalDB sorunları olmadan değildir:

  • SQL Server Developer Edition'ın yaptığı her şeyi desteklemez.
  • Yalnızca Windows'ta kullanılabilir.
  • Hizmet çalışır durumda olduğunda ilk test çalıştırmalarında gecikmeye neden olabilir.

Tam SQL Server özellik kümesini sağladığından ve genellikle çok kolay olduğundan, genellikle LocalDB yerine SQL Server Developer sürümünü yüklemenizi öneririz.

Bulut veritabanı kullanırken, hem hızı artırmak hem de maliyetleri azaltmak için veritabanının yerel bir sürümüne göre test etmek genellikle uygundur. Örneğin, üretimde SQL Azure kullanırken, yerel olarak yüklenmiş bir SQL Server'a karşı test edebilirsiniz; ikisi son derece benzerdir (ancak üretime geçmeden önce SQL Azure'a karşı testler çalıştırmak akıllıca olacaktır). Azure Cosmos DB kullanırken, Azure Cosmos DB öykünücüsü hem yerel olarak geliştirme hem de testleri çalıştırma için kullanışlı bir araçtır.

Test veritabanı oluşturma, dağıtma ve yönetme

Veritabanınız yüklendikten sonra testlerinizde kullanmaya başlayabilirsiniz. Çoğu basit durumda, test paketinizin birden çok test sınıfı arasında paylaşılan tek bir veritabanı vardır, bu nedenle test çalıştırmasının ömrü boyunca veritabanının tam olarak bir kez oluşturulduğundan ve dağıtıldığından emin olmak için bir mantığa ihtiyacımız vardır.

Xunit kullanılırken bu, veritabanını temsil eden ve birden çok test çalıştırması arasında paylaşılan bir sınıf fikstür aracılığıyla yapılabilir:

public class TestDatabaseFixture
{
    private const string ConnectionString = @"Server=(localdb)\mssqllocaldb;Database=EFTestSample;Trusted_Connection=True;ConnectRetryCount=0";

    private static readonly object _lock = new();
    private static bool _databaseInitialized;

    public TestDatabaseFixture()
    {
        lock (_lock)
        {
            if (!_databaseInitialized)
            {
                using (var context = CreateContext())
                {
                    context.Database.EnsureDeleted();
                    context.Database.EnsureCreated();

                    context.AddRange(
                        new Blog { Name = "Blog1", Url = "http://blog1.com" },
                        new Blog { Name = "Blog2", Url = "http://blog2.com" });
                    context.SaveChanges();
                }

                _databaseInitialized = true;
            }
        }
    }

    public BloggingContext CreateContext()
        => new BloggingContext(
            new DbContextOptionsBuilder<BloggingContext>()
                .UseSqlServer(ConnectionString)
                .Options);
}

Yukarıdaki fikstür örneği oluşturulurken, veritabanını bırakmak (önceki bir çalıştırmadan mevcut olması durumunda) ve ardından EnsureCreated() en son model yapılandırmanızla oluşturmak için kullanır EnsureDeleted() (bu API'lerin belgelerine bakın). Veritabanı oluşturulduktan sonra fikstür, testlerimizin kullanabileceği bazı verilerle onu tohumlar. Daha sonra yeni bir test için değiştirmek mevcut testlerin başarısız olmasına neden olabileceğinden, tohum verilerinizi düşünmeye zaman harcamaya değer.

Fikstürün bir test sınıfında kullanılması için fikstür türünüz üzerinden uygulamanız IClassFixture yeterlidir ve xUnit bunu oluşturucunuza ekler:

public class BloggingControllerTest : IClassFixture<TestDatabaseFixture>
{
    public BloggingControllerTest(TestDatabaseFixture fixture)
        => Fixture = fixture;

    public TestDatabaseFixture Fixture { get; }

Test sınıfınızın artık tam işlevsel bir Fixture bağlam örneği oluşturmak için testler tarafından kullanılabilecek bir özelliği vardır:

[Fact]
public void GetBlog()
{
    using var context = Fixture.CreateContext();
    var controller = new BloggingController(context);

    var blog = controller.GetBlog("Blog2").Value;

    Assert.Equal("http://blog2.com", blog.Url);
}

Son olarak, yukarıdaki fikstürün oluşturma mantığında bazı kilitler fark etmiş olabilirsiniz. Fikstür yalnızca tek bir test sınıfında kullanılıyorsa, xUnit tarafından tam olarak bir kez örneği oluşturulacağı garanti edilir; ancak aynı veritabanı fikstürünün birden çok test sınıfında kullanılması yaygındır. xUnit koleksiyon fikstürleri sağlar, ancak bu mekanizma test sınıflarınızın paralel çalışmasını engeller. Bu, test performansı için önemlidir. Bunu bir xUnit sınıf fikstür ile güvenli bir şekilde yönetmek için veritabanı oluşturma ve tohumlama ile ilgili basit bir kilit alıyoruz ve bunu asla iki kez yapmak zorunda olmadığımızı emin olmak için statik bir bayrak kullanıyoruz.

Verileri değiştiren testler

Yukarıdaki örnekte, test yalıtımı açısından kolay bir durum olan salt okunur bir test gösterildi: Hiçbir şey değiştirilmediğinden test girişimi mümkün değildir. Buna karşılık, verileri değiştiren testler birbiriyle karışabileceğinden daha sorunludur. Yazma testlerini yalıtmak için yaygın kullanılan tekniklerden biri testi bir işlemde sarmalama ve bu işlemin testin sonunda geri alınmasıdır. Veritabanına hiçbir şey işlenmediğinden, diğer testler herhangi bir değişiklik görmez ve girişimlerden kaçınılır.

Veritabanımıza blog ekleyen bir denetleyici yöntemi aşağıdadır:

[HttpPost]
public ActionResult AddBlog(string name, string url)
{
    _context.Blogs.Add(new Blog { Name = name, Url = url });
    _context.SaveChanges();

    return Ok();
}

Bu yöntemi aşağıdakilerle test edebiliriz:

[Fact]
public void AddBlog()
{
    using var context = Fixture.CreateContext();
    context.Database.BeginTransaction();

    var controller = new BloggingController(context);
    controller.AddBlog("Blog3", "http://blog3.com");

    context.ChangeTracker.Clear();

    var blog = context.Blogs.Single(b => b.Name == "Blog3");
    Assert.Equal("http://blog3.com", blog.Url);

}

Yukarıdaki test koduyla ilgili bazı notlar:

  • Aşağıdaki değişikliklerin veritabanına işlenmediğinden ve diğer testlere müdahale etmediğinden emin olmak için bir işlem başlatırız. İşlem hiçbir zaman işlenmediğinden, bağlam örneği atıldığında testin sonunda örtük olarak geri alınır.
  • İstediğimiz güncelleştirmeleri yaptıktan sonra, blogu aşağıdaki veritabanından yüklediğimizden emin olmak için bağlam örneğinin değişiklik izleyicisini ile ChangeTracker.Cleartemizleriz. Bunun yerine iki bağlam örneği kullanabiliriz, ancak aynı işlemin her iki örnek tarafından da kullanıldığından emin olmamız gerekir.
  • Testlerin zaten bir işlemde olan ve güncelleştirmeler için hazır olan bir bağlam örneğini alması için fikstürün CreateContextiçinde işlemi başlatmak isteyebilirsiniz. Bu, işlemin yanlışlıkla unutulduğu durumları önlemeye yardımcı olabilir ve hata ayıklaması zor olabilecek test girişimine yol açabilir. Ayrıca, farklı test sınıflarında da salt okunur ve yazma testlerini ayırmak isteyebilirsiniz.

İşlemleri açıkça yöneten testler

Ek bir zorluk sunan son bir test kategorisi vardır: verileri değiştiren ve ayrıca işlemleri açıkça yöneten testler. Veritabanları genellikle iç içe işlemleri desteklemediğinden, gerçek ürün kodu tarafından kullanılması gerektiğinden, işlemleri yukarıdaki gibi yalıtım için kullanmak mümkün değildir. Bu testler daha nadir olma eğiliminde olsa da, bunları özel bir şekilde işlemek gerekir: Her test sonrasında veritabanınızı özgün durumuna göre temizlemeniz ve bu testlerin birbiriyle karışmaması için paralelleştirme devre dışı bırakılmalıdır.

Örnek olarak aşağıdaki denetleyici yöntemini inceleyelim:

[HttpPost]
public ActionResult UpdateBlogUrl(string name, string url)
{
    // Note: it isn't usually necessary to start a transaction for updating. This is done here for illustration purposes only.
    using var transaction = _context.Database.BeginTransaction(IsolationLevel.Serializable);

    var blog = _context.Blogs.FirstOrDefault(b => b.Name == name);
    if (blog is null)
    {
        return NotFound();
    }

    blog.Url = url;
    _context.SaveChanges();

    transaction.Commit();
    return Ok();
}

Bir nedenden dolayı yönteminin seri hale getirilebilir bir işlemin kullanılmasını gerektirdiğini varsayalım (bu genellikle böyle bir durum değildir). Sonuç olarak, test yalıtımını garanti etmek için bir işlem kullanamayız. Test veritabanında gerçekten değişiklikler gerçekleştireceğinden, yukarıda gösterilen diğer testlere müdahale etmediğimizden emin olmak için kendi ayrı veritabanıyla başka bir fikstür tanımlayacağız:

public class TransactionalTestDatabaseFixture
{
    private const string ConnectionString = @"Server=(localdb)\mssqllocaldb;Database=EFTransactionalTestSample;Trusted_Connection=True;ConnectRetryCount=0";

    public BloggingContext CreateContext()
        => new BloggingContext(
            new DbContextOptionsBuilder<BloggingContext>()
                .UseSqlServer(ConnectionString)
                .Options);

    public TransactionalTestDatabaseFixture()
    {
        using var context = CreateContext();
        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        Cleanup();
    }

    public void Cleanup()
    {
        using var context = CreateContext();

        context.Blogs.RemoveRange(context.Blogs);

        context.AddRange(
            new Blog { Name = "Blog1", Url = "http://blog1.com" },
            new Blog { Name = "Blog2", Url = "http://blog2.com" });
        context.SaveChanges();
    }
}

Bu fikstür yukarıda kullanılana benzer, ancak özellikle bir Cleanup yöntem içerir; veritabanının başlangıç durumuna sıfırlandığından emin olmak için bunu her test sonrasında çağıracağız.

Bu fikstür yalnızca tek bir test sınıfı tarafından kullanılacaksa, yukarıdaki gibi bir sınıf fikstür olarak başvurabiliriz - xUnit aynı sınıf içindeki testleri paralelleştirmez (xUnit belgelerinde test koleksiyonları ve paralelleştirme hakkında daha fazla bilgi edinin). Ancak, bu fikstürün birden çok sınıf arasında paylaşılmasını istiyorsak, herhangi bir girişimi önlemek için bu sınıfların paralel çalışmadığından emin olmamız gerekir. Bunu yapmak için, bunu sınıf fikstür olarak değil xUnit toplama fikstür olarak kullanacağız.

İlk olarak fikstürümüze başvuran ve bunu gerektiren tüm işlemsel test sınıfları tarafından kullanılacak bir test koleksiyonu tanımlayacağız:

[CollectionDefinition("TransactionalTests")]
public class TransactionalTestsCollection : ICollectionFixture<TransactionalTestDatabaseFixture>
{
}

Şimdi test sınıfımızda test koleksiyonuna başvuruyoruz ve daha önce olduğu gibi oluşturucudaki fikstürleri kabul ediyoruz:

[Collection("TransactionalTests")]
public class TransactionalBloggingControllerTest : IDisposable
{
    public TransactionalBloggingControllerTest(TransactionalTestDatabaseFixture fixture)
        => Fixture = fixture;

    public TransactionalTestDatabaseFixture Fixture { get; }

Son olarak, her test sonrasında fikstür Cleanup yönteminin çağrılması için düzenleyerek test sınıfımızı atılabilir hale getiririz:

public void Dispose()
    => Fixture.Cleanup();

xUnit koleksiyon fikstürünün yalnızca bir kez örneğini oluşturduğundan, yukarıda yaptığımız gibi veritabanı oluşturma ve tohumlama etrafında kilitlemeyi kullanmamıza gerek olmadığını unutmayın.

Yukarıdakiler için örnek kodun tamamı burada görüntülenebilir.

Bahşiş

Veritabanını değiştiren testlere sahip birden çok test sınıfınız varsa, her biri kendi veritabanına başvuran farklı fikstürlere sahip olarak bunları paralel olarak çalıştırmaya devam edebilirsiniz. Birçok test veritabanı oluşturmak ve kullanmak sorunlu değildir ve yararlı olduğunda yapılmalıdır.

Verimli veritabanı oluşturma

Yukarıdaki örneklerde, güncel bir test veritabanımız olduğundan emin olmak için testleri çalıştırmadan önce ve EnsureCreated() kullandıkEnsureDeleted(). Bu işlemler bazı veritabanlarında biraz yavaş olabilir ve bu da kod değişikliklerini yineleyip testleri tekrar tekrar çalıştırdığınızda sorun oluşturabilir. Böyle bir durumda, fikstürünüzün oluşturucusunda geçici olarak yorum EnsureDeleted yapmak isteyebilirsiniz: Bu, test çalıştırmalarında aynı veritabanını yeniden kullanacaktır.

Bu yaklaşımın dezavantajı, EF Core modelinizi değiştirirseniz veritabanı şemanızın güncel olmayacağı ve testlerin başarısız olabileceğidir. Sonuç olarak, bunu yalnızca geliştirme döngüsü sırasında geçici olarak yapmanızı öneririz.

Verimli veritabanı temizleme

Yukarıda, değişiklikler veritabanına gerçekten işlendiğinde girişimi önlemek için her test arasındaki veritabanını temizlememiz gerektiğini gördük. Yukarıdaki işlem testi örneğinde, bunu EF Core API'lerini kullanarak tablonun içeriğini silerek yaptık:

using var context = CreateContext();

context.Blogs.RemoveRange(context.Blogs);

context.AddRange(
    new Blog { Name = "Blog1", Url = "http://blog1.com" },
    new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();

Bu genellikle bir tabloyu temizlemenin en verimli yolu değildir. Test hızı önemliyse, tabloyu silmek için ham SQL kullanmak isteyebilirsiniz:

DELETE FROM [Blogs];

Veritabanını verimli bir şekilde temizleyen yeniden sarma paketini kullanmayı da düşünebilirsiniz. Ayrıca, temizlenecek tabloları belirtmenizi gerektirmez ve bu nedenle modelinize tablolar eklendikçe temizleme kodunuzun güncelleştirilmesi gerekmez.

Özet

  • Gerçek bir veritabanında test yaparken, aşağıdaki test kategorilerini ayırt etmeye değer:
    • Salt okunur testler nispeten basittir ve yalıtım konusunda endişelenmenize gerek kalmadan her zaman aynı veritabanında paralel olarak yürütülebilir.
    • Yazma testleri daha sorunludur, ancak düzgün bir şekilde yalıtıldığından emin olmak için işlemler kullanılabilir.
    • İşlem testleri en sorunlu olanlardır ve veritabanını özgün durumuna geri döndürmek ve paralelleştirmeyi devre dışı bırakmak için mantık gerekir.
  • Bu test kategorilerini ayrı sınıflara ayırmak, testler arasındaki karışıklığı ve yanlışlıkla müdahaleyi önleyemeyebilir.
  • Dağıtılmış test verilerinize biraz önceden göz atıp testlerinizi, bu tohum verilerinin değişmesi durumunda çok sık bozulmayacak şekilde yazmaya çalışın.
  • Veritabanını değiştiren testleri paralelleştirmek ve büyük olasılıkla farklı tohum veri yapılandırmalarına izin vermek için birden çok veritabanı kullanın.
  • Test hızı önemliyse, test veritabanınızı oluşturmak ve çalıştırmalar arasında verilerini temizlemek için daha verimli tekniklere bakmak isteyebilirsiniz.
  • Her zaman test paralelleştirmeyi ve yalıtımı göz önünde bulundurun.