datasorce層のテストとMock
Groovy + Spock構成でMockを使ってテストコードを作ってみる。
- ライブラリ選定
- ライブラリ導入
- テスト対象
- テスト作成
完成したもの
テスト対象
USER_NAME テーブルの全データを返すだけのメソッド。コードはJDBCでH2にsql投げてみるで作ったものを流用
import com.example.springjdbc.domain.UserName;
import com.example.springjdbc.domain.UserNameRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.DataClassRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class UserNameRepositoryJdbcImpl implements UserNameRepository {
@Autowired
public JdbcTemplate jdbcTemplate;
@Override
public List<UserName> findAll() {
return jdbcTemplate.query(
"SELECT * FROM USER_NAME",
new DataClassRowMapper<>(UserName.class)
);
}
}
テスト
正常系としてfindAllメソッドがuserデータを2個返した場合の挙動をテストする。DBにsqlを投げてくれるところはこのデータソース層の責務外であるため、JDBCの挙動はMockとして作成する。ただ、DBに問い合わせてクラスに詰めてくれるのはJDBCがやってくれるので、実際はテストすることが無い。
import com.blogspot.toomuchcoding.spock.subjcollabs.Subject
import com.example.springjdbc.domain.UserName
import org.springframework.jdbc.core.JdbcTemplate
import spock.lang.Specification
class UserNameRepositoryJdbcImplSpec extends Specification {
JdbcTemplate jdbcTemplate = Mock(JdbcTemplate)
@Subject
UserNameRepositoryJdbcImpl sut
def "call findAll method"() {
given:
def userList = [
new UserName("FirstName", "LastName"),
new UserName("FirstName", "LastName")
]
when:
def actual = sut.findAll()
then:
1 * jdbcTemplate.query(*_) >> userList
actual.size() == 2
actual == userList
}
}
1. ライブラリの選定
groovy + spock構成とする。
Groovy
JVM上で動くスクリプト言語らしい。今回の目的はテストコードを書くことなので厳密に型とかを書きたい訳では無い。スクリプト言語らしく(?)型を宣言せずdef var
とかで変数を定義できるので楽。最終的に想定した値か、または挙動かどうかを確認できればよいのでgroovyを採用した。
ドキュメントは一応あるが複雑なことはしないので読んだことは無い。
makes modern programming features available to Java developers with almost-zero learning curve
Spock
Groovyと異なりこれはイロイロテスト便利ツールを提供してくれるJavaとGroovyで使用可能なフレームワーク。Junitが対抗馬だが、セットアップなのか・テスト対象のメソッドを呼び出したのか・結果と期待値を比較ししているのか各フェーズをブロックとして実装する仕組みがある点。また、Mockを提供しているため別途別のライブラリを導入しなくても良い(Junitは別途導入が必要)ため採用した。
2. ライブラリ導入
java17とspring(3.2.4) のプロジェクトにGradle,Spockを入れる。また、テストにはなんだかんだJnuitも必要なため合わせて追加する。 のプロジェクトにいれる。最終的なbuild.gradleは下記。
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.4'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
// テスト
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// Spock + Groovy の依存関係
testImplementation 'org.spockframework:spock-core:2.3-groovy-3.0'
testImplementation 'org.spockframework:spock-spring:2.3-groovy-3.0'
// @Subject用
testImplementation 'com.blogspot.toomuchcoding:spock-subjects-collaborators-extension:2.0.1-groovy-3.0'
}
// useJUnitPlatform() を適用
test {
useJUnitPlatform()
}
Junit導入
Spring Initializrにてtestフレームワークにチェックを入れていたのでbuild.gradleにorg.springframework.boot:spring-boot-starter-test
が入ってた(どうも今は勝手に入るらしくチェック項目自体無いっぽい)。この依存関係の中にJunitも入っているので特にいれる必要がない。
dependencies {
...省略...
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
依存関係はMavenで確認できる。
Groovy・Spock導入
githubのspockレポジトリに多分情報が集まってる。リファレンスとかのリンクもそこのREADME.adocに一覧として載ってる。一応Documentationもあるけど2022年で更新が止まってそうなので、やっぱりgithub見とくのが良さそう。
とか言いながら導入方法は書いてなかったのでspockのhomepageを参考にgradleに依存関係を足す。使用しているGroovyは3系であったため、バージョン指定してbuild.gradle
に足した。
dependencies {
...省略...
testImplementation 'org.spockframework:spock-core:2.3-groovy-3.0'
testImplementation 'org.spockframework:spock-spring:2.3-groovy-3.0'
}
Groovyのバージョン確認
% ./gradlew -v
Welcome to Gradle 8.6!
------------------------------------------------------------
Gradle 8.6
------------------------------------------------------------
Kotlin: 1.9.20
Groovy: 3.0.17
JVM: 22 (Oracle Corporation 22+36-2370)
spock-subjects-collaborators-extension
これも入れる。全く話に出ていなかったが、テスト対象をnewするのは面倒なためいい感じにインジェクションして欲しい。このライブラリをいれて@Subject
アノテーションをつけておけばいい感じにインジェクションしてくれる。コンストラクタ周りをテスト毎に基準しなくて良くなるため導入した。
class UserNameRepositoryJdbcImplSpec extends Specification {
JdbcTemplate jdbcTemplate = Mock(JdbcTemplate)
@Subject
UserNameRepositoryJdbcImpl sut
同様にbuild.gradleのdependienciesに追加するれば使える。ドキュメントはこれ。インジェクション用なので導入時のバージョンぐらいしか見てないかも。
dependencies {
...省略...
// テスト用の軽量 DI
testImplementation 'com.blogspot.toomuchcoding:spock-subjects-collaborators-extension:2.0.1-groovy-3.0'
}
3. テスト対象
DBから全ユーザを抜き出しUserNameへマッピングしたドメインオブジェクトを返してくれるレポジトリ層のテストを書いてみる。レポジトリ層は抜き出したユーザ情報を加工せず(しかもオブジェクトへのマッピングもJDBCがやってくれる)そのまま返却するメソッドである。 単体テスト(関心ごとの)外で実行されるDBへ接続・クエリを叩きユーザ情報を取得・情報をオブジェクトへマッピングはモックで実装し、実行された体でテストを書く。そのため特にテストとして書くことが無い。。
マッピング先のDTO
public record UserName (String FirstName, String LastName) {
public static UserName of (String firstName, String lastName) {
return new UserName(firstName, lastName);
}
}
Interfaceとレポジトリ層
public interface UserNameRepository {
List<UserName> findAll();
}
@Repository
public class UserNameRepositoryJdbcImpl implements UserNameRepository {
@Autowired
JdbcTemplate jdbcTemplate;
@Override
public List<UserName> findAll() {
return jdbcTemplate.query(
"SELECT * FROM USER_NAME",
new DataClassRowMapper<>(UserName.class)
);
}
}
4. テスト作成
Mock(クラス名)
でモック対象を定義。then節でモックが呼び出される回数の検証と呼ばれた場合返す値を定義、つまりMockを作る。
import com.blogspot.toomuchcoding.spock.subjcollabs.Subject
import com.example.springjdbc.domain.UserName
import org.springframework.jdbc.core.JdbcTemplate
import spock.lang.Specification
class UserNameRepositoryJdbcImplSpec extends Specification {
// Mockの宣言
JdbcTemplate jdbcTemplate = Mock(JdbcTemplate)
@Subject
UserNameRepositoryJdbcImpl sut
def "call findAll method"() {
given:
def userList = [
new UserName("FirstName", "LastName"),
new UserName("FirstName", "LastName")
]
when:
def actual = sut.findAll()
then:
// モックが呼ばれる回数と呼ばれた場合、返す値を定義
// 今回は1回呼ばれるとgivenで定義したuserListを返す
1 * jdbcTemplate.query(*_) >> userList
actual.size() == 2
actual == userList
}
}