【Spring Boot】JUnit×DBUnitでControllerクラス, Serviceクラス, Repositoryクラスをテストする

Spring BootのControllerクラス、ServiceクラスRepositoryクラスをJUnitとDBUnitを用いてテストするサンプルコードです。


## 環境
- OS：Windows10
- IDE：Eclipse2020-03
- Java：8
- MySQL：5.7
- Spring Boot：2.3.0
- JUnit：5
- DBUnit：2.7.0

## 階層構造
```
spring_boot_test_item
     |
    src/main/java
     |----jp.co.test_item
     |               |----app
     |               |     |---- controller
     |               |     |---- response
     |               |
     |               |----domain
     |                      |---- service
     |                      |---- repository
     |                      |---- model
     |
    src/main/resources
     |----application.yml
     |
     |
    src/main/test(※)

```

※src/main/test配下は、基本的に、src/main/java配下と同構成になるため割愛

## テーブルの作成
```sql:
CREATE TABLE `items` (
  `item_id` int(11) NOT NULL AUTO_INCREMENT,
  `item_name` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `item_explanation` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL,
  `category_id` int(11) NOT NULL DEFAULT '1',
  PRIMARY KEY (`item_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
```
上記DDLを実行して、テーブルを作成します。

## application.yml

```yaml:application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://接続先名/DB名?serverTimezone=JST&nullCatalogMeansCurrent=true
    username: ユーザー名
    password: パスワード
  jpa:
    database: MYSQL
    hibernate.ddl-auto: update
    
  main:
    allow-bean-definition-overriding: true

```
MySQLへの接続情報を記載します。
ポイントは以下2つ。

1. 「nullCatalogMeansCurrent=true」とすることで、同DBインスタンスの別データベースに同名のテーブルが存在する場合のエラーを回避
2. 「allow-bean-definition-overriding: true」（上の「main:」も忘れずに）とすることで、beanオーバーライドを許可
<br>してあげましょう。


## build.gradleの設定

```gradle:build.gradle
plugins {
	id 'org.springframework.boot' version '2.3.0.RELEASE'
	id 'io.spring.dependency-management' version '1.0.9.RELEASE'
	id 'java'
}

group = 'jp.co.test_item'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

dependencyManagement {
    imports {
        mavenBom "org.junit:junit-bom:5.6.2"//BOMのインポート
    }
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.4'
	runtimeOnly 'mysql:mysql-connector-java'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
	testImplementation 'org.junit.jupiter:junit-jupiter'//JUnit5に対応
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'//IDEなどのサポート用
    testCompile group: 'com.github.springtestdbunit', name: 'spring-test-dbunit', version: '1.3.0' // SpringでDBUnitを用いる際に必要
    testCompile group: 'org.dbunit', name: 'dbunit', version: '2.7.0' // DBUnitのAPI
}

test {
	useJUnitPlatform()
}

```
コメントの箇所が今回のポイントです。

## テスト対象の実装
テスト対象となるクラス群を実装します。

### 1. Controllerクラス

```java:ItemController.java
package jp.co.spring_boot_test_item.app.controller;

import java.sql.SQLException;
import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import jp.co.spring_boot_test_item.app.request.ItemPostRequest;
import jp.co.spring_boot_test_item.app.response.ItemResponse;
import jp.co.spring_boot_test_item.app.response.ItemsResponse;
import jp.co.spring_boot_test_item.domain.model.Item;
import jp.co.spring_boot_test_item.domain.service.ItemService;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class ItemController {

	private final ItemService itemService;

	@GetMapping("/item/{itemId}")
	public ResponseEntity<ItemResponse> getItemsByGet(@PathVariable int itemId){
		Item item = itemService.findByItemId(itemId);
		ItemResponse itemResponse = ItemResponse.builder().item(item).build();
		return new ResponseEntity<>(itemResponse, HttpStatus.OK);
	}

	@PostMapping("/items")
	public ResponseEntity<ItemsResponse> getItemsByPost(@Validated @RequestBody ItemPostRequest request){
		List<Item> items = itemService.findByCategoryId(request.getCategoryId());
		ItemsResponse itemResponse = ItemsResponse.builder().items(items).build();
		return new ResponseEntity<>(itemResponse, HttpStatus.OK);
	}

}

