测试的第三个时代

Rob 观点:我认为在测试和持久化方面,我们现在处于测试的“第三个时代”。

(I)第一个时代 - 所有持久化都经过模拟/存根处理

在第一个时代,针对真实数据库进行测试通常太慢、太昂贵且太困难。测试模拟/存根处理所有持久化(通常通过存储库 API)。测试仅在“集成服务器”上针对真实数据库运行。

(II)第二个时代 - 内存数据库

在第二个测试时代,H2 等内存数据库变得可用。这允许开发人员在内存数据库中使用这些数据库,而不是模拟所有持久化 API。

这导致在测试中使用更少的模拟/存根处理。

使用 H2 等内存数据库进行测试的局限性在于,与 Postgres 或 Oracle 等“真实”目标数据库相比,功能存在差异。类型(例如 UUID、数组、Json、Hstore、范围类型)和功能(SQL 函数、高级锁定、表分区等)存在差异。

(III)第三个时代 - 使用 docker 数据库进行测试

在持久性测试的“第三时代”,现在可以解决针对 H2 进行测试的局限性。Docker 使得在开发者机器上自动安装/设置数据库变得更加容易。开发者机器足够强大,可以针对“真实”目标数据库快速运行测试(其中数据库的 docker 版本在功能上与真实版本相同)。

  • 测试可以涵盖特定于数据库的功能和类型(测试覆盖率没有借口)
  • 使用 docker 测试容器的便利性需要与使用 H2 的便利性相匹配
  • 我们希望针对新的/干净的临时数据库运行测试
  • 我们需要快速运行测试 - 匹配内存数据库体验
  • 在 CI 服务器上运行的 build/tests 与在开发者机器上运行的 build/tests 相匹配

提供 ebean-test 以使针对 docker 测试容器的测试像使用 H2 一样简单且良好。刚接触某个项目的新开发者只需 git clonemvn clean test,它就能“正常工作”,无需任何设置步骤。

对于 Postgres、MySql、SQL Server,我们今天可以非常成功地使用 docker 测试容器。对于像 Oracle、SAP Hana 和 DB2 这样相对较重的数据库,我们可以成功,但根据情况,可能会有人主张在测试中坚持使用 H2。

就我个人而言,在过去几年中使用 ebean-test/docker 测试容器取得了很好的效果(主要是 Postgres)。我强烈建议人们考虑这种方法,以实现出色的测试覆盖率、更简单的测试代码以及开发者、CI 和生产之间的差异性降低。

ebean-test 还支持使用 ElasticSearchRedis 容器。

ebean-test 依赖

1. 将 ebean-test 添加为测试依赖项

<dependency>
  <groupId>io.ebean</groupId>
  <artifactId>ebean-test</artifactId>
  <version>13.25.0</version>
  <scope>test</scope>
</dependency>

2. 添加 application-test.yaml

application-test.yaml 配置文件添加到 src/test/resources 中,我们将其用于测试。

ebean:
  test:
    platform: h2 #, h2, postgres, mysql, oracle, sqlserver, hana, clickhouse, sqlite
    ddlMode: dropCreate # none | dropCreate | migrations | create
    dbName: my_app

我们可以修改此测试配置以控制执行的 DDL(create-all.sql 或迁移)以及我们希望针对其进行测试的数据库平台(可能使用 docker)。

3. 添加 ~/.ebean/ignore-docker-shutdown

mkdir ~/.ebean
touch ~/.ebean/ignore-docker-shutdown

对于在 CI 服务器上运行测试,我们通常希望在测试完成后停止并删除 docker 容器。但是,对于本地开发,我们希望保持 docker 容器运行,以便更快地运行测试。

添加标记文件 ~/.ebean/ignore-docker-shutdown 意味着 docker 容器将保持运行(这对于本地开发很有用)。

ebean-test 将负责

  • DDL 生成和执行模式
  • 基于数据库平台的 Docker 测试容器设置和执行
  • 当前用户和租户提供程序(如果尚未指定,以简化使用 @Who 属性等的测试)

