はじめに
Spring Boot + MyBatisで、APIサーバを作るというプロジェクトに参画することになったので、サーバサイドの単体テストを整理しておこうと思います。
そもそも、なぜ単体テストを整理するのか?ということですが過去経験上、開始時にこの整理(方針決め)をきちんとできていないと、どうしても環境に依存したテストクラスが生成され、別の人が実行すると動作しないということが、起きるということから来ています。
また、MyBatis自体も初めて触るので(これまではHibernate->JPAというのが多かった)、そこもキャッチアップしつつ、単体テストを組み立てていきます。なお、MyBatisの細かい解説は世の中にあふれているので、ここでは割愛します。
環境
Windows 11
Eclipse
Java21
Gradle
Spring Boot 3.3.4
MyBatis 3.0.3
H2Database
Lombok
MyBatisって
データベースを操作するための永続化フレームワークです。
JPAとは違い、直接SQLを書かなければならないため、SQLの知識が必要であったりDBMSを意識した実装(設定)が必要になります。一方で複雑なSQLも実行できるという利点があります。
どうやってSQLを記載するか
SQLの記載方法は3つ
・アノテーション
・XMLファイル
・SQLプロバイダベース(ここでは触れない)
です。(他にも、MyBatis Dynamic SQLってのもあるみたい)
@Select("SELECT * FROM user")
List<User> findAll();
アノテーションで書くと、上のような感じになりますが、長いSQLや項目が多いInsert文など可読性が下がります。
当然保守性も下がるため、今回はXMLファイル方式を採用することにします。
XMLファイルについては後述します。
XMLファイルの作成粒度
XMLファイルをプロジェクトに1ファイルとするか、複数ファイルに分割するか。
規模が小さいプロジェクトであれば、1ファイルでも良いでしょう。
そこそこの規模になるので、Repository(MyBatisでいうMapper)単位でXMLファイルを分割しようと思います。
単体テストの方針
以下のような方針でテストの実行方法を検討することにします。
具体的なテストケース、検証内容はここでは触れません。
・データベースはH2Database(MySQLモード)で実施する
・テストケースごとに、テストデータを投入する
・テスト実行都度、データベースをロールバックする(実行順に依存せず、何度でも実行できるように)
・DBの登録等、更新後はその更新後のデータを検証する
やってみる
プロジェクトの作成
開発ツールはなんでもOKです。
Spring Initializrを使ってプロジェクトを作ってください。
生成されたGradleの設定ファイルは以下の通りです。
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
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'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
SpringBootの設定
まずは,Spring Bootの設定ファイルです。
テスト用に使うので、src/main/resourcesに配置します。
spring:
datasource:
url: jdbc:h2:mem:testdb;MODE=MySQL
driver-class-name: org.h2.Driver
username: sa
password:
mybatis:
mapper-locations: classpath*:mappers/**/*.xml
configuration:
map-underscore-to-camel-case: true
H2の接続設定と、Mybatisの設定を記載します。
スキーマの作成
テスト用に仕様するスキーマを作成していきます。
src/main/resourcesに配置しておけば、SpringBootが自動的にテーブル作成してくれます。
CREATE TABLE user_info (
id INT PRIMARY KEY,
username VARCHAR(255) NOT NULL
);
Entityクラスの作成
MyBatisがDBからデータを取得する際にマッピングするための
Entityクラスを作ります。
package com.example.demo.entity;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class UserInfo {
private int id;
private String username;
}
Mapperクラスの作成(Repositoryです。名前はどちらでもOK)
MyBatisが、オブジェクトとSQLをマッピングするためのインタフェースです
package com.example.demo.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.example.demo.entity.UserInfo;
@Mapper
public interface UserInfoMapper {
// ユーザー情報を全て取得
List<UserInfo> findAll();
// IDでユーザー情報を取得
UserInfo findById(@Param("id") int id);
// ユーザー情報を追加
void insert(UserInfo userInfo);
// ユーザー情報を更新
void update(UserInfo userInfo);
// ユーザー情報を削除
void delete(@Param("id") int id);
}
@Mapperアノテーションで、MyBatisにこのクラスがMapperであることをお知らせします。
<?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.example.demo.mapper.UserInfoMapper">
<select id="findAll" resultType="com.example.demo.entity.UserInfo">
SELECT * FROM user_info
</select>
<select id="findById" resultType="com.example.demo.entity.UserInfo">
SELECT * FROM user_info WHERE id = #{id}
</select>
<insert id="insert" parameterType="com.example.demo.entity.UserInfo">
INSERT INTO user_info (id, username)
VALUES (#{id}, #{username})
</insert>
<update id="update" parameterType="com.example.demo.entity.UserInfo">
UPDATE user_info
SET username = #{username}
WHERE id = #{id}
</update>
<delete id="delete" parameterType="int">
DELETE FROM user_info WHERE id = #{id}
</delete>
</mapper>
ポイントは、
・namespaceをクラス名と一致させること
・idをメソッド名と一致させること
です。
それ以外の細かいところは、いろいろ詳細に解説されているサイトを参考にしてください。
テストクラスを作成
package com.example.demo.mapper;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.test.context.jdbc.Sql;
import com.example.demo.entity.UserInfo;
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // デフォルトのテストDBを無効化
public class UserInfoMapperTest {
@Autowired
private UserInfoMapper mapper;
@Test
@Sql(scripts = "./test-userinfo.sql")
void testFindAll_01() {
List<UserInfo> results = mapper.findAll();
assertThat(results).hasSize(3);
}
@Test
void testFindAll_0件の場合() {
List<UserInfo> results = mapper.findAll();
assertThat(results).hasSize(0);
}
@Test
@Sql(scripts = "./test-userinfo.sql")
void testFindById_該当あり() {
UserInfo result = mapper.findById(3);
assertThat(result).isNotNull();
assertThat(result.getUsername()).isEqualTo("鈴木次郎");
}
@Test
@Sql(scripts = "./test-userinfo.sql")
void testFindById_該当なし() {
UserInfo result = mapper.findById(5);
assertThat(result).isNull();
}
@Test
@Sql(scripts = "./test-userinfo.sql")
void testInsert() {
UserInfo userInfo = new UserInfo();
userInfo.setId(4);
userInfo.setUsername("テスト三郎");
mapper.insert(userInfo);
List<UserInfo> results = mapper.findAll();
assertThat(results).hasSize(4);
}
}
@MybatisTest
Spring Boot において MyBatis を使用した単体テストを実行するためのアノテーションです。
これにより、面倒な作業(例えばトランザクション制御など)を自動的に行ってくれます。
@AutoConfigureTestDatabase
デフォルトでは、Spring Boot のテスト時に埋め込みデータベースが起動されますが、
その起動を抑止しています(同じH2ですがMySQLモードで起動したかったので)
@Sql
これにより指定したSQLをテスト前に実行します。検索やUpdateなど事前にデータが必要なケースで使用します。
INSERT INTO user_info (id,username) VALUES (3,'鈴木次郎');
INSERT INTO user_info (id,username) VALUES (1,'山田太郎');
INSERT INTO user_info (id,username) VALUES (2,'佐藤花子');
最後に
ここまで調べながらの整理しましたが、2時間程度で出来ました。
今回は、DBアクセス層について書きましたが、
Controller->Service->Repository
のような構成になるので、Controller、Serivceも順次整理していきます。
また、SQLプロバイダベースのMapperを使用すれば、XMLファイルとか作らなくてよくて、より良い感じがしています。今度実際にやってみようと思います。
なお、Insert等、更新後の検証にはAssertJ-DBを使用したかったのですがトランザクションの問題でうまく検証できずに断念しています。誰か教えてください。
※MyBatisと、AssertJで違うコネクションを使ってしまう影響だと思うのですが・・・