Spock + Testcontainers でJavaのテストコードを書いてみた
目的
勉強用にSpock(Groovy)とTestcontainersでテストコードを書いてみました。
準備
Testcontainersはデフォルトで/var/run/docker.sock
からDocker daemonにアクセスしようとします。そこで実行環境のDockerソケットにシンボリックリンクを作ってあげます。
$ sudo ln -s $HOME/.rd/docker.sock /var/run/docker.sock
参考:
依存ライブラリを追加
今回はGradleで管理するのでbuild.gradle.kts
に以下のライブラリを追加します。コンテナのDBエンジンはMySQLを選択しました。
のちほど説明しますがFlywayはDDLで使用します。
// Spock
testImplementation("org.spockframework:spock-core:2.4-M1-groovy-4.0")
testImplementation("org.spockframework:spock-spring:2.4-M1-groovy-4.0")
// Testcontainers
testImplementation("org.testcontainers:spock:1.20.2")
testImplementation("org.testcontainers:mysql:1.20.2")
// Flyway
testImplementation("org.flywaydb:flyway-core:10.19.0")
testImplementation("org.flywaydb:flyway-mysql:10.19.0")
テスト対象のソースコード
以下のMyBatisのMapperをテストしてみます。
熊の情報をDBから取得するコードになります。
@Mapper
public interface DbMapper {
List<Bear> getAllBears();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ttsham6.bear.catalog.mapper.DbMapper">
<select id="getAllBears" resultType="com.ttsham6.bear.catalog.model.Bear">
SELECT * FROM bear ORDER BY id
</select>
</mapper>
テストコード全体
最初にテストコード全体をお見せします。
@SpringBootTest
@Testcontainers
class DbMapperSpec extends Specification {
@Shared
MySQLContainer mySQLContainer = new MySQLContainer("mysql:8.0")
.withDatabaseName("bear_db")
.withUsername("app")
.withPassword("P@ssw0rd")
@Autowired
DbMapper dbMapper
def setupSpec() {
mySQLContainer.start()
// DDL by flyway
Flyway flyway = Flyway.configure()
.dataSource(
mySQLContainer.getJdbcUrl(),
mySQLContainer.getUsername(),
mySQLContainer.getPassword()
).locations("classpath:sql/test")
.load()
flyway.migrate()
// setup datasource
System.setProperty("spring.datasource.url", mySQLContainer.getJdbcUrl())
System.setProperty("spring.datasource.username", mySQLContainer.getUsername())
System.setProperty("spring.datasource.password", mySQLContainer.getPassword())
System.setProperty("spring.datasource.driver-class-name", mySQLContainer.getDriverClassName())
System.setProperty("spring.datasource.hiakri.connection-timeout", "20000")
System.setProperty("spring.datasource.hikari.maximum-pool-size", "5")
}
def cleanupSpec() {
mySQLContainer.stop()
}
def setup() {
def connection = mySQLContainer.createConnection("")
def statement = connection.createStatement()
statement.executeUpdate("DELETE FROM habitat")
statement.executeUpdate("DELETE FROM bear")
statement.close()
connection.close()
}
def "getAllBears_正常系_全件取得"() {
given:
def connection = mySQLContainer.createConnection("")
def statement = connection.createStatement()
statement.executeUpdate(
"INSERT INTO bear (id, name, scientific_name, species, color, is_alive)\n" +
"VALUES\n" +
"(1000, 'ヒグマ', 'Ursus Arctos', 'ヒグマ', '茶色', true),\n" +
"(1001, 'ツキノワグマ', 'Ursus Thibetanus', 'ツキノワグマ', '黒', true),\n" +
"(1002, 'ホッキョクグマ', 'Ursus Maritimus', 'ホッキョクグマ', '白', true);"
)
when:
def result = dbMapper.getAllBears()
then:
result.size() == 3
result.get(0) == new Bear(1000, "ヒグマ", "Ursus Arctos", "ヒグマ", "茶色", true)
result.get(1) == new Bear(1001, "ツキノワグマ", "Ursus Thibetanus", "ツキノワグマ", "黒", true)
result.get(2) == new Bear(1002, "ホッキョクグマ", "Ursus Maritimus", "ホッキョクグマ", "白", true)
cleanup:
statement.close()
connection.close()
}
}
ここからポイントに絞って解説していきます。
Testcontainers設定
@Shared
MySQLContainer mySQLContainer = new MySQLContainer("mysql:8.0")
.withDatabaseName("bear_db")
.withUsername("app")
.withPassword("P@ssw0rd")
MySQLコンテナの設定をします。重要なのはDB名のみでユーザー名とパスワードは適当なもので大丈夫です。
def setupSpec() {
mySQLContainer.start()
setupSpec()
はこのクラス内で一度だけ実行されるメソッドになります。
最初にMySQLコンテナを起動します。
// DDL
Flyway flyway = Flyway.configure()
.dataSource(
mySQLContainer.getJdbcUrl(),
mySQLContainer.getUsername(),
mySQLContainer.getPassword()
).locations("classpath:sql/test")
.load()
flyway.migrate()
FlywayでDDLを実行します。接続情報はmySQLContainer
から取得できます。
CREATE DATABASE IF NOT EXISTS bear_db;
USE bear_db;
CREATE TABLE IF NOT EXISTS bear (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255),
scientific_name VARCHAR(255),
species VARCHAR(255),
color VARCHAR(255),
is_alive BOOLEAN
);
DDLファイルは.locations
で指定したディレクトリ(今回はtest/resources/sql/test
)に配置しました。ファイル名のプレフィックスにV*__
を付けないとFlywayが読み込んでくれないので注意してください。
// setup datasource
System.setProperty("spring.datasource.url", mySQLContainer.getJdbcUrl())
System.setProperty("spring.datasource.username", mySQLContainer.getUsername())
System.setProperty("spring.datasource.password", mySQLContainer.getPassword())
System.setProperty("spring.datasource.driver-class-name", mySQLContainer.getDriverClassName())
System.setProperty("spring.datasource.hiakri.connection-timeout", "20000")
System.setProperty("spring.datasource.hikari.maximum-pool-size", "5")
Springのdatasource設定をします。Flywayと同様にDBの接続情報はmySQLContainer
から取得しました。
ここまでが setupSpec()
の内容になります。
def cleanupSpec() {
mySQLContainer.stop()
}
cleanupSpec()
を使ってすべてのテストが終わったらMySQLコンテナを停止します。
DB初期化
テスト実行前にsetup()
メソッドでDB内のデータを初期化します。
def setup() {
def connection = mySQLContainer.createConnection("")
def statement = connection.createStatement()
statement.executeUpdate("DELETE FROM bear")
statement.close()
connection.close()
}
setupSpec()
がテストクラスの最初に1回だけ呼ばれるメソッドであるのに対し、setup()
は各テストの実行前に呼ばれます。今回はデータの初期化を行なっていますが、すべてのテストで共通に利用するデータをinsertすることもできます。
テスト実行
最後にテストメソッドの解説になります。
def "getAllBears_正常系_全件取得"() {
given:
def connection = mySQLContainer.createConnection("")
def statement = connection.createStatement()
statement.executeUpdate(
"INSERT INTO bear (id, name, scientific_name, species, color, is_alive)\n" +
"VALUES\n" +
"(1000, 'ヒグマ', 'Ursus Arctos', 'ヒグマ', '茶色', true),\n" +
"(1001, 'ツキノワグマ', 'Ursus Thibetanus', 'ツキノワグマ', '黒', true),\n" +
"(1002, 'ホッキョクグマ', 'Ursus Maritimus', 'ホッキョクグマ', '白', true);"
)
when:
def result = dbMapper.getAllBears()
then:
result.size() == 3
result.get(0) == new Bear(1000, "ヒグマ", "Ursus Arctos", "ヒグマ", "茶色", true)
result.get(1) == new Bear(1001, "ツキノワグマ", "Ursus Thibetanus", "ツキノワグマ", "黒", true)
result.get(2) == new Bear(1002, "ホッキョクグマ", "Ursus Maritimus", "ホッキョクグマ", "白", true)
cleanup:
statement.close()
connection.close()
}
各ブロックで行なっていることは以下のとおりです。
-
given
: テストケース固有のデータをinsert -
when
: テスト対象メソッドの実行 -
then
: テスト結果の評価 -
cleanup
: MySQLコンテナのconnectionをクローズ
最後に
ソースコード全体をgithubに置いておきます。
Discussion