Bagikan melalui


Pengujian terhadap sistem database produksi Anda

Di halaman ini, kita membahas teknik untuk menulis pengujian otomatis yang melibatkan sistem database tempat aplikasi berjalan dalam produksi. Ada pendekatan pengujian alternatif, di mana sistem database produksi ditukar dengan pengujian ganda; lihat halaman gambaran umum pengujian untuk informasi selengkapnya. Perhatikan bahwa pengujian terhadap database yang berbeda dari apa yang digunakan dalam produksi (misalnya Sqlite) tidak tercakup di sini, karena database yang berbeda digunakan sebagai pengujian ganda; pendekatan ini tercakup dalam Pengujian tanpa sistem database produksi Anda.

Rintangan utama dengan pengujian yang melibatkan database nyata adalah memastikan isolasi pengujian yang tepat, sehingga pengujian yang berjalan secara paralel (atau bahkan dalam serial) tidak saling mengganggu. Kode sampel lengkap untuk di bawah ini dapat dilihat di sini.

Tip

Halaman ini menunjukkan teknik xUnit , tetapi konsep serupa ada dalam kerangka kerja pengujian lainnya, termasuk NUnit.

Menyiapkan sistem database Anda

Sebagian besar sistem database saat ini dapat dengan mudah diinstal, baik di lingkungan CI maupun pada mesin pengembang. Meskipun cukup mudah untuk menginstal database melalui mekanisme penginstalan reguler, gambar Docker siap digunakan tersedia untuk sebagian besar database utama dan dapat membuat penginstalan sangat mudah di CI. Untuk lingkungan pengembang, GitHub Workspaces, Dev Container dapat menyiapkan semua layanan dan dependensi yang diperlukan - termasuk database. Meskipun ini membutuhkan investasi awal dalam penyiapan, setelah itu selesai Anda memiliki lingkungan pengujian yang berfungsi dan dapat berkonsentrasi pada hal-hal yang lebih penting.

Dalam kasus tertentu, database memiliki edisi atau versi khusus yang dapat membantu pengujian. Saat menggunakan SQL Server, LocalDB dapat digunakan untuk menjalankan pengujian secara lokal dengan hampir tidak ada pengaturan sama sekali, memutar instans database sesuai permintaan dan mungkin menghemat sumber daya pada komputer pengembang yang kurang kuat. Namun, LocalDB bukan tanpa masalahnya:

Kami umumnya merekomendasikan untuk menginstal edisi Pengembang SQL Server daripada LocalDB, karena menyediakan set fitur SQL Server lengkap dan umumnya sangat mudah dilakukan.

Saat menggunakan database cloud, biasanya sesuai untuk menguji terhadap versi lokal database, baik untuk meningkatkan kecepatan maupun untuk mengurangi biaya. Misalnya, saat menggunakan SQL Azure dalam produksi, Anda dapat menguji terhadap SQL Server yang diinstal secara lokal - keduanya sangat mirip (meskipun masih bijaksana untuk menjalankan pengujian terhadap SQL Azure itu sendiri sebelum masuk ke produksi). Saat menggunakan Azure Cosmos DB, emulator Azure Cosmos DB adalah alat yang berguna untuk mengembangkan secara lokal dan untuk menjalankan pengujian.

Membuat, menyemai, dan mengelola database pengujian

Setelah database diinstal, Anda siap untuk mulai menggunakannya dalam pengujian Anda. Dalam kebanyakan kasus sederhana, rangkaian pengujian Anda memiliki database tunggal yang dibagikan antara beberapa pengujian di beberapa kelas pengujian, jadi kami memerlukan beberapa logika untuk memastikan database dibuat dan diunggulkan tepat sekali selama masa pakai eksekusi pengujian.

Saat menggunakan Xunit, ini dapat dilakukan melalui perlengkapan kelas, yang mewakili database dan dibagikan di beberapa eksekusi pengujian:

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

Ketika perlengkapan di atas dibuat, ia menggunakan EnsureDeleted() untuk menghilangkan database (jika ada dari eksekusi sebelumnya), lalu EnsureCreated() membuatnya dengan konfigurasi model terbaru Anda (lihat dokumen untuk API ini). Setelah database dibuat, fikstur memilahnya dengan beberapa data yang dapat digunakan pengujian kami. Ada baiknya menghabiskan beberapa waktu untuk memikirkan data benih Anda, karena mengubahnya nanti untuk pengujian baru dapat menyebabkan pengujian yang ada gagal.

Untuk menggunakan perlengkapan dalam kelas pengujian, cukup terapkan IClassFixture melalui jenis perlengkapan Anda, dan xUnit akan menyuntikkannya ke konstruktor Anda:

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

    public TestDatabaseFixture Fixture { get; }

Kelas pengujian Anda sekarang memiliki Fixture properti yang dapat digunakan oleh pengujian untuk membuat instans konteks yang berfungsi penuh:

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