ebean.test.platform - application-test.yaml

我们使用 ebean.test.platform 指定在运行测试时要使用的数据库平台。例如,我们可以指定 h2、postgres、mysql 等作为运行测试的平台。

例如,针对 Postgres 进行测试
ebean:
  test:
    platform: postgres # h2, postgres, ...
    ddlMode: dropCreate # none | dropCreate | migrations | create
    dbName: my_app
例如,针对 MariaDB 进行测试
ebean:
  test:
    platform: mariadb
    ddlMode: dropCreate # none | dropCreate | migrations | create
    dbName: my_app

有关每个数据库平台的更多详细信息,请参阅
clickhouse , cockroach , db2 , h2 , hana , mariadb , mysql , nuodb , oracle , postgres , sqlite , sqlserver , yugabytedb

DDL 模式

大多数情况下,我们使用 dropCreate 模式,这意味着在运行所有测试之前,数据库将被删除,然后重新创建。

我们使用迁移来测试数据库迁移。

模式 说明
dropCreate 删除,然后创建所有表等。最常用的模式。
none 不运行任何 DDL。如果我们想在没有任何 DDL 更改的情况下运行 1 个特定测试,这将很有用。
migrations 运行数据库迁移,但首先删除数据库,确保迁移针对新数据库运行。
create 运行 create-all.sql DDL 脚本,但首先删除并重新创建数据库。

请注意,dropCreate 将生成 db-create-all.sqldb-drop-all.sql 脚本,这些脚本可以在 maven target 或 gradle build 目录中找到。

Docker 测试容器

Ebean 为 Postgres、MariaDB、MySql、SqlServer、Oracle、Hana、DB2、Clickhouse、CockroachDB、YugabyteDB、Redis、ElasticSearch 以及 DynamoDB 和 Localstack 提供了 docker 测试容器。

ebean-test 自动管理 docker 容器,并将其设置为准备运行测试。作为开发人员,我们只需要指定平台(例如 postgres),Ebean 就会完成剩下的工作

  • 启动 docker 容器
  • 等待容器就绪
  • 创建数据库和用户,并根据需要设置任何权限
  • 准备就绪后,允许运行测试

我们可以通过将 io.ebean.docker 的日志级别提高到 TRACE 来查看/回顾正在发生的事情。这样做时,我们可以看到类似这样的日志消息

... Docker test container start and setup

15:15:02.537 INFO  io.ebean.docker.commands.Commands - Container ut_postgres running with port:6432 db:test_ex user:test_ex mode:Create shutdown:
15:15:02.538 DEBUG io.ebean.docker.commands.Commands - docker exec -i ut_postgres pg_isready -h localhost -p 5432
15:15:02.645 DEBUG io.ebean.docker.commands.Commands - docker exec -i ut_postgres psql -U postgres -c select datname from pg_database
15:15:02.753 DEBUG io.ebean.docker.commands.Commands - docker exec -i ut_postgres psql -U postgres -c select rolname from pg_roles where rolname = 'test_ex'
15:15:02.871 DEBUG io.ebean.docker.commands.Commands - docker exec -i ut_postgres psql -U postgres -c select 1 from pg_database where datname = 'test_ex'
15:15:02.960 DEBUG io.ebean.docker.commands.Commands - create database extensions hstore,pgcrypto
15:15:02.960 DEBUG io.ebean.docker.commands.Commands - docker exec -i ut_postgres psql -U postgres -d test_ex -c create extension if not exists hstore
15:15:03.058 DEBUG io.ebean.docker.commands.Commands - docker exec -i ut_postgres psql -U postgres -d test_ex -c create extension if not exists pgcrypto
15:15:03.143 DEBUG io.ebean.docker.commands.Commands - waitForConnectivity ut_postgres ...
15:15:03.143 DEBUG io.ebean.docker.commands.Commands - checkConnectivity on ut_postgres ...
15:15:03.190 DEBUG io.ebean.docker.commands.Commands - connectivity confirmed for ut_postgres
15:15:03.190 DEBUG io.ebean.docker.commands.Commands - Container ut_postgres ready with port 6432

