Daoのテスト、ちゃんとやってますか?
システム開発でDao(Data Access Object)を作成する場合が多いですが、ちゃんとDaoの単体テストしてますか?
Daoの単体テストの場合はMockではなく実際のDBにデータを入出力して動作確認したほうが本来のテストの目的が果たせます。
色々な方法があるのですが、kotlin、springBoot、exposed、DatabaseRiderを使った例を紹介します。
テスト用のDBインスタンスを用意する
testCaseの中で実際にテーブルに行をINSERT、DELETE、UPDATEするので本番で使用するDBおは別なインスタンスを用意します。テーブル、view、カラム等のメタデータは本番のDBと一致している必要があります。
テーブルの中身は必要ないので本番のDBからメタデータだけを抜き出してテスト用のインスタンスにコピーします。
PostgreSQLであれば、
pg_dump -s -U ユーザ名 -W databasename > databasename.sql
パスワード:xxxxx
でDDL文をしてアフィルに出力できるので、これをテスト用のDBで読込ます
psql -U ユーザ名 -W newdb
パスワード:xxxxx
newdb=> \i databasename.sql
シーケンスを使っている場合はシーケンスの現在値に注意しましょう。(currentValueもDDLとして出力される)シーケンスは、シーケンスのcurrentValueにテストケースの結果が左右されるのは好ましくないので本来であればテストケースの中でシーケンスのcurrentValueを設定するのが好ましいです。
依存関係を追加する
dependencies {
testImplementation("com.google.truth:truth:1.4.2")
testImplementation("org.springframework.boot:spring-boot-starter-test:3.3.0")
testImplementation("com.github.database-rider:rider-core:1.42.0") // ★必須
testImplementation("com.github.database-rider:rider-spring:1.42.0") // ★必須
testImplementation("com.github.database-rider:rider-junit5:1.42.0") // ★必須
testImplementation("com.ninja-squad:springmockk:4.0.2")
testImplementation(kotlin("test"))
これはあくまでも例ですが、DatabaseRiderとして必須なのは
com.github.database-rider:rider-xxxx
の3つです。springBootを使用しているので、spring-boot-starter-testを追加しています。
mockライブラリとしてはspringmockk、
assertionライブラリとしてはtruth
を使用していますが、この2つは自分の好みです。
dbunit.ymlを作成する
DatabaseRiderは内部でdbUnitを使用しているので、dbunit.ymlを作成します。
以下はPostgreSQLの場合の例です
cacheConnection: true
cacheTableNames: true
leakHunter: false
mergeDataSets: false
mergingStrategy: METHOD
caseInsensitiveStrategy: LOWERCASE
raiseExceptionOnCleanUp: false
expectedDbType: UNKNOWN
disableSequenceFiltering: false
disablePKCheckFor: [""]
alwaysCleanBefore: false
alwaysCleanAfter: false
properties:
batchedStatements: false
qualifiedTableNames: false
schema: public
caseSensitiveTableNames: false
batchSize: 100
fetchSize: 100
allowEmptyFields: true
escapePattern:
connectionConfig:
driver: "org.postgresql.Driver"
url: "jdbc:postgresql://localhost:5432/test"
user: "test"
password: "test"
ここで注意しなければならないのが、caseSensitiveTableNamesです。
PostgreSQLの場合はテーブル名、カラム名等は””で括らない限りは小文字です。caseSensitiveTableNamesはfalseを設定します。ここはDBシステムによって異なります。
connectionConfigにテスト用のDBの接続先を設定します。これとは別にspringBootの場合はapplication.yml(properties)にもDBの接続先を設定します。
dbunit.ymlのconnectionConfigはDatabaseRiderがテスト用データをテストケース実施前にINSERTする場合、使われます。application.yml(properties)はテスト対象クラスが使用するDBの接続先です。基本、一致している必要があります。(すれ違っていたらテストにならない)
テストケース毎にテーブルを空にする
ここからが、DatabaseRiderを使用する上での小技集になります。
テストケース毎の独立性、他のテストケースとの干渉を避けるために、テストケース毎、開始前に対象テーブルを空にすべきです。その上でそのテストケースに必要な事前データをテーブルにINSERTすべきです。
@Test
@DisplayName("SELECTテスト")
@DBRider
@DataSet(
executeStatementsBefore = [
"truncate table test.test_table1",
"truncate table test.test_table2",
"truncate table test.test_table3"],
)
@Transactional
fun selectTest01() {
・・・・
のようにtruncate文を複数書けますが、そのシステムでテーブル数が数百もあると全部書くのは面倒です。必要なものだけ書いてもテスト対象クラスを書き直すと、ここも増減があるので修正漏れが発生する場合があります。
そういう場合はSQLスクリプトを実行するようにします。
@Test
@DisplayName("SELECTテスト")
@DBRider
@DataSet(
executeScriptsBefore = ["truncateAll.sql"],
)
@Transactional
fun selectTest01() {
・・・・
truncate table test.test_table1;
truncate table test.test_table2;
truncate table test.test_table3;
・・・
のようにSQLスクリプト文を外だしにして、そちらに全テーブル分のtruncate文を書けば漏れがありません。
テストデータにymlではなくExcelを使用する
DatabaseRiderはテストケース実行時に必要なテストデータをINSERTすることができますが、ymlを使う方法とexcelを使う方法があります。
ymlはテキスト形式なのですが、テストデータが複数件あると、縦に行が伸びていくのでテストデータの行を跨いでの目視がやりずらいです。
table_name:
# 1行目
- col_1: aaaaa
col_2: bbbbb
# 2行目
- col_1: aaaaa
col_2: bbbbb
・・・
これはExcelを使うと表形式なので目視がしやすく、これらを解決できます。ExcelのworkSheet、1ファイルでそのテストに必要なデータ、複数テーブルを定義できるのでこちらのほうが便利。
- シートがテーブル単位、複数テーブルINSERTできる
- シート名がテーブル名(PostgreSQの場合、スキーマ名は省略できる、public.test_table ⇒ test_table)
- セルが空の場合はカラムにもnullが入る
- カラムが日付、日時型の場合は、Excelのセルの書式も日付型である必要がある
- カラムが数値型(Numeric)、VARCHAR型の場合はExcelのセルの書式は標準でもいけるようだ。当然、数値型のカラムに数値ではないものが入っているとエラーになる
テスト結果のassertion
テーブルをINSERT、UPDATE、DELETEするようなテストの場合、その結果もExcelでassetionすることができます。
@Test
@DisplayName("更新するテスト")
@DBRider
@DataSet(
executeScriptsBefore = ["truncateAll.sql"],
value = ["DeleteTestData.xlsx"]
)
@ExpectedDataSet(
value = [ "DeleteTes01Expect.xlsx" ],
orderBy = ["id"], // ソート順
ignoreCols = ["cre_dt", "ope_dt"] // 無視するカラム
)
fun deleteTest01() {
・・・
}
@ExpectedDataSetでテーブルの期待値をExelで定義することができます。
この時にテーブルのソート順を指定しないとassertion Errorになる場合があります。
テスト対象クラスがシーケンスを使っていたり、常にCurrentTimestampでカラムを更新する場合(上の場合作成日時、更新日時)、そこは都度結果が違うので、無視するカラムに追加しておきます。
最後に
DatabaseRiderの使い方、小技の紹介でした。