```
@GetMapping、@PostMappingをつけた2つのメソッドが今回のテスト対象となります。

### 2. Requestクラス
```java:ItemPostRequest.java
package jp.co.spring_boot_test_item.app.request;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ItemPostRequest {

	private Integer categoryId;

}

```
上のControllerクラスの@PostMappingの付いたメソッドのRequestBodyの内容取得に使用します。

### 3. Responseクラス
```java:ItemResponse.java
package jp.co.spring_boot_test_item.app.response;

import java.util.List;

import jp.co.spring_boot_test_item.domain.model.Item;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class ItemResponse {

	// Item返却用
	private Item item;

	// List<Item>返却用
	private List<Item> items;

}


```
Controllerクラスでのレスポンス時に形を整えるために使用します。


### 4. Serviceクラス
```java:ItemService.java
package jp.co.spring_boot_test_item.domain.service;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import jp.co.spring_boot_test_item.domain.model.Item;
import jp.co.spring_boot_test_item.domain.repository.ItemRepository;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class ItemService {

	private final ItemRepository itemRepository;

	@Transactional(readOnly = true)
	public Item findByItemId(int itemId) {
		return itemRepository.findByItemId(itemId);
	}

	@Transactional(readOnly = true)
	public List<Item> findByCategoryId(int categoryId) {
		return itemRepository.findByCategoryId(categoryId);
	}

}
```
Serviceクラスです。
データの読み取り操作のみのため、@Transactional(readOnly=true)としておきます（デフォルトはfalse）。
こうすることで、データ操作処理（登録、更新、削除）処理をしようとしても処理は行われず、エラーも発生しなくなります。
（そもそもそんな記述をしていませんが、、）

### 5. ItemRepository.java
```java:ItemRepository.java
package jp.co.spring_boot_test_item.domain.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import jp.co.test_item.domain.model.Item;

@Repository
public interface ItemRepository extends JpaRepository<Item, Integer>{
	
	// 「select * from items where item_id = itemId（引数としてわたってくる変数）;」と同義
	public Item findByItemId(int itemId);
	
	// 「select * from items where category_id = categoryId（引数としてわたってくる変数）;」と同義
	public List<Item> findByCategoryId(int categoryId);

}
```
Repositoryクラスです。
Spring Data JPAの命名ルールに従ってメソッドを定義することで、クエリを自動生成してくれます。

### 6. Entityクラス
```java:Item.java
package jp.co.spring_boot_test_item.domain.model;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQuery;
import javax.persistence.Table;


/**
 * The persistent class for the items database table.
 *
 */
@Entity
@Table(name="items")
@NamedQuery(name="Item.findAll", query="SELECT i FROM Item i")
public class Item implements Serializable {
	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="item_id")
	private int itemId;

	@Column(name="category_id")
	private int categoryId;

	@Column(name="item_explanation")
	private String itemExplanation;

	@Column(name="item_name")
	private String itemName;

	public Item() {
	}

	public int getItemId() {
		return this.itemId;
	}

	public void setItemId(int itemId) {
		this.itemId = itemId;
	}

	public int getCategoryId() {
		return this.categoryId;
	}

	public void setCategoryId(int categoryId) {
		this.categoryId = categoryId;
	}

	public String getItemExplanation() {
		return this.itemExplanation;
	}

	public void setItemExplanation(String itemExplanation) {
		this.itemExplanation = itemExplanation;
	}

	public String getItemName() {
		return this.itemName;
	}

	public void setItemName(String itemName) {
		this.itemName = itemName;
	}
}
```
Entityクラスです。
今回は、DBのテーブル定義をもとにHibernate ORMを用いて生成しました。

## テストクラスの実装
### 1. ItemControllerTest.java
```java:ItemControllerTest.java
package jp.co.spring_boot_test_item.app.controller;