Akhirnya, Anda mungkin telah melihat beberapa penguncian dalam logika pembuatan perlengkapan di atas. Jika perlengkapan hanya digunakan dalam satu kelas pengujian, itu dijamin akan diinstansiasi tepat sekali oleh xUnit; tetapi umumnya menggunakan perlengkapan database yang sama di beberapa kelas pengujian. xUnit memang menyediakan perlengkapan pengumpulan, tetapi mekanisme itu mencegah kelas pengujian Anda berjalan secara paralel, yang penting untuk performa pengujian. Untuk mengelola ini dengan aman dengan perlengkapan kelas xUnit, kami mengambil kunci sederhana sekeliling pembuatan dan penyemaian database, dan menggunakan bendera statis untuk memastikan kita tidak perlu melakukannya dua kali.

Pengujian yang memodifikasi data

Contoh di atas menunjukkan pengujian baca-saja, yang merupakan kasus mudah dari sudut simpul isolasi pengujian: karena tidak ada yang dimodifikasi, gangguan pengujian tidak dimungkinkan. Sebaliknya, pengujian yang memodifikasi data lebih bermasalah, karena dapat mengganggu satu sama lain. Salah satu teknik umum untuk mengisolasi tes penulisan adalah membungkus pengujian dalam transaksi, dan agar transaksi tersebut digulung balik pada akhir pengujian. Karena tidak ada yang benar-benar diterapkan pada database, pengujian lain tidak melihat modifikasi dan gangguan apa pun dihindari.

Berikut adalah metode pengontrol yang menambahkan Blog ke database kami:

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

    return Ok();
}

Kita dapat menguji metode ini dengan yang berikut:

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

}

Beberapa catatan pada kode pengujian di atas:

  • Kami memulai transaksi untuk memastikan perubahan di bawah ini tidak diterapkan pada database, dan tidak mengganggu pengujian lain. Karena transaksi tidak pernah dilakukan, transaksi secara implisit digulung balik pada akhir pengujian ketika instans konteks dibuang.
  • Setelah membuat pembaruan yang kami inginkan, kami menghapus pelacak perubahan instans konteks dengan ChangeTracker.Clear, untuk memastikan kami benar-benar memuat blog dari database di bawah ini. Kita dapat menggunakan dua instans konteks sebagai gantinya, tetapi kita kemudian harus memastikan transaksi yang sama digunakan oleh kedua instans.
  • Anda bahkan mungkin ingin memulai transaksi di perlengkapan CreateContext, sehingga pengujian menerima instans konteks yang sudah dalam transaksi, dan siap untuk pembaruan. Ini dapat membantu mencegah kasus di mana transaksi secara tidak sengaja terlupakan, yang menyebabkan gangguan pengujian yang dapat sulit di-debug. Anda mungkin juga ingin memisahkan tes baca-saja dan tulis di kelas pengujian yang berbeda juga.

Pengujian yang secara eksplisit mengelola transaksi

Ada satu kategori akhir pengujian yang menyajikan kesulitan tambahan: pengujian yang memodifikasi data dan juga mengelola transaksi secara eksplisit. Karena database biasanya tidak mendukung transaksi berlapis, tidak dimungkinkan untuk menggunakan transaksi untuk isolasi seperti di atas, karena perlu digunakan oleh kode produk aktual. Meskipun pengujian ini cenderung lebih langka, perlu untuk menanganinya dengan cara khusus: Anda harus membersihkan database Anda ke keadaan aslinya setelah setiap pengujian, dan paralelisasi harus dinonaktifkan sehingga pengujian ini tidak mengganggu satu sama lain.

Mari kita periksa metode pengontrol berikut sebagai contoh:

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

Mari kita asumsikan bahwa untuk beberapa alasan, metode ini memerlukan transaksi yang dapat diserialisasikan untuk digunakan (ini biasanya tidak terjadi). Akibatnya, kami tidak dapat menggunakan transaksi untuk menjamin isolasi pengujian. Karena pengujian akan benar-benar menerapkan perubahan pada database, kami akan menentukan perlengkapan lain dengan database terpisahnya sendiri, untuk memastikan kami tidak mengganggu pengujian lain yang sudah ditunjukkan di atas:

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

Perlengkapan ini mirip dengan yang digunakan di atas, tetapi terutama berisi Cleanup metode; kita akan memanggil ini setelah setiap pengujian untuk memastikan bahwa database diatur ulang ke status awalnya.

