以前このブログで、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を利用する。