import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.RestController;

import jp.co.spring_boot_test_item.domain.model.Item;
import jp.co.spring_boot_test_item.domain.service.ItemService;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class ItemControllerTest {

	MockMvc mockMvc;

	@Mock // モックオブジェクトとして使用することを宣言
	private ItemService itemService;

	@InjectMocks // モックオブジェクトの注入
	private ItemController itemController;

	@BeforeEach // テストメソッド（@Testをつけたメソッド）実行前に都度実施
	public void initmocks() {
		MockitoAnnotations.initMocks(this); // アノテーションの有効化
		mockMvc = MockMvcBuilders.standaloneSetup(itemController).build(); // MockMvcのセットアップ
	}

	@Test
	public void test_getItemsByGet() throws Exception {

		when(itemService.findByItemId(1)).thenReturn(getItemIdOfItemId1()); // itemService.findByItemId(1)実行時にgetItemIdOfItemId1()の結果が返ることを定義
		this.mockMvc.perform(get("/item/{itemId}", 1)) // @GetMapping("/item/{itemId}")のメソッドの実行と結果確認
				.andExpect(status().isOk()) // 以下、結果確認
				.andExpect(jsonPath("$.item.itemId").value(1))
				.andExpect(jsonPath("$.item.itemName").value("ふしぎなアメ"))
				.andExpect(jsonPath("$.item.itemExplanation").value("レベル1アップします。"))
				.andExpect(jsonPath("$.item.categoryId").value(1));
	}

	@Test
	public void test_getItemsByPost() throws Exception {
		when(itemService.findByCategoryId(1)).thenReturn(getItemIdOfCategory1()); // findByCategoryId(1)実行時にgetItemIdOfCategory1()の結果が返ることを定義
		this.mockMvc.perform(post("/items").contentType(MediaType.APPLICATION_JSON).content("{\"categoryId\":\"1\"}")) // RequestBodyの要素の型と値を設定
				.andExpect(jsonPath("$.items", hasSize(2))) // 以下、結果確認
				.andExpect(status().isOk())
				.andExpect(jsonPath("$.items[0].itemId").value(1))
				.andExpect(jsonPath("$.items[0].itemName").value("ふしぎなアメ"))
				.andExpect(jsonPath("$.items[0].itemExplanation").value("レベル1アップします。"))
				.andExpect(jsonPath("$.items[0].categoryId").value(1))
				.andExpect(jsonPath("$.items[1].itemId").value(2))
				.andExpect(jsonPath("$.items[1].itemName").value("オボンのみ"))
				.andExpect(jsonPath("$.items[1].itemExplanation").value("HPをすこしだけかいふくする。"))
				.andExpect(jsonPath("$.items[1].categoryId").value(1));
	}

	public Item getItemIdOfItemId1() {

		Item item = new Item();

		item.setItemId(1);
		item.setItemName("ふしぎなアメ");
		item.setItemExplanation("レベル1アップします。");
		item.setCategoryId(1);

		return item;

	}

	public List<Item> getItemIdOfCategory1() {

		List<Item> items = new ArrayList<>();

		Item item1 = new Item();
		item1.setItemId(1);
		item1.setItemName("ふしぎなアメ");
		item1.setItemExplanation("レベル1アップします。");
		item1.setCategoryId(1);

		Item item2 = new Item();
		item2.setItemId(2);
		item2.setItemName("オボンのみ");
		item2.setItemExplanation("HPをすこしだけかいふくする。");
		item2.setCategoryId(1);

		items.add(item1);
		items.add(item2);

		return items;

	}

}

```

Controllerクラスのテストです。
@RestControllerなので、JSON形式でやりとりされるのがポイント。
テストコード自体も、合わせた内容にします。

### 2. ItemServiceTest.java

```java:ItemServiceTest.java
package jp.co.spring_boot_test_item.domain.service;

