.NET 5 - 启动 MS SQL docker 容器进行集成测试

CT1*_*.IT 2 sql-server integration-testing docker .net-5

我有一个 .NET 5 解决方案,其中包含一个服务项目,其中包含一系列负责从数据库保存/加载数据的存储库(主要通过 EFCore)。

我正在为这个项目编写一些集成测试,因此想针对真实的 SQL Server 数据库测试操作。

使用 docker 是否可以让运行测试的过程启动和拆卸 SQL Server docker 容器,如果可以的话,我们该如何进行呢?

目前,我只是手动运行 docker-compose up 来启动容器,然后运行 ​​dotnet test ,它有一个指向 docker 实例的连接字符串,并生成准备用于测试的虚拟数据。

我已经考虑过为测试项目创建一个 Dockerfile,但是根据我的理解,dockerfile 用于生成项目的映像,而且我实际上不想生成 docker 映像,我只想针对 docker 容器运行测试纯粹是为了测试范围而存在。

CT1*_*.IT 5

我已经成功地使用一个名为 dotnet testcontainers 的优秀 nuget 包来实现我所需要的内容(https://github.com/HofmeisterAn/dotnet-testcontainers

这允许我从 C# 代码中启动一个标准的 sql server docker 映像,导入我的 sql 脚本,然后将其全部拆除。

我使用 xUnit 进行测试,因此我使用 CollectionDefinition 原则来启动容器一次,然后将其丢弃。

数据库夹具.cs

public class DatabaseFixture : IDisposable
{
    private const int _hostPort = 1434;
    private const int _containerPort = 1433;
    private const string _databasePassword = "localdevpassword#123";
    private const string _databaseUser = "sa";

    private string _myDbContext_ConnectionString =>
        $"Server=tcp:localhost,{_hostPort};Database=MyDbContext;user id={_databaseUser};password={_databasePassword};";
    
    public TestcontainersContainer Container { get; private set; }
    public MyDbContext MyDb { get; private set; }
    
    public DatabaseFixture()
    {
        InitContainerTest();
        
        var myDbContextContextOptions = new DbContextOptionsBuilder<MyDbContext>()
            .UseSqlServer(_myDbContext_ConnectionString)
            .Options;
            
        MyDb = new MyDbContext(myDbContextContextOptions );

        // Seed the database if required
        
    }

    public void Dispose()
    {
        Container.StopAsync();
        Container.CleanUpAsync();
    }
    
    private void InitContainerTest()
    {
        
        var outputConsumer = Consume.RedirectStdoutAndStderrToStream(new MemoryStream(), new MemoryStream());
        var waitStrategy = Wait.ForUnixContainer().UntilMessageIsLogged(outputConsumer.Stdout, "INIT_COMPLETE");

        var containerBuilder = new TestcontainersBuilder<TestcontainersContainer>()
            .WithImage("mcr.microsoft.com/mssql/server:2019-latest")
            .WithPortBinding(_hostPort, _containerPort)
            .WithEnvironment("ACCEPT_EULA", "true")
            .WithEnvironment("SA_PASSWORD", _databasePassword)
            .WithBindMount(Path.GetFullPath("sql"), "/scripts/")
            .WithCommand("/bin/bash", "-c", "/scripts/init.sh")
            .WithOutputConsumer(outputConsumer)
            .WithWaitStrategy(waitStrategy);

        Container = containerBuilder.Build();
        
        var task = Container.StartAsync();
        task.WaitAndUnwrapException();
    }
}
Run Code Online (Sandbox Code Playgroud)

数据库集合.cs

[CollectionDefinition("Database collection")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
    // This class has no code, and is never created. Its purpose is simply
    // to be the place to apply [CollectionDefinition] and all the
    // ICollectionFixture<> interfaces.
}
Run Code Online (Sandbox Code Playgroud)

用户服务测试.cs

[Collection("Database collection")]
public class UserServiceTests
{
    private readonly UserService _sut;
    private readonly DatabaseFixture _databaseFixture;

    public UserServiceTests(DatabaseFixture databaseFixture)
    {
        _databaseFixture = databaseFixture;
        _sut = new UserService(_databaseFixture.MyDb);
    }

    [Fact]
    public async Task GetUserCompany_ShouldReturnCompanyId_WhenUserIdFound()
    {
        // Arrange
        var faker = new Faker<Thing>();
        faker.RuleFor(e => e.UserId, f => Guid.NewGuid());
        faker.RuleFor(e => e.CompanyId, f => Guid.NewGuid());
        var thing = faker.Generate();
        _databaseFixture.MyDb.Things.Add(thing);
        await _databaseFixture.MyDb.SaveChangesAsync();
        
        // Act
        var actualOutput = await _sut.GetCompanyIdFromUserId(thing.UserId);
        
        // Assert
        actualOutput.Should().Be(thing.CompanyId);
    }       
}
Run Code Online (Sandbox Code Playgroud)

需要做一些工作的事情

我需要在 Container.StartAsync() 之后手动添加延迟以允许数据库脚本运行。

我现在等待容器标准输出输出“INIT_COMPLETE”,这是复制的 init.sh 文件的最后一行。我现在还使用一点小技巧从 DatabaseFixture 中运行 StartAsync() 方法,以允许从非异步方法中运行异步代码。

var task = Container.StartAsync();
task.WaitAndUnwrapException();
Run Code Online (Sandbox Code Playgroud)