JUnit

JUnit5でコントローラのテストを行うMockMvcや、APIのテストを行うMockRestServiceServerを利用してみた

以前このブログで、MockMvcを利用してコントローラクラスのテストを行うプログラムを紹介したが、コントローラクラス内でAPI呼び出しを行う場合は、さらにMockRestServiceServerを利用する。

APIのテストを行うMockRestServiceServerについては、以下のサイトを参照のこと。
https://qiita.com/kazuki43zoo/items/fa9fea1c813f76080fe7

今回は、JUnit5で、コントローラのテストを行うMockMvcや、APIのテストを行うMockRestServiceServerを利用してみたので、そのサンプルプログラムを共有する。

前提条件

下記記事の実装が完了していること。

Spring BootのRest APIサービスで利用している、Swaggerによるドキュメント定義を編集してみたSpring BootのRest APIサービスのドキュメントを定義したり、API実行を行ったりできるためのライブラリにSwaggerが...

作成したサンプルプログラムの内容

作成したサンプルプログラムの構成は以下の通り。
サンプルプログラムの構成
なお、上記の赤枠は、このブログで掲載するソースコードである。

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>
「CODE×CODE」は、需要の高い技術(AWS, Python等)を習得できるプログラミングスクールスクールだった近年、さまざまな会社でクラウド(特にIaaSやPaaSのパブリッククラウド)の需要が非常に高まっていて、クラウドサービスによるシステム開...

また、コントローラクラスの内容は以下の通りで、初期表示時とユーザー情報登録時に、前提条件の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;
    }
    
}
【PR】「Filmora」は初心者でも本格的な動画編集ができる大変便利なツールだった「Filmora」は初心者でも使いやすい動画編集ツールで、テンプレートとして利用できるテキスト・動画・音楽などが充実していると共に、複数...

さらに、上記コントローラクラスの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



サラリーマン型フリーランスSEという働き方でお金の不安を解消しよう先日、「サラリーマン型フリーランスSE」という働き方を紹介するYouTube動画を視聴しましたので、その内容をご紹介します。 「サ...

テスト対象プログラムの実行結果

テスト対象プログラムの実行結果は、以下の通り。

1) 実行前のデータは、以下の通り。
サンプルプログラムの実行結果_1

2) 前提条件となるRest APIサービスのSpring Bootアプリケーションを起動後、今回作成したプロジェクトのSpring Bootアプリケーションを起動し、「http:// localhost:8084/」とアクセスすると、以下の画面が表示される。
サンプルプログラムの実行結果_2

3) 上記画面で「データ追加」ボタンを押下すると、以下の画面に遷移する。
サンプルプログラムの実行結果_3

4) 各項目を入力し「登録」ボタンを押下すると、以下のように、指定したデータが一覧に追加されることが確認できる。
サンプルプログラムの実行結果_4_1

サンプルプログラムの実行結果_4_2

5) 実行後のデータは以下の通りで、ID=3のデータが追加されていることが確認できる。
サンプルプログラムの実行結果_5



「AOMEI Backupper」は様々な形でバックアップ取得や同期処理が行える便利ツールだったパソコン内のデータを、ファイル/パーティション/ディスク等の様々な単位でバックアップしたり、バックアップ時のスケジュール設定やリアルタイ...

テストプログラムの実行結果

テストプログラムの実行結果は、以下の通り。

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

2) testAddNormalメソッドを実行した結果は以下の通りで、API実行後により追加されたユーザーデータが、ModelのuserDataListに追加されることが確認できる。
サンプルプログラムのテスト実行結果_2

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

要点まとめ

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