import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import jp.co.spring_boot_test_item.domain.model.Item;
import jp.co.spring_boot_test_item.domain.repository.ItemRepository;

public class ItemServiceTest {

	@Mock	// モックオブジェクトとして使用することを宣言
	private ItemRepository itemRepository;

	@InjectMocks	// モックオブジェクトの注入
	private ItemService itemService;

	@BeforeEach	// テストメソッド（@Testをつけたメソッド）実行前に都度実施
	public void initmocks() {
		MockitoAnnotations.initMocks(this);
	}

	@Test
	public void test_findByItemId() {

		when(itemRepository.findByItemId(100)).thenReturn(getItemId100()); // itemRepository.findByItemId(100)実行時にgetItemId100()の結果が返ることを定義
		Item item = itemRepository.findByItemId(100);

		// 以下、結果確認
		assertThat(item.getItemId(), is(100));
		assertThat(item.getItemName(), is("超ふしぎなアメ"));
		assertThat(item.getItemExplanation(), is("レベル100アップします。"));
		assertThat(item.getCategoryId(), is(1));
	}

	@Test
	public void test_findByCategoryId() {

		when(itemRepository.findByCategoryId(10)).thenReturn(getItemIdOfCategory10());	// itemRepository.findByCategoryId(10)実行時にgetItemIdOfCategory10()の結果が返ることを定義
		List<Item> items = itemRepository.findByCategoryId(10);

		// 以下、結果確認
		assertThat(items.get(0).getItemId(), is(1));
		assertThat(items.get(0).getItemName(), is("ふしぎなアメ"));
		assertThat(items.get(0).getItemExplanation(), is("レベル1アップします。"));
		assertThat(items.get(0).getCategoryId(), is(1));

		assertThat(items.get(1).getItemId(), is(2));
		assertThat(items.get(1).getItemName(), is("オボンのみ"));
		assertThat(items.get(1).getItemExplanation(), is("HPをすこしだけかいふくする。"));
		assertThat(items.get(1).getCategoryId(), is(1));

	}

	public Item getItemId100() {

		Item item = new Item();
		item.setItemId(100);
		item.setItemName("超ふしぎなアメ");
		item.setItemExplanation("レベル100アップします。");
		item.setCategoryId(1);

		return item;


	}

	public List<Item> getItemIdOfCategory10(){


		List<Item> items = new ArrayList<>();

		Item item1 = new Item();
		item1.setItemId(1);
		item1.setItemName("ふしぎなアメ");
		item1.setItemExplanation("レベル1アップします。");
		item1.setCategoryId(1);

		Item item2 = new Item();
		item2.setItemId(2);
		item2.setItemName("オボンのみ");
		item2.setItemExplanation("HPをすこしだけかいふくする。");
		item2.setCategoryId(1);

		items.add(item1);
		items.add(item2);

		return items;


	}

}

```
Controllerクラスのテストでも用いていますが、Mockitoを使用して下階層のRepositoryクラスの返却値を定義するのがポイント。

### 3. ItemRepositoryTest.java
```java:ItemRepositoryTest.java
package jp.co.spring_boot_test_item.domain.repository;

import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.excel.XlsDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.transaction.AfterTransaction;
import org.springframework.test.context.transaction.BeforeTransaction;
import org.springframework.transaction.annotation.Transactional;

import jp.co.spring_boot_test_item.domain.model.Item;

@SpringBootTest
@Transactional
public class ItemRepositoryTest {


	@Autowired
	JdbcTemplate jdbctemplate;

	@Autowired
	ItemRepository itemRepository;

	String item_id_str = null;

	Integer item_id_num = null;

	private File file = null;

	private FileOutputStream out;

    private FileInputStream in;

	File tempDir = new File("src/test/java/jp/co/spring_boot_test_item/domain/repository");

