以前に作成したSpringBoot + MyBatis + Thymeleaf + MySQL で超簡潔なCRUD処理でテストコードを書いてみたので自分用のアウトプットとして投稿させていただきます。
誤りがございましたらご指摘いただけますと幸いですm(_ _)m
環境
macOS Big Sur
Java 11
Spring Boot 2.4.5
SpringToolSuite4
Maven
MySQL
MyBatis
テスト対象
①GETリクエストで正しくページが表示される(HTTPステータスコード200が返される)こと。
②GETリクエスト時に、Modelのattributeに文字列"hello"が渡されていること。
③GETリクエスト時に、select処理によりModelのattributeにデータベースのUserListが格納されていること。
④ユーザーのIDを引数とした1件のselect処理が成功していること(DBUnit利用)。
⑤データベース全体のselect処理が成功していること(DBUnit利用)。
⑥insert処理が成功していること(DBUnit利用)。
⑦update処理が成功していること(DBUnit利用)。
⑧delete処理が成功していること(DBUnit利用)。
ディレクトリ構成
ほとんどは前回に作成したユーザーリストCRUD処理のファイルです。
今回ポイントとなるファイルに★マークを付けています。
.
├src/main/java/com/example/demo/
│ ├controller / UController.java
│ ├model / User.java
│ ├repository / UMapper.java
│ ├service / UService.java
│ └MyBatisPracticeApplication.java
└ src/main/resources/
│ ├mapper / UMapper.xml
│ ├templates / users
│ │ ├change.html
│ │ ├details.html
│ │ ├list.html
│ │ ├register.html
│ │ └top.html
│ ├application.properties
│ ├data.sql
│ └schema.sql
│
├src/test/java/com/example/demo/dbtest
│ ├ ★ CsvDataSetLoader.java
│ ├ ★ DB_CRUD_Test.java
│ └ ★ UControllerTest.java
└src/main/test/resources/testData
├ ★ after-create-data
│ ├table-ordering.txt
│ └users.csv
├ ★ after-delete-data
│ ├table-ordering.txt
│ └users.csv
├ ★ after-update-data
│ ├table-ordering.txt
│ └users.csv
└ ★ init-data
├table-ordering.txt
└users.csv
各ソースコード
pom.xml(一部抜粋)
まずはDBUnitを使用するために、dependenciesタグ内に以下の記載を追加します。
<dependency>
<groupId>com.github.springtestdbunit</groupId>
<artifactId>spring-test-dbunit</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.7.0</version>
<scope>test</scope>
</dependency>
UController.java
前回の記事で記載していたControllerファイルです。
GetMapping("/list")でアクセスするselect全件表示のメソッド内に、テスト①用にModelのattributeに文字列を渡すコードを記載しています。
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.demo.model.User;
import com.example.demo.service.UService;
import java.util.List;
@Controller
@RequestMapping("/users")
public class UController {
@Autowired
private UService service;
//select全件表示
@GetMapping("/list")
public String getUserList(Model model) {
List<User> userList = service.getUserList();
model.addAttribute("users", userList);
model.addAttribute("message", "hello");//テスト①用
return "users/list";
}
//トップページ top.html表示
@GetMapping("")
public String top(Model model, @ModelAttribute User u) {
model.addAttribute("users", service.getUserList()) ;
return "users/top";
}
//top→[詳細]押下 select1件
@GetMapping("details/id={id}")
public String details(@PathVariable("id") int id, Model model) {
model.addAttribute("users", service.getUserOne(id));
return "users/details";
}
//top→[新規作成]押下 th:hrefにより生成されたURLをGETで表示
@GetMapping("/register")
public String registerUser(Model model, @ModelAttribute User u) {
model.addAttribute("users", u);
return "users/register";
}
//register.html内の <form method="post"> で↓へ飛ぶ
@PostMapping("/register")
public String create(@Validated @ModelAttribute User u, BindingResult bindingresult) {
if (bindingresult.hasErrors()) {
System.out.println("エラー発生!" + bindingresult);
return "users/register";
}
service.insertOne(u);
return "redirect:/users";
}
//top→[変更]押下時にchange.htmlを表示するGET
@GetMapping("change/id={id}")
public String change(@PathVariable("id") int id, Model model) {
model.addAttribute("users", service.getUserOne(id));
return "users/change";
}
@PostMapping("change/id={id}")
public String update(@ModelAttribute User u, Model model) {
service.updateOne(u.getId(), u.getName(), u.getAge());
return "redirect:/users";
}
//top→[削除]押下時
@PostMapping("delete/id={id}")
public String delete(@PathVariable String id, @ModelAttribute User u) {
service.deleteOne(u);
return "redirect:/users";
}
}
UControllerTest.java
ここからテストのコードに入ります。
まずはテスト対象①〜③のコードです。
package com.example.demo;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasProperty;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
@AutoConfigureMockMvc//MockMvcの利用
@SpringBootTest
class UControllerTest {
@Autowired
private MockMvc mockmvc;
//テスト①
@Test
void getUserList処理でhttpステータスコード200が返る() throws Exception {
this.mockmvc.perform(get("/users/list"))
.andDo(print())
.andExpect(status().isOk());
}
//テスト②
@Test
void getUserList処理でModelのmessageにhelloが渡される() throws Exception {
this.mockmvc.perform(get("/users/list"))
.andExpect(model().attribute("message", "hello"));
}
//テスト③
@Test
void getUserList処理でModelのusersへUserListが格納_レコードの一つはidとして1を持つ() throws Exception {
this.mockmvc.perform(get("/users/list"))
.andExpect(model().attribute("users", hasItem(hasProperty("id", is(1)))));
}
}
CsvDataSetLoader.java
DBUnitで検証できるデータの形式としてxml, excel, csvがあるようですが、
今回はcsvファイルを利用しています。
当クラスはcsvファイルを利用するためのものです。
package com.example.demo.dbtest;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.csv.CsvURLDataSet;
import org.springframework.core.io.Resource;
import com.github.springtestdbunit.dataset.AbstractDataSetLoader;
public class CsvDataSetLoader extends AbstractDataSetLoader{
@Override
protected IDataSet createDataSet(Resource resource) throws Exception {
return new CsvURLDataSet(resource.getURL());
}
}
/*
* AbstractDataSetLoader
* データセットを読み込むための抽象クラス
*
* createDataSet()
* データセットを作成するためのファクトリメソッド
*
* Resource型
* 実ファイルへアクセスするための情報・振舞をもっている
* resourceオブジェクトには処理対象のCSVファイルのパスが格納される
*
* CsvURLDataSet
* resourceオブジェクトをもとにCSVの実ファイルを取得し、
* データセットオブジェクトへ変換
* →DBUnitが処理できるようになる
*
*
*/
DB_CRUD_Test.java
※テスト④とテスト⑤については、assertEqualsやらassertThatやらで期待値と実測値の記載順が異なるので、分かりやすいようにexpectとactualという変数で書いています。(少し冗長かもしれませんが;^^)
package com.example.demo.dbtest;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
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.support.DependencyInjectionTestExecutionListener;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.model.User;
import com.example.demo.service.UService;
import com.github.springtestdbunit.TransactionDbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import com.github.springtestdbunit.assertion.DatabaseAssertionMode;
@SpringBootTest
@Transactional
@DbUnitConfiguration(dataSetLoader = CsvDataSetLoader.class)//CsvDataSetLoaderを利用する
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
TransactionDbUnitTestExecutionListener.class
})
class DB_CRUD_Test {
@Autowired
private UService service;
//テスト④select1件
//init-data内のcsvファイルのデータを読み取り、IDを引数として1件selectした結果が期待どおりであるか、assertEqualsで検証しています。
@Test
@DatabaseSetup(value = "/testData/init-data/")
void selectOneのテスト() throws Exception {
User expect = User.builder().id(1).name("test1").age(21).build();//lombokのBuilder利用
User actual = service.getUserOne(1);//引数はID
assertEquals(expect, actual);
}
//テスト⑤select全件
//init-data内のcsvファイルのデータを読み取り、全件selectしてその要素数が期待通りであるか、assertEqualsで検証しています。
@Test
@DatabaseSetup(value = "/testData/init-data/")
void selectAllのテスト() throws Exception {
List<User> users = service.getUserList();
int expect = 3;
int actual = users.size();
assertEquals(expect, actual);//要素数が3であるかテスト
}
//テスト⑥insert
//init-data内のcsvファイルのデータを読み取ったのち、メソッド内の処理を実行し、ExpectedDatabaseで指定したディレクトリ内のcsvファイルと同じ中身になっているか検証しています。assertEqulas等は書かなくても勝手に検証してくれます。かしこい。
@Test
@DatabaseSetup(value = "/testData/init-data/")
@ExpectedDatabase(value = "/testData/after-create-data/", table = "users", assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED)//NON_STRICT:期待結果データファイルに存在しないテーブル・カラムが、実際のデータベースに存在しても無視する。UNORDERED:順序も無視する。
void insertのテスト() throws Exception {
User newUser = User.builder().id(4).name("test4").age(24).build();
service.insertOne(newUser);
}
//テスト⑦update テスト⑥とほぼ同じです。
@Test
@DatabaseSetup(value = "/testData/init-data/")
@ExpectedDatabase(value = "/testData/after-update-data/", table = "users", assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED)//NON_STRICT:期待結果データファイルに存在しないテーブル・カラムが、実際のデータベースに存在しても無視する。UNORDERED:順序も無視する。
void updateのテスト() throws Exception {
service.updateOne(3, "update", 23);//ID3のname「test3」が「update」になっている
}
//テスト⑧delete テスト⑦とほぼ同じです。
@Test
@DatabaseSetup(value = "/testData/init-data/")
@ExpectedDatabase(value = "/testData/after-delete-data/", table = "users", assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED)//NON_STRICT:期待結果データファイルに存在しないテーブル・カラムが、実際のデータベースに存在しても無視する。UNORDERED:順序も無視する。
void deleteのテスト() throws Exception {
List<User> users = service.getUserList();
service.deleteOne(users.get(2));
}
/*
* TestExecutionListeners内の解説
*
* DependencyInjectionTestExecutionListener.class
* テストで使用するインスタンスへのDI機能を提供している
*
* TransactionDbUnitTestExecutionListener.class
* 以下の2つの機能
* ・DbUnitTestExecutionListener : DBUnitを利用するためのもの
* ・TransactionalTestExecutionListener : Transaction処理が@DatabaseSetupの前にstartし、@ExpecctedDatabaseの後にendする
* ( 公式:https://springtestdbunit.github.io/spring-test-dbunit/apidocs/com/github/springtestdbunit/TransactionDbUnitTestExecutionListener.html )
*
*/
}
table-ordering.txt(全ディレクトリで内容は共通)
users
users.csv(init-data内)
初めに読み取るデータです。
id,name,age
1,test1,21
2,test2,22
3,test3,23
users.csv(after-create-data内)
4行目が追加されています。
id,name,age
1,test1,21
2,test2,22
3,test3,23
4,test4,24
users.csv(after-update-data内)
3行目のnameをupdateにしています。
id,name,age
1,test1,21
2,test2,22
3,update,23
users.csv(after-delete-data内)
3行目が削除されています。
id,name,age
1,test1,21
2,test2,22
以上!
以下、参考にさせていただいた記事です。(ありがとうございました!!m(_ _)m)
・Spring Bootでテストコードを書いてみる:HelloWorldの基礎的なテストから始まり、非常に分かりやすい解説で参考になりました。
・SpringBoot+MyBatisのCRUDのテストをDBUnitで書いてみた:私もMyBatisを使っているのでとても参考になりました。