测试的第三个时代
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 clone
和 mvn clean test
,它就能“正常工作”,无需任何设置步骤。
对于 Postgres、MySql、SQL Server,我们今天可以非常成功地使用 docker 测试容器。对于像 Oracle、SAP Hana 和 DB2 这样相对较重的数据库,我们可以成功,但根据情况,可能会有人主张在测试中坚持使用 H2。
就我个人而言,在过去几年中使用 ebean-test/docker 测试容器取得了很好的效果(主要是 Postgres)。我强烈建议人们考虑这种方法,以实现出色的测试覆盖率、更简单的测试代码以及开发者、CI 和生产之间的差异性降低。
ebean-test 还支持使用 ElasticSearch 和 Redis 容器。
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.sql
和 db-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 clone
和 mvn 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.sql
和 db-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 了解更多详情。