Aracılığıyla paylaş


Üretim veritabanı sisteminizde test yapma

Bu sayfada, uygulamanın üretimde çalıştığı veritabanı sistemini içeren otomatikleştirilmiş testler yazma tekniklerini ele aacağız. Alternatif test yaklaşımları mevcuttur; burada üretim veritabanı sistemi, test çiftleriyle değiştirilir. Daha fazla bilgi için teste genel bakış sayfasına göz atı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 olmadanTest 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.

Tavsiye

Bu sayfada xUnit teknikleri gösterilir, ancak NUnitgibi diğer test çerçevelerinde benzer kavramlar vardır.

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. Testcontainers gibi kitaplıklar, test sırasında kapsayıcılı veritabanı örneklerini otomatik olarak yöneterek bunu daha da basitleştirebilir. Geliştirici ortamı için GitHub WorkspacesDev Container veritabanı 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:

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, veritabanını temsil eden ve birçok test çalıştırması sırasında paylaşılan sınıf fikstüraracılığıyla bu 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şturulduğunda, veritabanını silmek için EnsureDeleted() kullanır (önceki bir çalıştırmadan mevcut ise) ve ardından en son model yapılandırmanızla yeniden oluşturmak için EnsureCreated() kullanır (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 üzerinde IClassFixture uygulamanız 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 bağlam örneği oluşturmak için testler tarafından kullanılabilecek bir Fixture özelliği vardır:

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

    var blog = (await 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, toplama fikstürleri sağlar, ancak bu mekanizma test sınıflarınızın paralel çalışmasını engeller ve bu da 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 olarak kullanılan tekniklerden biri, testi bir işlem içine almak ve bu işlemin testin sonunda geri döndürülmesidir. Veritabanına gerçekten hiçbir şey işlenmediğinden, diğer testler herhangi bir değişiklik görmez ve müdahaleler önlenir.

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

[HttpPost]
public async Task<ActionResult> AddBlog(string name, string url)
{
    _context.Blogs.Add(new Blog { Name = name, Url = url });
    await _context.SaveChangesAsync();

    return Ok();
}

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

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

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

    context.ChangeTracker.Clear();

    var blog = await context.Blogs.SingleAsync(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 gerçekleştirilmediğinden, bağlam örneği sonlandırı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 ChangeTracker.Clearile temizleriz. 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 işlemde olan ve güncellemelere hazır bir bağlam örneği alabilmesi için, fikstürün CreateContext'ında işlemi başlatmak isteyebilirsiniz. Bu, işlemin yanlışlıkla unutulduğu durumları önlemeye ve hata ayıklaması zor olabilecek testlere müdahale sorunlarına yol açılmasını engellemeye yardımcı olabilir. 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 async Task<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.
    await using var transaction = await _context.Database.BeginTransactionAsync(IsolationLevel.Serializable);

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

    blog.Url = url;
    await _context.SaveChangesAsync();

    await transaction.CommitAsync();
    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 Cleanup bir 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 referans verebiliriz - xUnit aynı sınıf içindeki testleri paralel hale getirmez (test koleksiyonları ve paralelleştirme hakkında daha fazla bilgi için xUnit belgelerine bakın). 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üryerine xUnit toplama fikstür olarak kullanacağız.

İlk olarak, fikstürümüze başvuracak ve gerektiren tüm işlemsel test sınıflarınca kullanılacak numaralı birtest 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, test sınıfımızı geçici hale getirerek fikstürün Cleanup metodunun her test sonrasında çağrılmasını sağlıyoruz.

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

xUnit’in koleksiyon fikstürünü yalnızca bir kez örneklediğini unutmayın, bu yüzden yukarıda yaptığımız gibi veritabanı oluşturma ve başlangıç verileri yüklerken kilit kullanmamıza gerek yoktur.

Yukarıdakilerin tam örnek kodu burada görüntülenebilir.

Tavsiye

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 testleri çalıştırmadan önce EnsureDeleted() ve EnsureCreated() kullanarak up-to-date test veritabanımız olduğundan emin olduk. 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 EnsureDeleted'ı geçici olarak yorum satırı haline getirmek isteyebilirsiniz: Bu, test çalıştırmaları boyunca aynı veritabanını yeniden kullanmanızı sağlar.

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 karışıklığı önlemek için her test arasında 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 şekilde temizleyen respawn 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 sorunlu olabilir, ancak düzgün bir şekilde yalıtılmalarını sağlamak 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.