このあたりをいい感じに書けるとMySQL周りのユニットテストが簡易にできるので、
それぞれについて書きつつ、最終的にサンプルを示す。
なおtestcontainersを使ってこのへんを一気にやってしまうということもできるが、コンテナのランタイムが必要になるため今回はスキップ。
1. MySQLの実行環境の用意
よくある手だがh2dbでmysql互換モードがあるのでそれを利用する。
h2dbのurlを以下のように生成することでmysql互換モードをonにしつつ初期化と同時にDBスキーマを生成するよう設定している。
private val databaseName = "example" private val databaseUrl = { val h2Options = Map( "MODE" -> "MYSQL", "DATABASE_TO_LOWER" -> "TRUE", "DB_CLOSE_DELAY" -> "-1", "INIT" -> s"CREATE SCHEMA IF NOT EXISTS ${databaseName}" ).map { case (key, value) => s"${key}=${value}" }.mkString(";") s"jdbc:h2:mem:test;${h2Options}" // jdbc:h2:mem:test;MODE=MYSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS example } // テスト用のDBを宣言 private val h2Database = Database.forURL(url = databaseUrl, driver = "org.h2.Driver")
2. DDLの適用
スキーマが存在しているだけだとクエリが投げられないのでDDLを流す必要があるが、 slickの TableQuery[T].schema
でDDLやtruncateなどのDMLがActionとして生成できる様になっているのでこれを利用。
例えばこんな感じでテーブル定義が存在していた場合に、、、
object Tables { import slick.jdbc.MySQLProfile.api._ class User(tag: slick.lifted.Tag) extends Table[(Int, String, String)](tag, "USER") { def id = column[Int]("id", O.PrimaryKey) def surname = column[String]("surname") def givenName = column[String]("given_name") def * = (id, surname, givenName) } }
以下のようにするとCREATE文を発行するActionが生成できる
TableQuery[Tables.User].schema.createIfNotExists.schema.createIfNotExists
その他はSlickのドキュメントを参照 scala-slick.org
また、 slick-codegenでコード生成している場合は、生成したすべてのテーブルに対するDDLを合わせて一つにしたものが Tables.schema
で呼び出せて非常に有用である。
Tables.schema
として以下のようなコードが生成される。
lazy val schema: profile.SchemaDescription = Array( XXX.schema: , YYY.schema ).reduceLeft(_ ++ _)
上記をあわせてテストコードのサンプル
任意のテストコードに対して BeforeAndAfterAll
と BeforeAndAfter
をミックスインして以下を実装する。
- beforeAllでDDLを流す
- beforeでTRUNCATE文を実行してデータをクリアする
- afterAllでDBをクローズ
これにより各テストケース内では常にきれいなDBが提供された状態となるため、
あとはテストケースを好きなだけ生やしていけばよい。
class ExampleDatabaseSpec extends AsyncFunSpec with BeforeAndAfterAll with BeforeAndAfter { // ProfileはMySQL固定(h2dbは使うがMySQL互換モードで動かすので) import slick.jdbc.MySQLProfile.api._ // テスト用のDBを宣言 private val databaseName = "example" private val databaseUrl = { val h2Options = Map( "MODE" -> "MYSQL", "DATABASE_TO_LOWER" -> "TRUE", "DB_CLOSE_DELAY" -> "-1", "INIT" -> s"CREATE SCHEMA IF NOT EXISTS ${databaseName}" ).map { case (key, value) => s"${key}=${value}" }.mkString(";") s"jdbc:h2:mem:test;${h2Options}" // jdbc:h2:mem:test;MODE=MYSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS example } private val h2Database = Database.forURL(url = databaseUrl, driver = "org.h2.Driver") override protected def beforeAll(): Unit = { // DDLを実行してテーブルを作成 Await.ready(h2Database.run(TableQuery[Tables.User].schema.createIfNotExists), 1.second) super.beforeAll() } override protected def afterAll(): Unit = { // DBを終了 h2Database.close() super.afterAll() } before { // テスト実行前にテーブルをまっさらにする Await.ready(h2Database.run(TableQuery[Tables.User].schema.truncate), 1.second) } it("when no data in db => return Nil") { // given: 何も与えない // when: DBにselectクエリを投げる val result = h2Database.run(TableQuery[Tables.User].result) // then: 空配列が返却される val expected = Nil result.map(res => assert(res == expected)) } }