...

15:15:03.239 [main] INFO  o.a.datasource.pool.ConnectionPool - DataSourcePool [db] autoCommit[false] transIsolation[READ_COMMITTED] min[2] max[200]
15:15:03.277 [main] INFO  io.ebean.internal.DefaultContainer - DatabasePlatform name:db platform:postgres


... DDL Execution

15:15:03.618 [main] INFO  io.ebean.DDL - Executing extra-dll - 0 statements
15:15:03.618 [main] INFO  io.ebean.DDL - Executing db-drop-all.sql - 26 statements
15:15:03.649 [main] DEBUG io.ebean.DDL - executing 1 of 26 alter table if exists address drop constraint if exists fk_address_country_code
15:15:03.651 [main] DEBUG io.ebean.DDL - executing 2 of 26 drop index if exists ix_address_country_code
...
15:15:03.701 [main] INFO  io.ebean.DDL - Executing db-create-all.sql - 28 statements
15:15:03.701 [main] DEBUG io.ebean.DDL - executing 1 of 28 create table address ( id                            bigserial not null, line1...
15:15:03.709 [main] DEBUG io.ebean.DDL - executing 2 of 28 create table contact ( id                            bigserial not null, first_n...
...
15:15:03.841 [main] INFO  io.ebean.DDL - Executing extra-dll - 1 statements
15:15:03.842 [main] DEBUG io.ebean.DDL - executing 1 of 1 create or replace view order_agg_vw as select d.order_id as id, d.order_id as or...

容器启动

为了启动 Docker 容器,ebean-test 钩入 Ebean 生命周期。这意味着,无论从 IDE、maven、gradle 还是任何构建工具运行测试,它都会“正常工作”。在之前的迭代中,Docker 容器的启动是专门钩入 maven 生命周期,但事实证明这样做并不理想。这种方法还避免了修改测试代码的需要。

此 Docker 测试容器集成提供了类似于使用 H2 数据库的开发人员体验。也就是说,ebean-test 会在需要时启动数据库(如果需要),设置数据库用户、角色、架构等(如果需要),并通过运行 DDL(通常是删除和重新创建表等)使数据库准备好进行测试,然后运行所有测试。

对项目不熟悉的开发人员只需 git clonemvn clean test,它就能“正常工作”,无需任何设置步骤(只要开发人员机器上安装了 Docker)。

容器关闭

在开发人员机器上,我们通常希望保持 Docker 容器运行,以便快速运行测试。对于通过 IDE 运行单个测试的开发人员,大多数情况下,这只会删除并重新创建表,因为容器已经运行,与使用内存中 H2 的速度非常接近。

为了在开发人员机器上保持 Docker 容器运行,我们在 ~/.ebean/ignore-docker-shutdown 处放置了一个标记文件。

在 CI 服务器上,我们希望在测试完成后停止并删除 Docker 容器。为此,我们将 shutdown 设置为 remove(停止并删除容器)或 stop(仅停止容器)。

ebean:
  test:
    shutdown: remove  # stop | remove
    platform: postgres # h2, postgres, mysql, oracle, sqlserver
    ddlMode: dropCreate # none | dropCreate | migrations
    dbName: my_app

不使用 Docker

如果我们不想启动和运行 Docker 容器,而是针对其他现有数据库进行测试,我们可以通过设置 useDocker: false 来实现。

以下配置针对现有 Postgres 数据库运行。通常,在不使用 docker 时,我们需要将用户名、密码和 URL 设置为适当的值。

当我们使用 useDocker: false 时,数据库和用户应已存在。

ebean:
  test:
    useDocker: false  ## DO NOT USE DOCKER
    platform: postgres # h2, postgres, mysql, oracle, sqlserver
    ddlMode: dropCreate # none | dropCreate | migrations | create
    dbName: test
    postgres:
      username: test
      password: test
      url: jdbc:postgresql://localhost:5432/test

当前用户和租户

ebean-test 将自动注册一个当前用户提供程序和一个当前租户提供程序。仅当您不自己设置它们时,才会设置它们。

这意味着,无需执行任何操作,我们就可以在测试中使用 @WhoCreated / @WhoModified,并且可以通过 io.ebean.test.UserContext 在测试中设置当前用户和租户。

// set the current userId which will be put
// into 'WhoCreated' and 'WhoModified' properties

UserContext.setUserId("U1");

DDL 生成属性

如果我们使用 ebean-test,则需要设置适当的属性来控制 DDL 的生成和运行。我们使用以下属性来控制 DDL 生成和 db-create-all.sqldb-drop-all.sql 的执行。

属性 说明
ddl.generate 设置为 true 以生成 db-create-all.sql 和 db-drop-all.sql DDL 脚本。
ddl.run 设置为 true 以运行 db-create-all.sql、db-drop-all.sql 和额外的 DDL 脚本
ddl.createOnly 设置为 true 以运行 db-create-all.sql,但运行 db-drop-all.sql。主要用于 H2 数据库内存测试,当我们知道数据库未填充/没有要首先删除的表时。
ddl.initSql 指定在运行 create-all ddl 之前要运行的 SQL 脚本。
ddl.seedSql 指定在运行 create-all ddl 之后要运行的 SQL 脚本。通常,这会将种子数据插入测试数据库。
ebean.migraton.run 设置为 true 或 false 以在 EbeanServer 启动时运行迁移

示例 application-test.properties

ebean.db.ddl.generate=true
ebean.db.ddl.run=true
ebean.db.ddl.initSql=initialise-test-db.sql
ebean.db.ddl.seedSql=seed-test-db.sql

datasource.db.username=sa
datasource.db.password=
datasource.db.databaseUrl=jdbc:h2:mem:tests
datasource.db.databaseDriver=org.h2.Driver

DDL/SQL 脚本运行器

我们可以使用 ScriptRunner 运行 DDL 和 SQL 脚本。通常,这些是用于测试的脚本,例如种子 SQL 脚本或截断 SQL 脚本。

脚本在它们自己的事务中执行,并在成功完成后提交。

简单使用的示例

Database database = DB.getDefault();
database.script().run("/scripts/test-script.sql");

在脚本中使用占位符的示例

Map<String,String> placeholders = new HashMap();
placeholders.put("tableName", "e_basic");

Database database = DB.getDefault();
database.script().run("/scripts/test-script.sql", placeholders);

在我们的 SQL 脚本中,引用占位符的方法是

delete from ${tableName}
select count(*) from ${tableName}

请注意,脚本的路径应以“/”开头。Ebean 将使用 this.getClass().getResource(PATH_TO_RESOURCE) 将脚本加载为资源,因此该资源应在类路径中可用。

ElasticSearch

使用 Ebean,我们可以单独使用 ElasticSearch(不使用其他数据库(Postgres 等)),或者我们可以将 ElasticSearch 与另一个 [真实来源] 数据库(Postgres 等)结合使用。

要将 ElasticSearch 自动启动为 docker 容器,请在 application-test.yaml 中设置 ebean.docstore 属性,如下所示

ebean:
  test:
    platform: h2
    ddlMode: dropCreate # none | dropCreate | migrations | create
    dbName: myapp

  docstore:
    url: http://127.0.0.1:9201
    active: true
    generateMapping: true
    dropCreate: true

    elastic:
      version: 5.6.0
      port: 9201

转到 database / elasticsearch 了解更多详情。

Redis

当我们想将 Redis 用于 L2 缓存时,我们可以让 ebean-test 自动启动一个 redis docker 测试容器。为此,添加 ebean.test.redis=latest 属性,如下例所示

ebean:
  test:
    redis: latest
    platform: h2 # h2, postgres, mysql, oracle, sqlserver, sqlite
    ddlMode: dropCreate # none | dropCreate | migrations | create
    dbName: my_app

转到 database / redis 了解更多详情。