N-LAB.

JUnit 5 × MyBatis × DBUnitでRepositoryのユニットテストを実装する

投稿日:2024/10/14

目標

  • Spring Bootプロジェクトで実装されているRepositoryの単体テスト(UT)の実装をします
  • ここではORMにはMyBatis、Javaのバージョンは21を使用しています

※JPAを使用している場合は適宜Repositoryの実装を読み替えてください。

  • 本記事ではデータベースにはH2 Databaseを使用しています。

※MySQLなど他のデータベースを使用している場合は適宜実装を読み替えてください。

条件

  • Repositoryではデータベースからデータを検索するメソッドが実装されていること


目次

  1. JUnit5とDBUnitの導入
  2. データローダーの実装
  3. 単体テストコード(UT)の実装


JUnit5とDBUnitの導入

  • build.gradleに以下を追加します。mavenを利用している場合は適宜読み替えてください。
dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.2'
    testImplementation group: 'org.dbunit', name: 'dbunit', version: '2.+'
    testImplementation group: 'com.github.springtestdbunit', name: 'spring-test-dbunit', version: '1.3.0'
    runtimeOnly 'com.h2database:h2'
    testImplementation 'com.h2database:h2'
}
test {
    useJUnitPlatform()
}

※データベースにH2 Databaseを使用していない場合は以下の追加は不要です。
・runtimeOnly 'com.h2database:h2'
・testImplementation 'com.h2database:h2'

データローダーの実装

  • DBUnitを利用してテスト用の初期データ登録を行います
  • xmlに記載されたデータを読み込みデータ登録を行います
  • ここで実装したデータローダーを各単体テストケースで実行することで、xmlに記載したデータを検索対象のデータとして扱うことができます

XmlDataLoader.java

import com.github.springtestdbunit.dataset.AbstractDataSetLoader;
import java.io.InputStream;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.springframework.core.io.Resource;

public class XmlDataLoader extends AbstractDataSetLoader {
  @Override
  protected IDataSet createDataSet(Resource resource) throws Exception {
    FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();
    builder.setColumnSensing(true);
    try (InputStream stream = resource.getInputStream()) {
      return builder.build(stream);
    }
  }
}

test/resources/dataset/user_find_by_id.xml

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
  <user id="1" email="test1@test.com" />
  <user id="2" email="test2@test.com" password="test2" />
</dataset>
  • 1つ目のレコード(user id="1"の行)に記載したカラムでテーブル定義が決まるので、passwordを省略した場合はテーブルの定義からpasswordが削除(idとemailだけ定義される)されます。そのため上記の例では2つ目のレコード(user id="2"の行)に記載したpasswordの値(test2)は取得できずnullになります。


  • この問題を解決するためにデータローダーに以下を設定しています。
builder.setColumnSensing(true);


単体テストコード(UT)の実装

  • ここでは以下のファイルが実装されていることを想定しています

resources/schema.sql

DROP TABLE IF EXISTS user;

CREATE TABLE user (
  id INTEGER PRIMARY KEY AUTO_INCREMENT,
  email VARCHAR(255) NOT NULL,
  password VARCHAR(255)
);

User.java

@Builder
@Getter
@Setter
@ToString
public class User {
  private Long id;
  private String email;
  private String password;
}

UserRepository.java

@Repository
public class UserRepository {
  private final UserMapper userMapper;
  public UserRepository(UserMapper userMapper) {
    this.userMapper = userMapper;
  }

  public Optional<User> findById(Long id) throws Exception {
    return userMapper.findById(id);
  }
}

UserMapper.java

@Mapper
public interface UserMapper {
  Optional<User> findById(Long id);
}

resources/mapper/UserMapper.xml

<?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="foo.bar.UserMapper">
  <select id="findById" resultType="foo.bar.User">
    select * from user where id = #{id}
  </select>
</mapper>

application.yml

spring:
  datasource:
    url: jdbc:h2:mem:test;NON_KEYWORDS=USER;DATABASE_TO_UPPER=false;
    username: root
    password: root
    driver-class-name: org.h2.Driver
  sql:
    init:
      mode: always
      encoding: UTF-8
  h2:
    console:
      enabled: true
      path: /h2-console
mybatis:
  configuration:
    map-underscore-to-camel-case: true
  mapper-locations: classpath*:/mapper/*.xml
  type-aliases-package: foo.bar


1. UserRepositoryTest.javaを新規作成し、以下の内容を実装します

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import com.github.springtestdbunit.TransactionDbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.transaction.annotation.Transactional;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestExecutionListeners({
    DependencyInjectionTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class,
    TransactionDbUnitTestExecutionListener.class
})
@Transactional
@DbUnitConfiguration(dataSetLoader = XmlDataLoader.class)
public class UserRepositoryTest {
  @Autowired
  private UserRepository userRepository;

  @Test
  @DisplayName("指定したIDのユーザー情報が取得できること")
  @DatabaseSetup("/dataset/user_find_by_id.xml")
  public void findById() throws Exception {
    // Arrange
    User expectedUser = User.builder()
        .id(2L)
        .email("test2@test.com")
        .password("test2")
        .build();

    // Act
    User actualUser = userRepository.findById(expectedUser.getId()).orElse(null);

    // Assert
    assertNotNull(actualUser);
    assertEquals(expectedUser.getId(), actualUser.getId());
    assertEquals(expectedUser.getEmail(), actualUser.getEmail());
    assertEquals(expectedUser.getPassword(), actualUser.getPassword());
  }
}
  • @DbUnitConfigurationアノテーションのdataSetLoaderに前項で作成したデータローダーを指定します。各テストケースに@DatabaseSetupアノテーションを記述し、初期データとして利用したいxmlを指定します。


  • 上記アノテーションを使用するために@TestExecutionListenersに以下を指定します。

TransactionDbUnitTestExecutionListener.class

  • xmlはtest/resources配下にdatasetディレクトリを作成してそちらに配置しています。


  • @TestExecutionListenersに以下を指定することでテスト実行時にSpringのDIコンテナのライフサイクル管理機能を有効にしたうえでDIを利用できます。

DependencyInjectionTestExecutionListener.class
DirtiesContextTestExecutionListener.class


以上で全ての手順は完了になります