Pengujian tanpa sistem database produksi Anda

Di halaman ini, kami membahas teknik untuk menulis pengujian otomatis yang tidak melibatkan sistem database tempat aplikasi berjalan dalam produksi, dengan bertukar database Anda dengan pengujian ganda. Ada berbagai jenis pengujian ganda dan pendekatan untuk melakukan ini, dan disarankan untuk membaca secara menyeluruh Memilih strategi pengujian untuk sepenuhnya memahami berbagai opsi. Akhirnya, dimungkinkan juga untuk menguji terhadap sistem database produksi Anda; ini tercakup dalam Pengujian terhadap sistem database produksi Anda.

Tip

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

Pola repositori

Jika Anda telah memutuskan untuk menulis pengujian tanpa melibatkan sistem database produksi Anda, maka teknik yang direkomendasikan untuk melakukannya adalah pola repositori; untuk latar belakang selengkapnya tentang ini, lihat bagian ini. Langkah pertama untuk menerapkan pola repositori adalah mengekstrak kueri EF Core LINQ Anda ke lapisan terpisah, yang nantinya akan kita dukung atau tiruan. Berikut adalah contoh antarmuka repositori untuk sistem blog kami:

public interface IBloggingRepository
{
    Blog GetBlogByName(string name);

    IEnumerable<Blog> GetAllBlogs();

    void AddBlog(Blog blog);

    void SaveChanges();
}

... dan berikut adalah implementasi sampel parsial untuk penggunaan produksi:

public class BloggingRepository : IBloggingRepository
{
    private readonly BloggingContext _context;

    public BloggingRepository(BloggingContext context)
        => _context = context;

    public Blog GetBlogByName(string name)
        => _context.Blogs.FirstOrDefault(b => b.Name == name);

    // Other code...
}

Tidak banyak yang bisa dilakukan: repositori hanya membungkus konteks EF Core, dan mengekspos metode yang menjalankan kueri database dan pembaruan di atasnya. Poin kunci yang perlu diperhatikan adalah bahwa metode kami GetAllBlogs mengembalikan IEnumerable<Blog>, dan bukan IQueryable<Blog>. Mengembalikan yang terakhir berarti bahwa operator kueri masih dapat disusupi atas hasilnya, mengharuskan EF Core masih terlibat dalam menerjemahkan kueri; ini akan mengalahkan tujuan memiliki repositori di tempat pertama. IEnumerable<Blog> memungkinkan kita untuk dengan mudah membasuh atau mengejek apa yang dikembalikan repositori.

Untuk aplikasi ASP.NET Core, kita perlu mendaftarkan repositori sebagai layanan dalam injeksi dependensi dengan menambahkan yang berikut ke aplikasi ConfigureServices:

services.AddScoped<IBloggingRepository, BloggingRepository>();

Akhirnya, pengontrol kami disuntikkan dengan layanan repositori alih-alih konteks EF Core, dan menjalankan metode di atasnya:

private readonly IBloggingRepository _repository;

public BloggingControllerWithRepository(IBloggingRepository repository)
    => _repository = repository;

[HttpGet]
public Blog GetBlog(string name)
    => _repository.GetBlogByName(name);

Pada titik ini, aplikasi Anda dirancang sesuai dengan pola repositori: satu-satunya titik kontak dengan lapisan akses data - EF Core - sekarang melalui lapisan repositori, yang bertindak sebagai mediator antara kode aplikasi dan kueri database aktual. Tes sekarang dapat ditulis hanya dengan menyatu keluar repositori, atau dengan menipunya dengan pustaka tiruan favorit Anda. Berikut adalah contoh tes berbasis tiruan menggunakan pustaka Moq populer:

[Fact]
public void GetBlog()
{
    // Arrange
    var repositoryMock = new Mock<IBloggingRepository>();
    repositoryMock
        .Setup(r => r.GetBlogByName("Blog2"))
        .Returns(new Blog { Name = "Blog2", Url = "http://blog2.com" });

    var controller = new BloggingControllerWithRepository(repositoryMock.Object);

    // Act
    var blog = controller.GetBlog("Blog2");

    // Assert
    repositoryMock.Verify(r => r.GetBlogByName("Blog2"));
    Assert.Equal("http://blog2.com", blog.Url);
}

Kode sampel lengkap dapat dilihat di sini.

SQLite dalam memori

SQLite dapat dengan mudah dikonfigurasi sebagai penyedia EF Core untuk rangkaian pengujian Anda alih-alih sistem database produksi Anda (misalnya SQL Server); lihat dokumen penyedia SQLite untuk detailnya. Namun, biasanya sebaiknya gunakan fitur database dalam memori SQLite saat pengujian, karena menyediakan isolasi yang mudah antara pengujian, dan tidak memerlukan berurusan dengan file SQLite yang sebenarnya.

