テストの第三世代

Robの意見:永続化とテストに関して、私たちは現在「テストの第三世代」にいると考えています。

(I) 第一世代 - すべての永続化をモック/スタブ化

第一世代では、実データベースに対するテストは一般的に遅すぎ、コストがかかりすぎ、困難でした。テストでは、すべての永続化(多くの場合、リポジトリAPIを介して)をモック/スタブ化していました。テストは、「統合サーバー」でのみ実データベースに対して実行されました。

(II) 第二世代 - インメモリDB

第二世代のテストでは、H2のようなインメモリデータベースが利用可能になりました。これにより、開発者はすべての永続化APIをモックアウトするのではなく、これらのインメモリデータベースを使用できるようになりました。

これにより、テストで使用されるモック/スタブの量が大幅に減少しました。

H2のようなインメモリDBを使用したテストの制限は、PostgresやOracleなどの「実際の」ターゲットデータベースと比較した機能の違いにあります。型(UUID、配列、Json、Hstore、範囲型など)と機能(SQL関数、高度なロック、テーブルパーティショニングなど)の違いです。

(III) 第三世代 - Docker DBを使用したテスト

H2に対するテストの制限は、この「永続化テストの第三世代」で対処できるようになりました。Dockerにより、開発マシンへのデータベースのインストール/セットアップを自動化することが容易になりました。開発マシンは、(Dockerバージョンのデータベースは本物と同じ機能を持つため)「実際の」ターゲットデータベースに対して十分な速度でテストを実行できるほど強力です。

  • テストはデータベース固有の機能と型を網羅できます(テストカバレッジに関する言い訳はありません)
  • Dockerテストコンテナの使用の容易さは、H2の使用の容易さに匹敵する必要があります。
  • 新しい/クリーンな一時データベースに対してテストを実行したいと考えています。
  • テストを高速に実行する必要があります - インメモリデータベースのエクスペリエンスに匹敵します。
  • CIサーバーで実行されるビルド/テストは、開発マシンで実行されるビルド/テストと一致する必要があります。

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は、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

各データベースプラットフォームの詳細については、以下を参照してください。
clickhousecockroachdb2h2hanamariadbmysqlnuodboraclepostgressqlitesqlserveryugabytedb

DDLモード

ほとんどの場合、dropCreateモードを使用します。これは、すべてのテストを実行する前にデータベースが削除されてから再作成されることを意味します。

データベースのマイグレーションをテストするためにマイグレーションを使用します。

モード 説明
dropCreate すべてのテーブルなどを削除してから作成します。最も一般的に使用されるモードです。
none DDLを実行しません。DDLの変更なしで特定のテストを実行したい場合に役立ちます。
migrations DBマイグレーションを実行しますが、最初にデータベースを削除して、マイグレーションが新しいデータベースに対して実行されるようにします。
create create-all.sql DDLスクリプトを実行しますが、最初にデータベースを削除して再作成します。

dropCreatedb-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コンテナを停止して削除したいと考えています。これを行うには、shutdownremove(コンテナを停止して削除する)または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://: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の生成とdb-create-all.sqlおよびdb-drop-all.sqlの実行を制御する適切なプロパティを設定する必要があります。以下のプロパティを使用して、DDLの生成とdb-create-all.sqlおよびdb-drop-all.sqlの実行を制御します。

プロパティ 説明
ddl.generate db-create-all.sqldb-drop-all.sqlのDDLスクリプトを生成するには、trueに設定します。
ddl.run db-create-all.sqldb-drop-all.sql、および追加のDDLスクリプトを実行するには、trueに設定します。
ddl.createOnly db-create-all.sqlを実行するが、db-drop-all.sqlを実行しないようにするには、trueに設定します。データベースにデータが入力されていない/削除するテーブルがないことがわかっている場合、主にH2データベースのインメモリテストで使用されます。
ddl.initSql create-all ddlの実行前に実行するSQLスクリプトを指定します。
ddl.seedSql create-all DDLを実行した後に実行するSQLスクリプトを指定します。通常、これはテストデータベースにシードデータ挿入します。
ebean.migration.run EbeanServerの起動時にマイグレーションを実行するかどうかをtrueまたはfalseで設定します。

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スクリプトやTRUNCATE 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を使用すると、他のデータベース(PostgreSQLなど)なしでElasticSearchを単独で使用したり、他の[真実のソース]データベース(PostgreSQLなど)と組み合わせてElasticSearchを使用したりできます。

DockerコンテナとしてElasticSearchを自動的に起動するには、下記のように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

詳細については、データベース/Elasticsearchを参照してください。

Redis

L2キャッシングにRedisを使用する場合、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

詳細については、データベース/Redisを参照してください。