以前このブログで、MockMvcを利用してコントローラクラスのテストを行うプログラムを紹介したが、コントローラクラス内でAPI呼び出しを行う場合は、さらにMockRestServiceServerを利用する。
APIのテストを行うMockRestServiceServerについては、以下のサイトを参照のこと。
https://qiita.com/kazuki43zoo/items/fa9fea1c813f76080fe7
今回は、JUnit5で、コントローラのテストを行うMockMvcや、APIのテストを行うMockRestServiceServerを利用してみたので、そのサンプルプログラムを共有する。
前提条件
下記記事の実装が完了していること。
作成したサンプルプログラムの内容
作成したサンプルプログラムの構成は以下の通り。
なお、上記の赤枠は、このブログで掲載するソースコードである。
pom.xmlの内容は以下の通りで、JUnit5を利用できるための設定を追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | <?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サービスの呼び出しを行っている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | 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を利用している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | 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を利用する。