以前このブログで、MockMvcを利用してコントローラクラスのテストを行うプログラムを紹介したが、コントローラクラス内でAPI呼び出しを行う場合は、さらにMockRestServiceServerを利用する。
APIのテストを行うMockRestServiceServerについては、以下のサイトを参照のこと。
https://qiita.com/kazuki43zoo/items/fa9fea1c813f76080fe7
今回は、JUnit5で、コントローラのテストを行うMockMvcや、APIのテストを行うMockRestServiceServerを利用してみたので、そのサンプルプログラムを共有する。
前提条件
下記記事の実装が完了していること。
作成したサンプルプログラムの内容
作成したサンプルプログラムの構成は以下の通り。

なお、上記の赤枠は、このブログで掲載するソースコードである。
pom.xmlの内容は以下の通りで、JUnit5を利用できるための設定を追加している。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example.demo</groupId>
<artifactId>demoRestApiCallWeb</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>demoRestApiCallWeb</name>
<description>Demo Rest Api Call Web Project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- JUnit5を利用するための設定を追加 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>また、コントローラクラスの内容は以下の通りで、初期表示時とユーザー情報登録時に、前提条件のAPIサービスの呼び出しを行っている。
package com.example.demo;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;
@Controller
public class DemoController {
/** RestTemplateオブジェクト */
@Autowired
private RestTemplate restTemplate;
/** HttpHeadersオブジェクト */
@Autowired
private HttpHeaders httpHeaders;
/** ログ出力のためのクラス */
private static Log log = LogFactory.getLog(DemoController.class);
/**
* ユーザーデータを取得し、初期表示画面に遷移する
* @param model Modelオブジェクト
* @return 初期表示画面へのパス
*/
@RequestMapping("/")
public String index(Model model) {
// ユーザーデータリストをAPIで取得し、Modelオブジェクトに設定する
ResponseEntity<List<UserData>> response = restTemplate.exchange(
"http://localhost:8085/users", HttpMethod.GET,
null, new ParameterizedTypeReference<List<UserData>>() {});
model.addAttribute("userDataList", response.getBody());
return "index";
}
/**
* ユーザー登録画面に遷移する
* @param model Modelオブジェクト
* @return ユーザー登録画面へのパス
*/
@PostMapping("/toAdd")
public String toAdd(Model model) {
model.addAttribute("demoForm", new DemoForm());
return "add";
}
/**
* ユーザー登録を行い、初期表示画面に遷移する
* @param demoForm Formオブジェクト
* @param model Modelオブジェクト
* @return 初期表示画面に遷移する処理
*/
@PostMapping(value = "/add", params = "next")
public String add(DemoForm demoForm, Model model){
try {
// ユーザー登録処理を行う
UserData newUserData = getAddUserData(demoForm);
restTemplate.exchange(
"http://localhost:8085/users", HttpMethod.POST
, new HttpEntity<>(newUserData, httpHeaders)
, UserData.class);
} catch(Exception ex) {
log.error(ex);
model.addAttribute("message", "エラーが発生しました。");
return toAdd(model);
}
// 初期表示画面に遷移
return index(model);
}
/**
* ユーザー登録画面から、初期表示画面に戻る
* @param model Modelオブジェクト
* @return 初期表示画面に戻る処理
*/
@PostMapping(value = "/add", params = "back")
public String toIndex(Model model){
// 初期表示画面に戻る
return index(model);
}
/**
* 引数のフォームから、戻り値ユーザーデータの値を生成する
* @param demoForm Formオブジェクト
* @return ユーザーデータ
*/
private UserData getAddUserData(DemoForm demoForm) {
UserData userData = new UserData();
userData.setId(Long.valueOf(demoForm.getId()));
userData.setName(demoForm.getName());
userData.setBirthY(Integer.valueOf(demoForm.getBirthY()));
userData.setBirthM(Integer.valueOf(demoForm.getBirthM()));
userData.setBirthD(Integer.valueOf(demoForm.getBirthD()));
userData.setSex(demoForm.getSex());
userData.setMemo(demoForm.getMemo());
return userData;
}
}さらに、上記コントローラクラスのindexメソッド・addメソッドのテストを行うテストクラスは以下の通りで、コントローラのテストを行うためのMockMvcや、APIのテストを行うためのMockRestServiceServerを利用している。
package com.example.demo;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import com.fasterxml.jackson.databind.ObjectMapper;
@SpringBootTest
public class DemoControllerTest {
/** MockMvcオブジェクト */
private MockMvc mockMvc;
/** MockServerオブジェクト */
private MockRestServiceServer mockServer;
/** テスト対象となるコントローラクラス */
@Autowired
private DemoController target;
/** テスト対象となるコントローラクラスで呼ばれるRestTemplateオブジェクト */
@Autowired
private RestTemplate restTemplate;
/** JSON文字列とObjectの変換を行うオブジェクト */
@Autowired
private ObjectMapper objectMapper;
/**
* 各テストメソッドを実行する前に実行する処理
*/
@BeforeEach
public void setUp() {
// MockMvcオブジェクトにテスト対象クラスを設定
mockMvc = MockMvcBuilders.standaloneSetup(target)
.setViewResolvers(viewResolver())
.build();
// テスト対象となるコントローラクラスで呼ばれる
// RestTemplateにMockサーバーを割り当てる
mockServer = MockRestServiceServer
.bindTo(restTemplate)
.build();
}
/**
* ViewResolverをThymeleaf用に設定する
* @return 設定したViewResolver
*/
private ViewResolver viewResolver() {
// この設定が無いと、javax.servlet.ServletException: Circular view path [add]:
// would dispatch back to the current handler URL [/add] again.
// という例外が発生する場合がるため、追加する。
InternalResourceViewResolver viewResolver
= new InternalResourceViewResolver();
viewResolver.setPrefix("classpath:templates/");
viewResolver.setSuffix(".html");
return viewResolver;
}
/**
* DemoControllerクラスのindexメソッドのテストを実行する
* @throws Exception 任意例外
*/
@Test
public void testIndex() throws Exception {
String mockServerRes = objectMapper.writeValueAsString(getUserDataList());
List<UserData> userDataList = getUserDataList();
System.out.println("*** testIndexメソッド 開始 ***");
// テスト対象となるコントローラクラスのindexメソッドで呼ばれるAPI実行時のモック設定をする
// APIのURLを設定
mockServer.expect(requestTo("http://localhost:8085/users"))
// HTTPメソッドをGETに設定
.andExpect(method(HttpMethod.GET))
// APIの戻り値を設定
.andRespond(withSuccess(mockServerRes, MediaType.APPLICATION_JSON));
System.out.println("API実行後の戻り値 : " + mockServerRes);
// テスト対象メソッド(index)を実行
mockMvc.perform(get("/"))
// HTTPステータスがOKであることを確認
.andExpect(status().isOk())
// 次画面の遷移先がindex.htmlであることを確認
.andExpect(view().name("index"))
// Modelオブジェクトにエラーが無いことを確認
.andExpect(model().hasNoErrors())
// Modelオブジェクトの設定値を確認
.andExpect(model().attribute("userDataList", userDataList));
System.out.println("Modelオブジェクト、userDataListの設定値 : " + userDataList);
System.out.println("*** testIndexメソッド 終了 ***");
}
/**
* testIndexメソッドで利用するユーザーデータリストを返却する
* @return ユーザーデータリスト
*/
private List<UserData> getUserDataList() {
List<UserData> userDataList = new ArrayList<>();
userDataList.add(
new UserData(1, "テスト プリン1", 2012, 3, 12, "1", "テスト1"));
userDataList.add(
new UserData(2, "テスト プリン2", 2013, 1, 7, "2", "テスト2"));
return userDataList;
}
/**
* DemoControllerクラスのaddメソッド(正常時)のテストを実行する
* @throws Exception 任意例外
*/
@Test
public void testAddNormal() throws Exception {
UserData userData
= new UserData(3, "テスト プリン3", 2010, 8, 31, "2", "テスト3");
String jsonUserData = objectMapper.writeValueAsString(userData);
System.out.println("*** testAddNormalメソッド 開始 ***");
// テスト対象となるコントローラクラスのaddメソッドで呼ばれるAPI実行時のモック設定をする
mockServer.expect(requestTo("http://localhost:8085/users"))
// HTTPメソッドをPOSTに設定
.andExpect(method(HttpMethod.POST))
// HTTP HeaderのContent-Typeがapplication/jsonであることを確認
.andExpect(header("Content-Type", "application/json"))
// リクエストボディの設定値を確認
.andExpect(content().string(jsonUserData))
// APIの戻り値を設定
.andRespond(withSuccess(jsonUserData, MediaType.APPLICATION_JSON));
System.out.println("API実行時のリクエストボディ : " + jsonUserData);
System.out.println("API実行後の戻り値 : " + jsonUserData);
String mockServerRes = objectMapper.writeValueAsString(getUserDataList2());
List<UserData> userDataList2 = getUserDataList2();
// テスト対象となるコントローラクラスのindexメソッドで呼ばれるAPI実行時のモック設定をする
mockServer.expect(requestTo("http://localhost:8085/users"))
// HTTPメソッドをGETに設定
.andExpect(method(HttpMethod.GET))
// APIの戻り値を設定
.andRespond(withSuccess(mockServerRes, MediaType.APPLICATION_JSON));
// テスト対象メソッド(add)を実行
DemoForm demoForm
= new DemoForm("3", "テスト プリン3", "2010", "8", "31", "2", "テスト3");
mockMvc.perform(MockMvcRequestBuilders.post("/add")
// paramsに設定する値を指定
.param("next", "next")
// formオブジェクトを設定
.flashAttr("demoForm", demoForm))
// HTTPステータスがOKであることを確認
.andExpect(status().isOk())
// 次画面の遷移先がindex.htmlであることを確認
.andExpect(view().name("index"))
// Modelオブジェクトにエラーが無いことを確認
.andExpect(model().hasNoErrors())
// Modelオブジェクトの設定値を確認
.andExpect(model().attribute("userDataList", userDataList2));
System.out.println("formオブジェクトの設定値 : " + demoForm);
System.out.println("Modelオブジェクト、userDataListの設定値 : " + userDataList2);
System.out.println("*** testAddNormalメソッド 終了 ***");
}
/**
* testAddNormalメソッドで利用するユーザーデータリストを返却する
* @return ユーザーデータリスト
*/
private List<UserData> getUserDataList2() {
List<UserData> userDataList = new ArrayList<>();
userDataList.add(
new UserData(1, "テスト プリン1", 2012, 3, 12, "1", "テスト1"));
userDataList.add(
new UserData(2, "テスト プリン2", 2013, 1, 7, "2", "テスト2"));
userDataList.add(
new UserData(3, "テスト プリン3", 2010, 8, 31, "2", "テスト3"));
return userDataList;
}
/**
* DemoControllerクラスのaddメソッド(異常時)のテストを実行する
* @throws Exception 任意例外
*/
@Test
public void testAddException() throws Exception {
UserData userData
= new UserData(3, "テスト プリン3", 2010, 2, 31, "2", "テスト3");
String jsonUserData = objectMapper.writeValueAsString(userData);
DemoExceptionResponse demoResp
= new DemoExceptionResponse("生年月日が存在しない日付になっています。"
, "Validation failed for argument [0] in public com.example.demo.UserData"
, new Date());
String badResponseData = objectMapper.writeValueAsString(demoResp);
System.out.println("*** testAddExceptionメソッド 開始 ***");
// テスト対象となるコントローラクラスのaddメソッドで呼ばれるAPI実行時のモック設定をする
mockServer.expect(requestTo("http://localhost:8085/users"))
// HTTPメソッドをPOSTに設定
.andExpect(method(HttpMethod.POST))
// HTTP HeaderのContent-Typeがapplication/jsonであることを確認
.andExpect(header("Content-Type", "application/json"))
// リクエストボディの設定値を確認
.andExpect(content().string(jsonUserData))
// APIの戻り値(例外)を設定
.andRespond((response) -> {
throw new HttpClientErrorException(
HttpStatus.BAD_REQUEST, badResponseData);
});
System.out.println("API実行時のリクエストボディ : " + jsonUserData);
System.out.println("API実行後の戻り値(例外) : "
+ "Bad Request, " + badResponseData);
// テスト対象メソッド(add)を実行
DemoForm demoForm
= new DemoForm("3", "テスト プリン3", "2010", "2", "31", "2", "テスト3");
mockMvc.perform(MockMvcRequestBuilders.post("/add")
// paramsに設定する値を指定
.param("next", "next")
// formオブジェクトを設定
.flashAttr("demoForm", demoForm))
// HTTPステータスがOKであることを確認
.andExpect(status().isOk())
// 次画面の遷移先がadd.htmlであることを確認
.andExpect(view().name("add"))
// Modelオブジェクトにエラーが無いことを確認
.andExpect(model().hasNoErrors())
// Modelオブジェクトの設定値を確認
.andExpect(model().attribute("message", "エラーが発生しました。"))
.andExpect(model().attribute("demoForm", new DemoForm()));
System.out.println("formオブジェクトの設定値 : " + demoForm);
System.out.println("Modelオブジェクト、messageの設定値 : " + "エラーが発生しました。");
System.out.println("Modelオブジェクト、demoFormの設定値 : " + new DemoForm());
System.out.println("*** testAddExceptionメソッド 終了 ***");
}
}その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/junit5-mockmvc-mockapi/demoRestApiCallWeb
テスト対象プログラムの実行結果
テスト対象プログラムの実行結果は、以下の通り。
2) 前提条件となるRest APIサービスのSpring Bootアプリケーションを起動後、今回作成したプロジェクトのSpring Bootアプリケーションを起動し、「http:// localhost:8084/」とアクセスすると、以下の画面が表示される。

3) 上記画面で「データ追加」ボタンを押下すると、以下の画面に遷移する。

4) 各項目を入力し「登録」ボタンを押下すると、以下のように、指定したデータが一覧に追加されることが確認できる。


5) 実行後のデータは以下の通りで、ID=3のデータが追加されていることが確認できる。

テストプログラムの実行結果
テストプログラムの実行結果は、以下の通り。
1) testIndexメソッドを実行した結果は以下の通りで、API実行後の戻り値が、ModelのuserDataListに設定されることが確認できる。

2) testAddNormalメソッドを実行した結果は以下の通りで、API実行後により追加されたユーザーデータが、ModelのuserDataListに追加されることが確認できる。

3) testAddExceptionメソッドを実行した結果は以下の通りで、API実行により例外が返却され、エラー処理が行われることが確認できる。

要点まとめ
- JUnit5でコントローラクラス内でAPI呼び出しを行うテストを行うには、MockMvcに加え、MockRestServiceServerを利用する。