	@BeforeTransaction // 各@Transactional実行前に都度実施
	public void initdb() throws Exception{
		Connection connection = jdbctemplate.getDataSource().getConnection();
		IDatabaseConnection dbconnection = new DatabaseConnection(connection);
		try {

			// AUTO_INCREMENTの現在値を取得
			String sql = "SHOW TABLE STATUS where  name = 'items'";
			PreparedStatement statement = connection.prepareStatement(sql);
			ResultSet result = statement.executeQuery();
			while (result.next()) {
				item_id_str = result.getString("Auto_increment");
			}

			// itemsテーブルのバックアップを取得
			QueryDataSet partialDataSet = new QueryDataSet(dbconnection);
			partialDataSet.addTable("items");
			file=File.createTempFile("items",".xml", tempDir);
			out = new FileOutputStream(file);
			FlatXmlDataSet.write(partialDataSet, out);
			out.flush();
			out.close();
			
			// DBの値を削除し、テストデータを投入する（ExcelファイルのパスはItemRepositoryTest.javaと同階層）
			IDataSet dataset = new XlsDataSet(new File("src/test/java/jp/co/spring_boot_test_item/domain/repository/ItemRepositoryTest.xlsx"));
			DatabaseOperation.CLEAN_INSERT.execute(dbconnection, dataset);		

		}catch(Exception e) {
			e.printStackTrace();
		}finally {

			connection.close();
			dbconnection.close();

		}
	}


	@AfterTransaction	// 各@Transactional実行後に都度実施
	public void teardown() throws Exception{
		Connection connection = jdbctemplate.getDataSource().getConnection();
		IDatabaseConnection dbconnection = new DatabaseConnection(connection);
		try {

			// テストデータを削除し、バックアップファイルの内容を戻す
			in = new FileInputStream(file);
			IDataSet dataSet= new FlatXmlDataSetBuilder().build(in);
			DatabaseOperation.CLEAN_INSERT.execute(dbconnection,dataSet);

			// AUTO_INCREMENTをテスト実行前の値に戻す
			String sql = new StringBuilder("ALTER TABLE items AUTO_INCREMENT=").append(item_id_str).toString();
			PreparedStatement statement = connection.prepareStatement(sql);
			statement.executeUpdate();

		}catch(Exception e) {
			e.printStackTrace();
		}finally {
			connection.close();
			dbconnection.close();
			file.deleteOnExit();	// DBバックアップファイルの削除
		}
	}

	@Test
	public void test_findByItemId() {

		Item expectedItem = new Item();

		Item actualItem = itemRepository.findByItemId(1);

		assertThat(actualItem.getItemId(), is(1));
		assertThat(actualItem.getItemName(), is("ふしぎなアメ"));
		assertThat(actualItem.getItemExplanation(), is("レベル1アップします。"));
		assertThat(actualItem.getCategoryId(), is(1));

	}

	@Test
	public void test_findByCategoryId() {

		Item item1 = new Item();
		item1.setItemId(1);
		item1.setItemName("ふしぎなアメ");
		;
		item1.setItemExplanation("レベル1アップします。");
		item1.setCategoryId(1);

		Item item2 = new Item();
		item2.setItemId(4);
		item2.setItemName("オボンのみ");
		item2.setItemExplanation("HPをすこしだけかいふくする。");
		item2.setCategoryId(1);

		List<Item> expectedItems = new ArrayList<Item>();
		expectedItems.add(item1);
		expectedItems.add(item2);

		List<Item> actualItems = itemRepository.findByCategoryId(1);

		assertThat(actualItems, hasSize(2));

		for (int i = 0; i < actualItems.size(); i++) {
			for (int j = 0; j < expectedItems.size(); j++) {
				if (actualItems.get(i).getItemId() == expectedItems.get(j).getItemId()) {
					assertThat(actualItems.get(i).getItemName(), is(expectedItems.get(j).getItemName()));
					assertThat(actualItems.get(i).getItemExplanation(), is(expectedItems.get(j).getItemExplanation()));
					assertThat(actualItems.get(i).getCategoryId(), is(expectedItems.get(j).getCategoryId()));
					break;
				}
			}
		}
	}