Jika perlengkapan ini hanya akan digunakan oleh satu kelas pengujian, kita dapat mereferensikannya sebagai perlengkapan kelas seperti di atas - xUnit tidak menyejajarkan pengujian dalam kelas yang sama (baca selengkapnya tentang koleksi pengujian dan paralelisasi dalam dokumen xUnit). Namun, jika kita ingin berbagi perlengkapan ini antara beberapa kelas, kita harus memastikan kelas-kelas ini tidak berjalan secara paralel, untuk menghindari gangguan. Untuk melakukan itu, kita akan menggunakan ini sebagai perlengkapan koleksi xUnit daripada sebagai perlengkapan kelas.

Pertama, kami mendefinisikan koleksi pengujian, yang mereferensikan perlengkapan kami dan akan digunakan oleh semua kelas pengujian transaksional yang memerlukannya:

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

Kami sekarang mereferensikan koleksi pengujian di kelas pengujian kami, dan menerima perlengkapan di konstruktor seperti sebelumnya:

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

    public TransactionalTestDatabaseFixture Fixture { get; }

Akhirnya, kami membuat kelas pengujian kami sekali pakai, mengatur metode perlengkapan Cleanup yang akan dipanggil setelah setiap pengujian:

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

Perhatikan bahwa karena xUnit hanya pernah membuat instans perlengkapan koleksi sekali, tidak perlu bagi kita untuk menggunakan penguncian sekeliling pembuatan database dan penyemaian seperti yang kita lakukan di atas.

Kode sampel lengkap untuk hal di atas dapat dilihat di sini.

Tip

Jika Anda memiliki beberapa kelas pengujian dengan pengujian yang memodifikasi database, Anda masih dapat menjalankannya secara paralel dengan memiliki perlengkapan yang berbeda, masing-masing mereferensikan databasenya sendiri. Membuat dan menggunakan banyak database pengujian tidak bermasalah dan harus dilakukan setiap kali membantu.

Pembuatan database yang efisien

Dalam sampel di atas, kami menggunakan EnsureDeleted() dan EnsureCreated() sebelum menjalankan pengujian, untuk memastikan kami memiliki database pengujian terbaru. Operasi ini bisa sedikit lambat dalam database tertentu, yang dapat menjadi masalah saat Anda melakukan iterasi atas perubahan kode dan menjalankan kembali pengujian berulang-ulang. Jika demikian, Anda mungkin ingin mengomentari EnsureDeleted sementara di konstruktor perlengkapan Anda: ini akan menggunakan kembali database yang sama di seluruh eksekusi pengujian.

Kerugian dari pendekatan ini adalah bahwa jika Anda mengubah model EF Core Anda, skema database Anda tidak akan diperbarui, dan pengujian mungkin gagal. Akibatnya, sebaiknya lakukan ini sementara selama siklus pengembangan.

Pembersihan database yang efisien

Kami melihat di atas bahwa ketika perubahan benar-benar diterapkan pada database, kita harus membersihkan database antara setiap pengujian untuk menghindari gangguan. Dalam sampel pengujian transaksional di atas, kami melakukan ini dengan menggunakan API EF Core untuk menghapus konten tabel:

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

Ini biasanya bukan cara yang paling efisien untuk membersihkan tabel. Jika kecepatan pengujian menjadi perhatian, Anda mungkin ingin menggunakan SQL mentah untuk menghapus tabel sebagai gantinya:

DELETE FROM [Blogs];

Anda mungkin juga ingin mempertimbangkan untuk menggunakan paket respawn , yang secara efisien menghapus database. Selain itu, Anda tidak perlu menentukan tabel yang akan dihapus, sehingga kode pembersihan Anda tidak perlu diperbarui karena tabel ditambahkan ke model Anda.

Ringkasan

  • Saat menguji terhadap database nyata, ada baiknya membedakan antara kategori pengujian berikut:
    • Pengujian baca-saja relatif sederhana, dan selalu dapat dijalankan secara paralel terhadap database yang sama tanpa harus khawatir tentang isolasi.
    • Tes tulis lebih bermasalah, tetapi transaksi dapat digunakan untuk memastikannya terisolasi dengan benar.
    • Pengujian transaksional adalah logika yang paling bermasalah, mengharuskan untuk mengatur ulang database kembali ke keadaan semula, serta menonaktifkan paralelisasi.
  • Memisahkan kategori pengujian ini menjadi kelas terpisah dapat menghindari kebingungan dan gangguan yang tidak disengaja antara pengujian.
  • Berikan beberapa pemikiran di muka untuk data pengujian benih Anda, dan coba tulis pengujian Anda dengan cara yang tidak akan terlalu sering rusak jika data benih tersebut berubah.
  • Gunakan beberapa database untuk menyejajarkan pengujian yang memodifikasi database, dan mungkin juga untuk memungkinkan konfigurasi data benih yang berbeda.
  • Jika kecepatan pengujian menjadi perhatian, Anda mungkin ingin melihat teknik yang lebih efisien untuk membuat database pengujian Anda, dan untuk membersihkan datanya di antara eksekusi.
  • Selalu ingat paralelisasi dan isolasi pengujian.