You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用TestContainers和Xunit时,多项目测试如何复用单个Docker容器

多项目并行测试复用Cassandra容器的解决方案

问题根源

dotnet test默认会为每个测试项目启动独立的进程,你之前实现的项目内静态单例无法跨进程共享,所以每个项目进程都会创建自己的Cassandra容器,导致资源过载。


方案1:利用TestContainers容器复用特性(推荐)

TestContainers原生支持容器复用,开启后同一个镜像的容器会被所有测试进程共享,直到手动销毁。

实现步骤

  1. 封装共享容器类,开启复用并配置固定标识:
public class SharedCassandraContainer : IAsyncLifetime
{
    private readonly CassandraContainer _container;
    public ISession Session { get; private set; }

    // 静态单例保证项目内唯一
    private static readonly object _lock = new object();
    private static SharedCassandraContainer _instance;

    private SharedCassandraContainer()
    {
        _container = new CassandraContainer("cassandra:latest")
            .WithReuse(true) // 开启容器复用
            .WithName("shared-test-cassandra") // 固定容器名称,跨进程识别
            .WithPortBinding(9042, 9042); // 固定端口映射,避免随机端口问题
    }

    public static SharedCassandraContainer Instance
    {
        get
        {
            lock (_lock)
            {
                _instance ??= new SharedCassandraContainer();
                return _instance;
            }
        }
    }

    public async Task InitializeAsync()
    {
        await _container.StartAsync();
        // 初始化Cassandra会话
        var cluster = Cluster.Builder()
            .AddContactPoint(_container.Hostname)
            .WithPort(_container.GetMappedPublicPort(9042))
            .Build();
        Session = await cluster.ConnectAsync();
    }

    public async Task DisposeAsync()
    {
        Session?.Dispose();
        // 复用模式下不要在这里销毁容器,否则其他项目无法复用
        // 测试完成后手动执行:`docker rm -f shared-test-cassandra` 清理
    }
}
  1. 在各测试项目的测试类中注入使用:
public class OrderTests : IClassFixture<SharedCassandraContainer>
{
    private readonly ISession _cassandraSession;

    public OrderTests(SharedCassandraContainer container)
    {
        _cassandraSession = container.Session;
    }

    // 测试方法示例
    [Fact]
    public async Task CanInsertOrder()
    {
        // 使用_cassandraSession执行测试逻辑
    }
}

注意事项

  • 固定端口需确保本地无其他进程占用9042,否则启动失败
  • 复用模式下容器不会自动销毁,需手动清理或在CI流程中添加清理步骤

方案2:跨进程文件锁+共享连接信息

通过文件锁保证只有第一个测试进程启动容器,其他进程读取共享文件获取连接参数。

核心代码示例

public class SharedCassandraContainer : IAsyncLifetime
{
    public ISession Session { get; private set; }
    private readonly string _lockFilePath = Path.Combine(Path.GetTempPath(), "test-cassandra.lock");
    private readonly string _connInfoPath = Path.Combine(Path.GetTempPath(), "test-cassandra-conn.json");
    private CassandraContainer _container;

    public async Task InitializeAsync()
    {
        // 尝试获取跨进程锁
        using var lockStream = new FileStream(_lockFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
        try
        {
            lockStream.Lock(0, 0);
            // 只有拿到锁的进程启动容器
            _container = new CassandraContainer("cassandra:latest")
                .WithPortBinding(9042, 9042);
            await _container.StartAsync();

            // 写入连接信息到共享文件
            var connInfo = new { Host = _container.Hostname, Port = _container.GetMappedPublicPort(9042) };
            File.WriteAllText(_connInfoPath, JsonSerializer.Serialize(connInfo));
        }
        catch (IOException)
        {
            // 未拿到锁,等待连接信息文件生成
            while (!File.Exists(_connInfoPath))
            {
                await Task.Delay(1000);
            }
        }
        finally
        {
            lockStream.Unlock(0, 0);
        }

        // 读取连接信息并创建会话
        var connData = JsonSerializer.Deserialize<dynamic>(File.ReadAllText(_connInfoPath));
        var cluster = Cluster.Builder()
            .AddContactPoint(connData.Host)
            .WithPort((int)connData.Port)
            .Build();
        Session = await cluster.ConnectAsync();
    }

    public async Task DisposeAsync()
    {
        Session?.Dispose();
        // 只有启动容器的进程负责销毁
        if (_container != null)
        {
            await _container.StopAsync();
            File.Delete(_connInfoPath);
            File.Delete(_lockFilePath);
        }
    }
}

方案3:禁用测试项目并行执行

如果可以接受测试串行执行,在解决方案根目录的Directory.Build.props中添加配置:

<Project>
  <PropertyGroup>
    <VSTestParallelize>false</VSTestParallelize>
  </PropertyGroup>
</Project>

这样dotnet test会逐个运行测试项目,项目内的单例就能生效,不会创建多个容器,但测试执行时间会变长。


内容的提问来源于stack exchange,提问作者Kalpani

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 18:08:08