	@Test
	public void test_save()  throws Exception{

		// 更新
		Item Item1 = new Item();
		Item1.setItemId(1);
		Item1.setItemName("ふつうのアメ");
		Item1.setItemExplanation("20kcal摂取できます。");
		Item1.setCategoryId(3);

		itemRepository.save(Item1);

		// 追加
		Item item2 = new Item();
		item2.setItemName("ペロペロキャンディー");
		item2.setItemExplanation("子どもにあげると喜ばれます（たぶん）。");
		item2.setCategoryId(3);

		itemRepository.save(item2);


		 // DBの値の確認（更新分）
		Item dbItem1 = itemRepository.findByItemId(1);

		assertThat(dbItem1.getItemId(), is(1));
		assertThat(dbItem1.getItemName(), is("ふつうのアメ"));
		assertThat(dbItem1.getItemExplanation(), is("20kcal摂取できます。"));
		assertThat(dbItem1.getCategoryId(), is(3));

		// DBの値の確認（追加分）
		item_id_num = Integer.valueOf(item_id_str);
		Item dbItem2 = itemRepository.findByItemId(item_id_num);

		assertThat(dbItem2.getItemId(), is(item_id_num));
		assertThat(dbItem2.getItemName(), is("ペロペロキャンディー"));
		assertThat(dbItem2.getItemExplanation(), is("子どもにあげると喜ばれます（たぶん）。"));
		assertThat(dbItem2.getCategoryId(), is(3));

	}
}