Untuk menggunakan SQLite dalam memori, penting untuk dipahami bahwa database baru dibuat setiap kali koneksi tingkat rendah dibuka, dan koneksi tersebut dihapus saat koneksi tersebut ditutup. Dalam penggunaan normal, EF Core DbContext membuka dan menutup koneksi database sesuai kebutuhan - setiap kali kueri dijalankan - untuk menghindari menjaga koneksi untuk waktu yang lama yang tidak perlu. Namun, dengan SQLite dalam memori ini akan menyebabkan pengaturan ulang database setiap kali; jadi sebagai solusinya, kami membuka koneksi sebelum meneruskannya ke EF Core, dan mengatur agar ditutup hanya ketika pengujian selesai:

    public SqliteInMemoryBloggingControllerTest()
    {
        // Create and open a connection. This creates the SQLite in-memory database, which will persist until the connection is closed
        // at the end of the test (see Dispose below).
        _connection = new SqliteConnection("Filename=:memory:");
        _connection.Open();

        // These options will be used by the context instances in this test suite, including the connection opened above.
        _contextOptions = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlite(_connection)
            .Options;

        // Create the schema and seed some data
        using var context = new BloggingContext(_contextOptions);

        if (context.Database.EnsureCreated())
        {
            using var viewCommand = context.Database.GetDbConnection().CreateCommand();
            viewCommand.CommandText = @"
CREATE VIEW AllResources AS
SELECT Url
FROM Blogs;";
            viewCommand.ExecuteNonQuery();
        }

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

    BloggingContext CreateContext() => new BloggingContext(_contextOptions);

    public void Dispose() => _connection.Dispose();

Pengujian sekarang dapat memanggil CreateContext, yang mengembalikan konteks menggunakan koneksi yang kami siapkan di konstruktor, memastikan kami memiliki database bersih dengan data seeded.

Kode sampel lengkap untuk pengujian dalam memori SQLite dapat dilihat di sini.

Penyedia dalam memori

Seperti yang dibahas di halaman gambaran umum pengujian, menggunakan penyedia dalam memori untuk pengujian sangat tidak disarankan; pertimbangkan untuk menggunakan SQLite sebagai gantinya, atau menerapkan pola repositori. Jika Anda telah memutuskan untuk menggunakan dalam memori, berikut adalah konstruktor kelas pengujian khas yang menyiapkan dan menyemai database dalam memori baru sebelum setiap pengujian:

public InMemoryBloggingControllerTest()
{
    _contextOptions = new DbContextOptionsBuilder<BloggingContext>()
        .UseInMemoryDatabase("BloggingControllerTest")
        .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning))
        .Options;

    using var context = new BloggingContext(_contextOptions);

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

Kode sampel lengkap untuk pengujian dalam memori dapat dilihat di sini.

Penamaan database dalam memori

Database dalam memori diidentifikasi dengan nama string sederhana, dan dimungkinkan untuk terhubung ke database yang sama beberapa kali dengan memberikan nama yang sama (inilah sebabnya sampel di atas harus memanggil EnsureDeleted sebelum setiap pengujian). Namun, perhatikan bahwa database dalam memori berakar di penyedia layanan internal konteks; sementara dalam kebanyakan kasus konteks berbagi penyedia layanan yang sama, mengonfigurasi konteks dengan opsi yang berbeda dapat memicu penggunaan penyedia layanan internal baru. Ketika itu terjadi, secara eksplisit meneruskan instans InMemoryDatabaseRoot yang sama untuk UseInMemoryDatabase semua konteks yang harus berbagi database dalam memori (ini biasanya dilakukan dengan memiliki bidang statis InMemoryDatabaseRoot ).

Transaksi

Perhatikan bahwa secara default, jika transaksi dimulai, penyedia dalam memori akan memberikan pengecualian karena transaksi tidak didukung. Anda mungkin ingin transaksi diabaikan secara diam-diam, dengan mengonfigurasi EF Core untuk diabaikan InMemoryEventId.TransactionIgnoredWarning seperti pada sampel di atas. Namun, jika kode Anda benar-benar bergantung pada semantik transaksional - misalnya tergantung pada pemutaran kembali yang benar-benar mengembalikan perubahan - pengujian Anda tidak akan berfungsi.

Tampilan

Penyedia dalam memori memungkinkan definisi tampilan melalui kueri LINQ, menggunakan ToInMemoryQuery:

modelBuilder.Entity<UrlResource>()
    .ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));