目录:网上冲浪指南

在 ASP.NET Core 集成测试中连接数据库

2020/02/13

现在大部分的网站都会使用外部的数据库作为数据存储,不过这些外部数据库在测试的时候却会时不时的给我们使些绊子。为了能够在集成测试中模拟数据访问层,大家想出了各种各样的方案,一种是替换数据提供器,例如 EF Core 的 InMemory Db ProviderSQLite InMemory Mode,还有一种是直接去 Mock DbContext。不过这两种方案在一些特殊的情况下就犹如隔靴搔痒,没办法直击痛点。如果你也遇到了这种情况,不如就直接上真的数据库来协助进行集成测试。

整个 SQL Server

测试环境往往是云服务器上的 GitLab Runner,那么直接使用 docker 就可以快速方便的启动一个真实的数据库。

.gitlab-ci.yaml
test-web:
  stage: test
  services:
    - name: mcr.microsoft.com/mssql/server:2017-latest-ubuntu
      alias: dbserver-test
      command: ["/opt/mssql/bin/sqlservr"]
  variables:
    ACCEPT_EULA: Y
    MSSQL_SA_PASSWORD: [email protected]
    TZ: Asia/Shanghai
    TEST_DB_HOST: dbserver-test
    TEST_DB_PORT: 1433
  image: zeekozhu/aspnetcore-build-yarn:3.1-chromium
  script:
    - bash fake.sh -t Web:Test

在本机的上面运行测试的话,我个人通常使用 docker-compose 来运行数据库软件,无他,因为我用的 Linux,用 Docker 是最简单的方案。

docker-compose.yaml
services:
  dbserver-test:
    image: mcr.microsoft.com/mssql/server:2017-latest-ubuntu
    command: ["/opt/mssql/bin/sqlservr"]
    environment:
      - ACCEPT_EULA=Y
      - [email protected]
      - TZ=Asia/Shanghai
    ports:
      - "1434:1433"

因为只是用来跑集成测试用的,所以没必要持久化数据。

自动创建表结构

因为用的是 EF Core,而且代码中也没有用到存储过程,所以可以使用 EF Core 在测试服务器启动的时候来创建数据库表结构。不过 EF Core 并没有办法来创建数据库对象,创建数据库的部分还是需要我们自己来完成。

public static class TestDbBuilder
{
    private static int _seqNumber;

    private static string TestDbName => "BlogTestDb" + Interlocked.Increment(ref _seqNumber); (1)

    private static string BuildConnStr(string dbName)
    {
        var host = Environment.GetEnvironmentVariable("TEST_DB_HOST") ?? "dbserver-test";
        var port = Environment.GetEnvironmentVariable("TEST_DB_PORT") ?? "1434";
        return
            $"Data Source={host},{port};Initial Catalog={dbName};Persist Security Info=True;User ID=sa;[email protected];MultipleActiveResultSets=true";
    }

    public static string PrepareDbConnectionString()
    {
        var dbName = TestDbName;
        var connStr = BuildConnStr("master"); (2)
        using var conn = new SqlConnection(connStr);
        var createDbCmd = new SqlCommand(
            [email protected]"
DROP DATABASE IF EXISTS {dbName}; (3)
CREATE DATABASE {dbName} COLLATE Chinese_PRC_CI_AS",
            conn);
        conn.Open();
        createDbCmd.ExecuteNonQuery();
        return BuildConnStr(dbName);
    }
}
1 集成测试可能会并发执行,所以需要为不同的测试生成不同的数据库
2 在业务数据库还没创建的时候,需要先连接到 master 数据库执行创建业务数据库的 SQL
3 创建业务数据库的时候也要记得将之前测试中创建的数据库给删除掉

创建好数据库之后,就可以使用 DbContext.EnsureCreated() 来生成表结构。

public void Configure(
    IApplicationBuilder app,
    IWebHostEnvironment env,
    ILoggerFactory loggerFactory,
    IHostApplicationLifetime lifetime)
{
    lifetime.ApplicationStarted.Register(
        () =>
        {
            using var scope = app.ApplicationServices.CreateScope();
            scope.ServiceProvider.GetService<BlogDbContext>().EnsureCreated();
        });

    // ...
}

终于,我们能够在集成测试中用上真实的数据库了,他的表现几乎跟线上的数据库行为一致,而且不存在任何 ORM 的兼容性问题,但代价是我们将需要更多的资源以及时间用来运行集成测试。这值得吗,可能只有你自己才知道。