```
Repositoryクラスのテストです。
ポイントは以下の2つ。
1. @Mockを用いる代わりに、クラスの上に@SpringBootTestアノテーションを付与
DBUnitの機能により、テストメソッド実行前後に、テストデータとDBデータの入れ替えが行われます。
2. @Transactionalを用いてのテストなので、テスト実行前後に、@BeforeEach/@AfterEachでなく、@BeforeTransaction/@AfterTransactionを用いる（そうしないと特にCUD系の処理の時に、トランザクションロックがかかる可能性があります）

今回、上の階層では使っていませんが、挿入、更新（insert, update）系のコードも書き、テスト後にDBのAUTO_INCREMENTを実行前のものに戻す記述も入れてみました。

## ItemRepositoryTest.xlsx
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/356958/2979578d-a5d0-56b4-cf70-2265fd16a6a6.png)



Repositoryクラスのテスト用のデータです。
今回は、ItemRepositoryTest.javaと同階層に配置して使います。

ポイントは、
1. 1行目にカラム名を記載する
2. シート名はテーブル名にする（今回は「items」）
ことです。

## 補足事項
1. Controllerクラスには、画面遷移を伴う@Controllerではなく、WebAPIで用いられる、@RestControllerを用いています。
2. 今回は各クラスの書き方に重点を置いているので、例外やエラーのテストは省略しています。

## 本記事の更新履歴
1. 2020年6月13日：
コードをGitHub
[spring-boot-test-sample](https://github.com/CHI-3/spring-boot-test-sample)
に追加。
2. 2020年6月15日：Entityクラス、Serviceクラスのトランザクション制御に関する記述を追記

## 参考
### ■JUnit
#### JUnit全般
- [JUnit5 | junit.org](https://junit.org/junit5/)
- [JUnit5ユーザーガイド](https://oohira.github.io/junit5-doc-jp/user-guide/)

#### appication.ymlの設定
- [Spring Boot 1.5→2.1 に移行したときのお話 | Qiita](https://qiita.com/kohii/items/7fc2fd4f1d5a07568b36)
- [Spring Boot 2.1 でテスト時 @Bean を挿げ替えたかった | Qiita](https://qiita.com/yukihane/items/c296caedf173e8e345d4)

#### build.gradleの設定
- [Testing in Java & JVM projects | Gradle6.4.1](https://docs.gradle.org/current/userguide/java_testing.html#using_junit5)
- [Gradleプロジェクトでspring-boot-starter-testにJUnit5を導入するときの競合を解決する | Qiita](https://qiita.com/talesleaves/items/bf0d80d887bbc5c3635e)

#### アノテーション
- [JUnit 4からJUnit 5に移行する | Qiita](https://qiita.com/unhurried/items/2e2deca4ea97ae3684b0)
- [Eclipse を使って JUnit5 を導入したときのメモ | Qiita](https://qiita.com/naotawool/items/a8d77ff9d468fedab2c9)
- [テスト | Spring](https://spring.pleiades.io/spring-framework/docs/current/spring-framework-reference/testing.html)
- [springbootのテストについて-備忘録- | 忘れっぽいエンジニアの備忘録](https://k-sasaki.net/2017/12/164/)


#### アサーション
- [JUnit4とJUnit5のアサーション | DMCA](https://www.codeflow.site/ja/article/junit-assertions)
- [Class Assertions | junit.org](https://junit.org/junit5/docs/5.0.1/api/org/junit/jupiter/api/Assertions.html)
<br>→ JUnit5
- [Class Assert | junit.org](https://junit.org/junit4/javadoc/4.12/org/junit/Assert.html)
<br>→ JUnit4

#### テスト実装
- [10.2.2. レイヤごとのテスト実装 | TERASOLUNA](http://terasolunaorg.github.io/guideline/5.5.1.RELEASE/ja/UnitTest/ImplementsOfUnitTest/ImplementsOfTestByLayer.html)
- [Spring Bootでテストを書くときのやりかたまとめ | Qiita](https://qiita.com/mitsuya/items/be50dc329b4f3abe5ac5)
- [Springで外部APIをリクエストする場合のテスト | 壁にでも話してろ](https://www.yo1000.com/resttemplate-test/)
- [Testing your REST controllers and clients with Spring | DIMITR.IM](https://dimitr.im/testing-your-rest-controllers-and-clients-with-spring)
- [Spring Test – How to test a JSON Array in jsonPath | Mkyong.com](https://mkyong.com/spring-boot/spring-test-how-to-test-a-json-array-in-jsonpath/)

### ■Mockito
#### Mockito全般
- [mockito.org](https://site.mockito.org/)
- [【Java】Mockitoの飲み方（入門） | Orizuru](https://orizuru.io/blog/java/mockito-primer/)
- [Spring Bootでmockitoを使ってテストする | 株式会社CONFRAGE ITソリューション事業部](https://confrage.jp/spring-boot%E3%81%A7mockito%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%86%E3%82%B9%E3%83%88%E3%81%99%E3%82%8B/)

#### MockMvc
- [10.2.4.2. MockMvc | TERASOLUNA](https://terasolunaorg.github.io/guideline/5.4.1.RELEASE/ja/UnitTest/ImplementsOfUnitTest/UsageOfLibraryForTest.html#mockmvc)
- [Spring Boot MockMvc not working test with @PathVariable [duplicate] | stack overflow](https://stackoverflow.com/questions/45996088/spring-boot-mockmvc-not-working-test-with-pathvariable/45996346)

### ■DBUnit
#### DBUnit全般
- [DBUnit | TECHSCORE](https://www.techscore.com/tech/Java/Others/DBUnit/index/)
- [【超初心者向け】DBUnit超入門 | Qiita](https://qiita.com/tarosa0001/items/70a1efa9edac2d83ba1a)

### ■その他
#### Spring
- [Springの@Transactional(readOnly=true)で読み取り専用のトランザクションにする | 株式会社CONFRAGE ITソリューション事業部](https://confrage.jp/spring%E3%81%AEtransactionalreadonlytrue%E3%81%A7%E8%AA%AD%E3%81%BF%E5%8F%96%E3%82%8A%E5%B0%82%E7%94%A8%E3%81%AE%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B6%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AB/)