Kiểm thử tích hợp cơ sở dữ liệu cho .NET

Viết test cơ sở dữ liệu một lần.
Chạy trên mọi engine.

Thư viện .NET chạy cùng một thân test trên SQL Server, PostgreSQL và MySQL. Mỗi lần chạy có một container cơ sở dữ liệu dùng một lần. Không bao giờ đụng đến cơ sở dữ liệu dùng chung, staging hay production.

  • Docker đang chạy
  • .NET 8 trở lên
  • Giấy phép MIT

vì sao

Bộ test cơ sở dữ liệu nào cũng gặp cùng sáu vấn đề. Phần lớn các nhóm tự giải quyết bằng tay.

  1. 01 · Cơ sở dữ liệu để test lấy từ đâu?

    Mỗi engine một container dùng một lần

    Khởi động ở lần dùng đầu tiên và dùng chung suốt lần chạy. Container được xóa ngay khi lần chạy kết thúc.

  2. 02 · Schema được đưa vào bằng cách nào?

    Schema từ các tệp trên đĩa

    Áp dụng theo thứ tự, với quy tắc tách câu lệnh riêng của từng engine. Schema chỉ áp dụng được một phần sẽ báo lỗi rõ ràng ngay lúc thiết lập.

  3. 03 · Một test xóa một cột và test sau thất bại vì nó

    Shape guard

    Đánh thất bại đúng test đã xóa hoặc đổi kiểu một đối tượng cơ sở dữ liệu dùng chung, nêu rõ đối tượng và thay đổi, thay vì đổ lỗi cho test chạy sau.

  4. 04 · Dữ liệu của test này rò sang test sau

    Dọn dữ liệu giữa các test

    CleanAsync() làm rỗng mọi bảng bất kể khóa ngoại, và đặt lại bộ đếm identity.

  5. 05 · "Bộ test chạy chậm"

    Thời gian chạy từng test

    Đặt DBTEST_TIMING=1 để ghi thời gian từng test vào tệp TSV. Mặc định tắt.

  6. 06 · Cùng các assertion phải viết ba lần

    Một bộ assertion

    Engine nằm sau một abstract class, nên không phải viết cùng các assertion ba lần.

một thân test, ba engine

Viết test một lần. Mỗi engine một lớp nhỏ.

Test được viết một lần trong một abstract class. Mỗi engine có một lớp nhỏ cho biết dùng engine nào và schema nằm ở đâu.

CustomerTests.cs
public abstract class CustomerTests : DbTestBase
{
    [TestMethod]
    public async Task Inserted_Customer_Should_Be_Counted()
    {
        await Harness.CleanAsync();
        await Harness.ExecuteAsync(
            "INSERT INTO customers (email, display_name) VALUES (@email, @name)",
            [new DbParameterValue("@email", "ada@example.test"), new DbParameterValue("@name", "Ada")]);

        var count = await Harness.QueryScalarAsync<int>(
            "SELECT COUNT(*) FROM customers WHERE email = @email",
            [new DbParameterValue("@email", "ada@example.test")]);

        Assert.AreEqual(1, count);
    }
}
PostgresCustomerTests.cs
[TestClass]
public sealed class PostgresCustomerTests : CustomerTests
{
    protected override DbEngine CreateEngine() => new PostgreSqlEngine();
    protected override string SchemaRoot => "Schema/postgres";
}
MsSqlCustomerTests.cs
[TestClass]
public sealed class MsSqlCustomerTests : CustomerTests
{
    protected override DbEngine CreateEngine() => new MsSqlEngine();
    protected override string SchemaRoot => "Schema/mssql";
}
MySqlCustomerTests.cs
[TestClass]
public sealed class MySqlCustomerTests : CustomerTests
{
    protected override DbEngine CreateEngine() => new MySqlEngine();
    protected override string SchemaRoot => "Schema/mysql";
}

Hai override: đó là toàn bộ chi phí cho thêm một engine.

cài đặt

Lớp cơ sở (base class) cho MSTest, cùng một gói engine cho mỗi cơ sở dữ liệu bạn kiểm thử.

bash
dotnet add package DbTest.MSTest        # base class for MSTest (brings DbTest.Core)
dotnet add package DbTest.MsSql         # SQL Server engine
dotnet add package DbTest.PostgreSql    # PostgreSQL engine
dotnet add package DbTest.MySql         # MySQL engine
dotnet add package DbTest.MsSql.Dacpac  # deploy a .dacpac (SQL Server database projects)

Chỉ cần DbTest.Core là có harness, không phụ thuộc framework kiểm thử nào.

TestRun.cs · một lần cho mỗi test project
// Once per test project: tests share a container, so they run one at a time, and the
// containers are removed as soon as the run ends.
[assembly: DoNotParallelize]

[TestClass]
public static class TestRun
{
    [AssemblyCleanup]
    public static Task Cleanup() => DbTestRun.DisposeAllAsync();
}

cách hoạt động

Bốn bước đến một lần chạy xanh.

  1. Cài đặt

    Thêm DbTest.MSTest và một gói engine cho mỗi cơ sở dữ liệu. Docker phải đang chạy.

  2. Trỏ đến schema

    Để các tệp SQL trong một thư mục cho mỗi engine và đặt SchemaRoot. Các tệp được áp dụng theo thứ tự đường dẫn.

  3. Viết test một lần

    Kế thừa DbTestBase trong một abstract class, rồi thêm một [TestClass] cho mỗi engine.

  4. Chạy dotnet test

    Container khởi động ở lần dùng đầu tiên, shape guard kiểm tra mọi test, và container được xóa khi lần chạy kết thúc.

shape guard

Chỉ ra test đã làm hỏng. Không đổ lỗi cho test sau.

Khi một test xóa hoặc đổi kiểu một đối tượng cơ sở dữ liệu dùng chung, shape guard (bộ canh hình dạng schema) đánh thất bại đúng test đó, nêu rõ đối tượng và thay đổi. Các test chạy sau không bị đổ lỗi.

Cách shape guard hoạt động
✗ FAILED
Assert.Fail failed. The test left a shared database object reshaped. Restore it before the
test ends (the shape guard compares against the schema as applied):
[ShapeGuard] CustomerTests.Adds_A_Column left dbo.customers columns changed (id,email) -> (id,email,probe)

Dự án cơ sở dữ liệu SQL Server

DbTest.MsSql.Dacpac

Một dự án cơ sở dữ liệu (SSDT hoặc Microsoft.Build.Sql) có các đối tượng phụ thuộc lẫn nhau giữa các thư mục, nên không có thứ tự tệp nào áp dụng đúng được. DbTest.MsSql.Dacpac triển khai tệp .dacpac đã build bằng DacFx, theo thứ tự phụ thuộc.

Xem dự án mẫu

Đừng mock cơ sở dữ liệu nữa. Hãy kiểm thử nó.

dotnet add package DbTest.MSTest