今回はSpring Boot3(Java)で作った簡単なCRUD操作ができるプログラムに単体テストコードを書いていこうと思います。
単体テストコードを書いて単体テストされる側のプログラムは前回の記事でも紹介しているので以下を参考にしてもらえればと思います。
ポイントは以下の通りです。
- テストコードはsrc\main側と同じフォルダ構成でsrc\testに格納してファイル名の最後にTestなどの名前を付与する。
- controllerのテストはMockMvcのperformでリクエストを実行する
- andExpectやassertEqualsなどを使ってテスト結果が正しいことを検証する。
DemoApplicationTests.java
プロジェクト作成時に自動で作成されるDemoApplicationTests.javaは今回使用しません。
テストされる側のファイル名にTestsを付与したファイルを新規で作成してテストコードを書いていきます。
Controllerクラスの単体テストコード
Controllerクラスの単体テストでは主にエンドポイントURLにリクエストして以下を確認しています。
- ステータスコードが200(正常)で返ってくるか?
- 処理結果が正しいテンプレートファイル(html)に返しているか?
※view().name(テンプレートファイル名.html)
また、ここではサービスクラスのテストは行わないのでサービスクラスをMockMvcでモック化してテストを行っています。一部サービスクラスを使ってしまってますが・・・・
UserControllerTests.java
package com.example.demo.controller;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.when;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import com.example.demo.dto.UserAddRequest;
import com.example.demo.dto.UserUpdateRequest;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
@AutoConfigureMockMvc
@ActiveProfiles("test")
@WebMvcTest(UserController.class)
public class UserControllerTests {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService mockService;
@Test
void ユーザー一覧画面が表示される() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.view().name("user/userlist"))
.andReturn();
}
@Test
void ユーザー新規登録画面が表示される() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/user/add"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.view().name("user/useradd"))
.andReturn();
}
@Test
void ユーザー編集画面が表示される() throws Exception {
User user = new User();
user.setUserid("test02");
user.setName("テスト");
user.setEmail("aaa@example.com");
when(mockService.findById("test02")).thenReturn(user);
mockMvc.perform(MockMvcRequestBuilders.get("/user/test02/edit"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.view().name("user/useredit"))
.andReturn();
}
@Test
void ユーザー削除後にリダイレクトされている() throws Exception {
User expect = new User();
expect.setUserid("test02");
expect.setName("テスト");
expect.setEmail("aaa@example.com");
mockMvc.perform(MockMvcRequestBuilders.get("/user/test02/delete"))
.andExpect(MockMvcResultMatchers.view().name("redirect:/"))
.andReturn();
}
@Test
void ユーザー新規登録時のバリデーション有の画面が正しい() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/user/create"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.view().name("user/useradd"))
.andReturn();
}
@Test
void ユーザー編集時のバリデーション有の画面が正しい() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/user/update"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.view().name("user/useredit"))
.andReturn();
}
@Test
void ユーザー新規登録時のバリデーション無しの画面が正しい() throws Exception {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("test11");
userAddRequest.setName("テスト");
userAddRequest.setEmail("aaa@example.com");
mockMvc.perform((MockMvcRequestBuilders.post("/user/create")).flashAttr("userAddRequest",userAddRequest))
.andExpect(MockMvcResultMatchers.view().name("redirect:/"))
.andReturn();
}
@Test
void ユーザー編集時のバリデーション無しの画面が正しい() throws Exception {
UserUpdateRequest userUpdateRequest = new UserUpdateRequest();
userUpdateRequest.setUserid("test10");
userUpdateRequest.setName("テスト1");
userUpdateRequest.setEmail("aaaa@example.com");
mockMvc.perform((MockMvcRequestBuilders.post("/user/update")).flashAttr("userUpdateRequest",userUpdateRequest))
.andExpect(MockMvcResultMatchers.view().name("redirect:/"))
.andReturn();
}
}
serviceクラスの単体テストコード
serviceクラスのテストでは、controllerから呼び出された実処理の単体テストを行っています。
今回は本番プログラムにcrud操作の実処理を書いているので実際のテストデータが正しくCRUD操作をできているかをテストしています。CRUDの意味は以下の通りです。
- Create(登録)
- Read(読み取り)
- Update(更新)
- Delete(削除)
UserServiceTests.java
package com.example.demo.service;
import static org.junit.jupiter.api.Assertions.assertAll;
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.ActiveProfiles;
import com.example.demo.entity.User;
import com.example.demo.dto.UserAddRequest;
import com.example.demo.dto.UserUpdateRequest;
@SpringBootTest
@ActiveProfiles("test")
public class UserServiceTests {
@Autowired
UserService userService;
@Test
void ユーザー一覧取得() {
List<User> result = userService.findAll();
int count = result.size();
assertEquals(2,count);
}
@Test
void ユーザー新規登録() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("newtestuser");
userAddRequest.setName("テスト");
userAddRequest.setEmail("aaa@example.com");
int pre_count = userService.findAll().size();
userService.save(userAddRequest);
int after_count = userService.findAll().size();
//*実行前より1件増えている */
assertEquals(pre_count + 1,after_count);
userService.delete("newtestuser");
}
@Test
void ユーザー削除() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("deltestuser");
userAddRequest.setName("テスト");
userAddRequest.setEmail("aaa@example.com");
userService.save(userAddRequest);
//削除前ユーザー件数
int pre_count = userService.findAll().size();
userService.delete("deltestuser");
//削除後ユーザー件数
int after_count = userService.findAll().size();
//*実行前より1件減っている */
assertEquals(pre_count -1,after_count);
}
@Test
void ユーザーID検索() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("searchuser");
userAddRequest.setName("テスト");
userAddRequest.setEmail("aaa@example.com");
userService.save(userAddRequest);
User result = userService.findById("searchuser");
assertAll("ユーザー情報一致確認",
() -> assertEquals("searchuser",result.getUserid(),"id一致"),
() -> assertEquals("テスト",result.getName(),"名前一致"),
() -> assertEquals("aaa@example.com",result.getEmail(),"メールアドレス一致")
);
userService.delete("searchuser");
}
@Test
void ユーザー編集() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("updateuser");
userAddRequest.setName("テスト");
userAddRequest.setEmail("aaa@example.com");
//1件追加
userService.save(userAddRequest);
//追加したユーザーを以下で編集
UserUpdateRequest userUpdateRequest = new UserUpdateRequest();
userUpdateRequest.setUserid("updateuser");
userUpdateRequest.setName("編集テスト");
userUpdateRequest.setEmail("bbb@example.com");
//更新
userService.update(userUpdateRequest);
User result = userService.findById("updateuser");
assertAll("ユーザー情報一致確認",
() -> assertEquals("updateuser",result.getUserid(),"id一致"),
() -> assertEquals("編集テスト",result.getName(),"名前が変更されている"),
() -> assertEquals("bbb@example.com",result.getEmail(),"メールアドレスが変更されている")
);
userService.delete("updateuser");
}
}
MyBatis(Daoクラス)の単体テストコード
Daoクラスは、MyBatisというORマッパーを使用しているのでマッパークラスのテストになります。
アノテーション@MybatisTestを付与しています。取得したデータの件数だけではなく各データの値もテストしています。一つの検証メソッドで複数の検証(assertEquals)を使うときはassertAllを使います。
UserDaoTests.java
package com.example.demo.dao;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertAll;
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.test.context.ActiveProfiles;
import com.example.demo.dto.UserAddRequest;
import com.example.demo.dto.UserUpdateRequest;
import com.example.demo.entity.User;
@MybatisTest
@ActiveProfiles("test")
public class UserDaoTests {
@Autowired
private UserDao userDao;
@Test
void ユーザー一覧取得() {
List<User> result = userDao.findAll();
int count = result.size();
assertAll("ユーザ情報取得",
() -> assertEquals("test01",result.get(0).getUserid(),"id一致"),
() -> assertEquals("テスト01",result.get(0).getName(),"名前一致"),
() -> assertEquals("test01@example.com",result.get(0).getEmail(),"メール一致"),
() -> assertEquals(2,count,"取得件数"));
}
@Test
void ユーザー新規登録() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("newtestuser");
userAddRequest.setName("テスト");
userAddRequest.setEmail("aaa@example.com");
userDao.save(userAddRequest);
User result = userDao.findById("newtestuser");
assertAll("新規ユーザー情報登録確認",
() -> assertEquals("newtestuser",result.getUserid(),"id一致"),
() -> assertEquals("テスト",result.getName(),"名前一致"),
() -> assertEquals("aaa@example.com",result.getEmail(),"メール一致"));
userDao.delete("newtestuser");
}
@Test
void ユーザー削除() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("deltestuser");
userAddRequest.setName("テスト");
userAddRequest.setEmail("aaa@example.com");
userDao.save(userAddRequest);
//削除前ユーザー件数
int pre_count = userDao.findAll().size();
userDao.delete("deltestuser");
//削除後ユーザー件数
int after_count = userDao.findAll().size();
//*実行前より1件減っている */
assertEquals(pre_count -1,after_count);
}
@Test
void ユーザーID検索() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("searchuser");
userAddRequest.setName("テスト");
userAddRequest.setEmail("aaa@example.com");
userDao.save(userAddRequest);
User result = userDao.findById("searchuser");
assertAll("ユーザー情報一致確認",
() -> assertEquals("searchuser",result.getUserid(),"id一致"),
() -> assertEquals("テスト",result.getName(),"名前一致"),
() -> assertEquals("aaa@example.com",result.getEmail(),"メールアドレス一致")
);
userDao.delete("searchuser");
}
@Test
void ユーザー編集() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("updateuser");
userAddRequest.setName("テスト");
userAddRequest.setEmail("aaa@example.com");
//1件追加
userDao.save(userAddRequest);
//追加したユーザーを以下で編集
UserUpdateRequest userUpdateRequest = new UserUpdateRequest();
userUpdateRequest.setUserid("updateuser");
userUpdateRequest.setName("編集テスト");
userUpdateRequest.setEmail("bbb@example.com");
//更新
userDao.update(userUpdateRequest);
User result = userDao.findById("updateuser");
assertAll("ユーザー情報一致確認",
() -> assertEquals("updateuser",result.getUserid(),"id一致"),
() -> assertEquals("編集テスト",result.getName(),"名前が変更されている"),
() -> assertEquals("bbb@example.com",result.getEmail(),"メールアドレスが変更されている")
);
userDao.delete("updateuser");
}
}
dtoクラスの単体テストコード
今回作成したdtoクラスはバリデーション処理の単体テストとなっています。バリデーションはspringboot3から変更となったのでjavaxではなくjakarta.validationを使用していいます。
Validation.buildDefaultValidatorFactory().getValidator();でインスタンスを取得し、ConstraintViolationにバリデーションエラーとなった情報を格納してviolations.size()でエラー数と
violations.stream().forEachでエラー―の内容が想定通りであることをテストしています。
UserAddRequestTests.java
package com.example.demo.dto;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
@SpringBootTest
@ActiveProfiles("test")
public class UserAddRequestTests {
private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
@Test
public void ユーザーID桁数チェック() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("test1234567890123456789012345");
userAddRequest.setName("test1234567890123456789012345");
userAddRequest.setEmail("aaa@example.com");
Set<ConstraintViolation<UserAddRequest>> violations = validator.validate(userAddRequest);
assertEquals(violations.size(),1);
violations.stream().forEach(v ->{
assertEquals(v.getMessage(), "ユーザーIDは24桁以内で入力してください");
});
}
@Test
public void ユーザーID入力チェック() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("");
userAddRequest.setName("test1234567890123456789012345");
userAddRequest.setEmail("aaa@example.com");
Set<ConstraintViolation<UserAddRequest>> violations = validator.validate(userAddRequest);
assertEquals(violations.size(),1);
violations.stream().forEach(v ->{
assertEquals(v.getMessage(), "ユーザーIDを入力してください(必須)");
});
}
@Test
public void 名前桁数チェック() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("test1");
userAddRequest.setName("test1234567890123456789012345test123456789012345678901234512");
userAddRequest.setEmail("aaa@example.com");
Set<ConstraintViolation<UserAddRequest>> violations = validator.validate(userAddRequest);
assertEquals(violations.size(),1);
violations.stream().forEach(v ->{
assertEquals(v.getMessage(), "名前は50桁以内で入力してください");
});
}
@Test
public void 名前入力チェック() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("test1");
userAddRequest.setName("");
userAddRequest.setEmail("aaa@example.com");
Set<ConstraintViolation<UserAddRequest>> violations = validator.validate(userAddRequest);
assertEquals(violations.size(),1);
violations.stream().forEach(v ->{
assertEquals(v.getMessage(), "名前を入力してください(必須)");
});
}
@Test
public void メールアドレスバリデーションチェック() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("test1");
userAddRequest.setName("テスト01");
userAddRequest.setEmail("@");
Set<ConstraintViolation<UserAddRequest>> violations = validator.validate(userAddRequest);
assertEquals(violations.size(),1);
violations.stream().forEach(v ->{
assertEquals(v.getMessage(), "メールアドレスの形式で入力してください");
});
}
@Test
public void メールアドレス入力() {
UserAddRequest userAddRequest = new UserAddRequest();
userAddRequest.setUserid("test1");
userAddRequest.setName("テスト01");
userAddRequest.setEmail("");
Set<ConstraintViolation<UserAddRequest>> violations = validator.validate(userAddRequest);
assertEquals(violations.size(),1);
violations.stream().forEach(v ->{
assertEquals(v.getMessage(), "メールアドレスを入力してください(必須)");
});
}
}
編集時のバリデーションも基本同じロジックを継承しているので同じような感じで書いています。
package com.example.demo.dto;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
@SpringBootTest
@ActiveProfiles("test")
public class UserUpdateRequestTests {
private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
@Test
public void ユーザーID桁数チェック() {
UserUpdateRequest userUpdateRequest = new UserUpdateRequest();
userUpdateRequest.setUserid("test1234567890123456789012345");
userUpdateRequest.setName("test1234567890123456789012345");
userUpdateRequest.setEmail("aaa@example.com");
Set<ConstraintViolation<UserAddRequest>> violations = validator.validate(userUpdateRequest);
assertEquals(violations.size(),1);
violations.stream().forEach(v ->{
assertEquals(v.getMessage(), "ユーザーIDは24桁以内で入力してください");
});
}
@Test
public void ユーザーID入力チェック() {
UserUpdateRequest userUpdateRequest = new UserUpdateRequest();
userUpdateRequest.setUserid("");
userUpdateRequest.setName("test1234567890123456789012345");
userUpdateRequest.setEmail("aaa@example.com");
Set<ConstraintViolation<UserAddRequest>> violations = validator.validate(userUpdateRequest);
assertEquals(violations.size(),1);
violations.stream().forEach(v ->{
assertEquals(v.getMessage(), "ユーザーIDを入力してください(必須)");
});
}
@Test
public void 名前桁数チェック() {
UserUpdateRequest userUpdateRequest = new UserUpdateRequest();
userUpdateRequest.setUserid("test1");
userUpdateRequest.setName("test1234567890123456789012345test123456789012345678901234512");
userUpdateRequest.setEmail("aaa@example.com");
Set<ConstraintViolation<UserAddRequest>> violations = validator.validate(userUpdateRequest);
assertEquals(violations.size(),1);
violations.stream().forEach(v ->{
assertEquals(v.getMessage(), "名前は50桁以内で入力してください");
});
}
@Test
public void 名前入力チェック() {
UserUpdateRequest userUpdateRequest = new UserUpdateRequest();
userUpdateRequest.setUserid("test1");
userUpdateRequest.setName("");
userUpdateRequest.setEmail("aaa@example.com");
Set<ConstraintViolation<UserAddRequest>> violations = validator.validate(userUpdateRequest);
assertEquals(violations.size(),1);
violations.stream().forEach(v ->{
assertEquals(v.getMessage(), "名前を入力してください(必須)");
});
}
@Test
public void メールアドレスバリデーションチェック() {
UserUpdateRequest userUpdateRequest = new UserUpdateRequest();
userUpdateRequest.setUserid("test1");
userUpdateRequest.setName("テスト01");
userUpdateRequest.setEmail("@");
Set<ConstraintViolation<UserAddRequest>> violations = validator.validate(userUpdateRequest);
assertEquals(violations.size(),1);
violations.stream().forEach(v ->{
assertEquals(v.getMessage(), "メールアドレスの形式で入力してください");
});
}
@Test
public void メールアドレス入力() {
UserUpdateRequest userUpdateRequest = new UserUpdateRequest();
userUpdateRequest.setUserid("test1");
userUpdateRequest.setName("テスト01");
userUpdateRequest.setEmail("");
Set<ConstraintViolation<UserAddRequest>> violations = validator.validate(userUpdateRequest);
assertEquals(violations.size(),1);
violations.stream().forEach(v ->{
assertEquals(v.getMessage(), "メールアドレスを入力してください(必須)");
});
}
}
とりあえずこれで単体テストを簡単に実装してみました。
単体テストの実行
上記単体テストを実行してみます。
VSCodeでテストを選択して実行すればテストが実行されます。もちろんデバッグも可能です。
テストが成功すると以下のように緑になります。
レポートはbuild\reports\tests\test\index.htmlに出力されます。
Jacocoレポート出力
別記事の構築手順で実施している場合はJacocoでのカバレッジレポートも出力できます。
VSCode上でGradle→Tasks→verification→jacocoTestsReportで実行できます。
実行結果は以下に出力されます。
build\reports\jacoco\test\html\index.html
やってみた感想
JavaSpringBootは単体テストのツールが揃っているので思ったより簡単にできました。カバレッジが出力できるのは非常に便利です。カバー率を絶対に100%にするんだというのならかなり面倒な気がしますが….
今回のテストは、我流で調べながら書いたものなので非効率だったり無意味なものもあるような気がしています。この辺の精度を上げる必要があると思いました。
以上で単体テストコードの実装まで終わったので次回はGitHubActionsを使って自動でビルドとテストを実行できる環境を作ってみたいと思います。
コメント