読者です 読者をやめる 読者になる 読者になる

かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その40 )( 貸出申請画面の作成11 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その39 )( 貸出申請画面の作成10 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 貸出申請画面の作成
      • テストの作成 ( 3回目 )

参照したサイト・書籍

  1. mockMvc test Error Message
    http://stackoverflow.com/questions/25288930/mockmvc-test-error-message

    • 今回のテストとは直接関係ありませんが、MockMvc で取得したコンテンツを出力する方法が書かれていました。
    • .andDo(MockMvcResultHandlers.print()) と書くと出力されます。
  2. 総称型の未検査キャスト
    http://www.profaim.jp/lang-ref/java/generics/cast.php

    • テストクラスで Map.get の戻り値を List<String> でキャストしたら「無検査キャスト」の警告が出たので、その時の調査で参照しました。

目次

  1. テスト作成前に ExceptionHandlerAdvice クラス、WebappErrorController クラスの呼び出され方を確認してみました
  2. ExceptionHandlerAdvice クラスのテストの作成
  3. WebappErrorController クラスのテストの作成
  4. 全てのテストが成功するか確認する
  5. commit、Push、Pull Request、マージ
  6. 次回は。。。

手順

テスト作成前に ExceptionHandlerAdvice クラス、WebappErrorController クラスの呼び出され方を確認してみました

  • @RequestMapping が付加された Controller クラスのメソッドが呼び出されて、そのメソッド内で例外が throw された場合、
    • @ControllerAdvice&@ExceptionHandler(Exception.class) が付加された ExceptionHandlerAdvice.handleException メソッドが呼び出されます。
    • @RequestMapping("/error") が付加された WebappErrorController.index メソッドは呼び出されません。
  • 存在しない URL にアクセスした場合、
    • @RequestMapping("/error") が付加された WebappErrorController.index メソッドが呼び出されます。
    • @ControllerAdvice は機能しませんでした。Controller クラスのメソッドが呼び出されなければ @ControllerAdvice は機能しないようです。
  • MockMvc を使用した場合、@RequestMapping が付加された Controller クラスのメソッドが呼び出されてそのメソッド内で例外が throw された場合に ExceptionHandlerAdvice.handleException メソッドが呼び出される動作は再現できますが、存在しない URL にアクセスして WebappErrorController.index メソッドが呼び出される動作は再現できませんでした。

    MockMvc で存在しない URL にアクセスすると単に 404 ( Not Found ) が返るだけで、WebappErrorController.index メソッドは呼び出されず、コンテンツは何も返ってきませんでした。

ExceptionHandlerAdvice クラスのテストの作成

  1. src/main/java/ksbysample/webapp/lending/web の下の ExceptionHandlerAdviceTest.java で「Create Test」ダイアログを表示し、テストクラスを作成します。

    f:id:ksby:20151231142003p:plain

  2. src/test/java/ksbysample/webapp/lending/web の下に ExceptionHandlerAdviceTest.java が作成されますので、リンク先の内容 に変更します。

  3. テストを実行します。ExceptionHandlerAdviceTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'ExceptionHandlerAdviceTest' with Coverage」を選択します。

    テストが成功することが確認できます。

    f:id:ksby:20151231172859p:plain

WebappErrorController クラスのテストの作成

MockMvc で存在しない URL にアクセスして、WebappErrorController.index メソッドを呼び出してテストするということが出来ないようなので、WebappErrorController.index メソッドを直接呼び出してテストするようにします。

  1. テスト作成中に気づいた問題点を修正します。

  2. src/main/java/ksbysample/webapp/lending/web の下の WebappErrorController.java で「Create Test」ダイアログを表示し、テストクラスを作成します。

    f:id:ksby:20151231202439p:plain

  3. src/test/java/ksbysample/webapp/lending/web の下に WebappErrorControllerTest.java が作成されますので、リンク先のその1の内容 に変更します。

  4. テストを実行します。WebappErrorControllerTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'WebappErrorControllerTest' with Coverage」を選択します。

    テストが成功することが確認できます。

    f:id:ksby:20151231203147p:plain

全てのテストが成功するか確認する

  1. 最後に全てのテストが成功するか確認します。Project View のルートでコンテキストメニューを表示して「Run 'Tests in 'ksbysample...' with Coverage」を選択します。

    テストが実行され、全て成功することが確認できます。

    f:id:ksby:20151231220336p:plain

  2. clean タスクの実行→「Rebuild Project」メニューの実行→build タスクの実行を行い、"BUILD SUCCESSFUL" のメッセージが出力されることも確認します。

    build タスクを実行したところ、注意:C:\project-springboot\ksbysample-webapp-lending\src\test\java\ksbysample\webapp\lending\web\WebappErrorControllerTest.javaの操作は、未チェックまたは安全ではありません。 というメッセージが出力されました。また LibraryHelperTest.java でも警告が出力されました。両方の原因を調査します。

    f:id:ksby:20151231220857p:plain

  3. 調査の前にテストクラスのコンパイル時に -Xlint:all が付くように build.gradle を リンク先の内容 に変更します。

  4. Gradle projects view から compileTestJava タスクを実行します。

    以下の画像の結果が出力されました。List<String> errorInfoList = (List<String>) model.get("errorInfoList");IntelliJ IDEA の自動補完で (List<String>) のキャストを付けていたのですが、それだとダメだったようです。

    f:id:ksby:20151231222528p:plain

  5. Object型から Generics型へのキャストをしているために警告が出るそうなので、テストメソッド@SuppressWarnings("unchecked") を付けて警告が出ないようにします。src/test/java/ksbysample/webapp/lending/web の下の WebappErrorControllerTest.javaリンク先のその2の内容 に変更します。

  6. LibraryHelperTest.java の方の原因は result = new Delegate() { の実装のところで Delegate の定義は public interface Delegate<T> となっているにもかかわらず <T> の部分がなかったためでした。src/test/java/ksbysample/webapp/lending/helper/library の下の LibraryHelperTest.javaリンク先の内容 に修正します。

  7. 調査中に ID の有効期限が切れてテストが失敗する現象が出ましたので、問題のあるデータを修正します。src/test/resources/testdata/base の下の user_info.csvリンク先の内容 に変更します。

  8. 再度 clean タスクの実行→「Rebuild Project」メニューの実行→build タスクの実行を行い、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    今度は無事 "BUILD SUCCESSFUL" のメッセージが出力されることが確認できました。

    f:id:ksby:20151231232026p:plain

commit、Push、Pull Request、マージ

  1. ここまでの変更内容を commit します。

  2. 細かい変更履歴が分かった方が後から見た時に分かりやすそうに思えたので、今回は rebase で1つにまとめずそのままマージします。

  3. GitHub へ Push、feature/43-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/43-issue ブランチを削除、をします。

次回は。。。

以下のソフトウェアがバージョアンアップしているので、バージョンアップを実施します。

その後で貸出承認画面 ( 承認者のみ ) の作成に進む予定です。

ソースコード

ExceptionHandlerAdviceTest.java

package ksbysample.webapp.lending.web;

import ksbysample.common.test.SecurityMockMvcResource;
import ksbysample.webapp.lending.Application;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.stereotype.Controller;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.web.bind.annotation.RequestMapping;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class ExceptionHandlerAdviceTest {

    private static final String MESSAGE_EXCEPTION = "Exception が throw されました";
    private static final String MESSAGE_RUNTIMEEXCEPTION = "RuntimeException が throw されました";
    
    @Autowired
    private ExceptionHandlerAdvice exceptionHandlerAdvice;

    @Rule
    @Autowired
    public SecurityMockMvcResource mvc;
    
    @Controller
    @RequestMapping("/exceptionHandlerAdviceTest")
    public static class ExceptionHandlerAdviceTestController {
        @RequestMapping("/exception")
        public String exception() throws Exception {
            if (true) {
                throw new Exception(MESSAGE_EXCEPTION);
            }
            return null;
        }

        @RequestMapping("/runtimeException")
        public String runtimeException() {
            if (true) {
                throw new RuntimeException(MESSAGE_RUNTIMEEXCEPTION);
            }
            return null;
        }
    }
    
    @Test
    public void testHandleException_Controllerクラス内でExceptionがthrowされた場合() throws Exception {
        // when
        MvcResult result = mvc.authTanakaTaro.perform(get("/exceptionHandlerAdviceTest/exception?parama=1&paramb=2"))
                // 以下のコメントを解除すると MockMvc で取得したコンテンツが標準出力に出力される
                // .andDo(MockMvcResultHandlers.print())
                .andExpect(status().isOk())
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(view().name("error"))
                .andReturn();

        // then
        String content = result.getResponse().getContentAsString();
        assertThat(content)
                .contains("エラーが発生したURL: http://localhost/exceptionHandlerAdviceTest/exception")
                .contains("<span>" + MESSAGE_EXCEPTION + "</span>")
                .contains("parama:1")
                .contains("paramb:2")
                .contains("<span>java.lang.Exception: " + MESSAGE_EXCEPTION + "</span>");
    }

    @Test
    public void testHandleException_Controllerクラス内でRuntimeExceptionがthrowされた場合() throws Exception {
        // when
        MvcResult result = mvc.authTanakaTaro.perform(get("/exceptionHandlerAdviceTest/runtimeException"))
                .andExpect(status().isOk())
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(view().name("error"))
                .andReturn();

        // then
        String content = result.getResponse().getContentAsString();
        assertThat(content)
                .contains("エラーが発生したURL: http://localhost/exceptionHandlerAdviceTest/runtimeException")
                .contains("<span>" + MESSAGE_RUNTIMEEXCEPTION + "</span>")
                .contains("<span>java.lang.RuntimeException: " + MESSAGE_RUNTIMEEXCEPTION + "</span>");
    }

}

ExceptionHandlerAdvice.java

package ksbysample.webapp.lending.web;

import com.google.common.base.Joiner;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

@ControllerAdvice
public class ExceptionHandlerAdvice {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
    @ExceptionHandler(Exception.class)
    public ModelAndView handleException(Exception e
            , HttpServletRequest request
            , HttpServletResponse response) throws IOException {
        String url;
        if (StringUtils.equals(request.getRequestURI(), "/error")) {
            url = (String) request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI);
        } else {
            url = request.getRequestURL().toString();
        }
        url += (StringUtils.isNotEmpty(request.getQueryString()) ? "?" + request.getQueryString() : "");
        logger.error("URL = {}", url, e);

        ModelAndView model = new ModelAndView("error");
        List<String> errorInfoList = new ArrayList<>();

        // エラーメッセージ
        if (e != null && StringUtils.isNotEmpty(e.getMessage())) {
            model.addObject("errorMessage", e.getMessage());

            // Spring Security の機能でアクセス不可と判断された場合に HTTPステータスコードが 403 になるようにする
            if (e instanceof org.springframework.security.access.AccessDeniedException) {
                response.setStatus(HttpStatus.FORBIDDEN.value());
            }
        } else {
            model.addObject("errorMessage", response.getStatus() + " " + HttpStatus.valueOf(response.getStatus()).getReasonPhrase());
        }
        // エラー発生日時
        model.addObject("currentdt", LocalDateTime.now());
        // URL
        errorInfoList.add(" ");
        errorInfoList.add("エラーが発生したURL: " + url);
        // URLパラメータ
        errorInfoList.add("URLパラメータ一覧:");
        Map<String, String[]> params = request.getParameterMap();
        params.entrySet().stream()
                .forEach(param -> errorInfoList.add(param.getKey() + ":" + Joiner.on(", ").join(param.getValue())));
        model.addObject("errorInfoList", errorInfoList);
        // スタックトレース
        if (e != null) {
            try (
                    StringWriter sw = new StringWriter();
                    PrintWriter pw = new PrintWriter(sw)
            ) {
                e.printStackTrace(pw);
                pw.flush();
                String stacktrace = sw.toString();
                errorInfoList.add(" ");
                Arrays.asList(stacktrace.split(System.lineSeparator())).stream()
                        .forEach(line -> errorInfoList.add(line));
            }
        }
        
        return model;
    }

}

WebappErrorControllerTest.java

■その1

package ksbysample.webapp.lending.web;

import ksbysample.webapp.lending.Application;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.RequestDispatcher;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class WebappErrorControllerTest {
    
    private static final String REQUEST_URI_NOT_FOUND = "/notFoundUrl";
    
    @Autowired
    private WebappErrorController webappErrorController;

    @Test
    public void testIndex() throws Exception {
        // setup
        Exception e = new Exception("");
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setRequestURI("/error");
        request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, REQUEST_URI_NOT_FOUND);
        MockHttpServletResponse response = new MockHttpServletResponse();
        response.setStatus(HttpStatus.NOT_FOUND.value());

        // when
        ModelAndView modelAndView = webappErrorController.index(e, request, response);

        // then
        Map<String, Object> model = modelAndView.getModel();
        assertThat(model.get("errorMessage")).isEqualTo(HttpStatus.NOT_FOUND.value() + " " + HttpStatus.NOT_FOUND.getReasonPhrase());
        List<String> errorInfoList = (List<String>) model.get("errorInfoList");
        assertThat(errorInfoList)
                .contains("エラーが発生したURL: " + REQUEST_URI_NOT_FOUND);
    }
}

■その2




build.gradle

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'de.undercouch.download'
apply plugin: 'groovy'

sourceCompatibility = 1.8
targetCompatibility = 1.8

compileJava.options.compilerArgs = ['-Xlint:all']
compileTestGroovy.options.compilerArgs = ['-Xlint:all']
compileTestJava.options.compilerArgs = ['-Xlint:all']
  • 以下の2行を追加します。
    • compileTestGroovy.options.compilerArgs = ['-Xlint:all']
    • compileTestJava.options.compilerArgs = ['-Xlint:all']

LibraryHelperTest.java

    @Test
    public void testGetSelectedLibrary_図書館が選択されている場合() throws Exception {
        new NonStrictExpectations() {{
            libraryForsearchDao.selectSelectedLibrary();
            result = new Delegate<LibraryForsearch>() {
                LibraryForsearch aDelegateMethod() {
                    LibraryForsearch libraryForsearch = new LibraryForsearch();
                    libraryForsearch.setSystemid("System_Id");
                    libraryForsearch.setFormal("図書館名");
                    return libraryForsearch;
                }
            };
        }};

        String result = libraryHelper.getSelectedLibrary();
        assertThat(result).isEqualTo("選択中:図書館名");
    }
  • testGetSelectedLibrary_図書館が選択されている場合() メソッド内の new Delegate()new Delegate<LibraryForsearch>() へ変更します。Delegate の後の <T> の部分には、内部で定義するメソッドの戻り値の型を記述します。

user_info.csv

user_id,username,password,mail_address,enabled,cnt_badcredentials,expired_account,expired_password
1,"tanaka taro",$2a$10$LKKepbcPCiT82NxSIdzJr.9ph.786Mxvr.VoXFl4hNcaaAn9u7jje,tanaka.taro@sample.com,1,0,"9999-12-31 23:59:00.000000","9999-12-31 23:59:00.000000"
2,"suzuki hanako",$2a$10$.fiPEZ155Rl41/e.mdM3A.mG0iEQNPmhjFL/aIiV8dZnXsCd.oqji,suzuki.hanako@test.co.jp,1,0,"9999-12-31 23:59:00.000000","9999-12-31 23:59:00.000000"
3,"kimura masao",$2a$10$yP1dLPIq9j7WQVH6ruSwkepf8jIkPxTtncbSnYM0/jAGQ4HCQO8R.,kimura.masao@test.co.jp,0,0,"2015-12-31 22:30:54.425000","2015-10-15 22:31:03.316000"
4,"endo yoko",$2a$10$PVFe8Lh1Pkjc54DWS9mJL.q407x51ZK8MSXhwuTF9zxCnnt80LKwy,endo.yoko@sample.com,1,0,"2015-01-10 22:31:55.454000","2015-12-31 22:32:11.886000"
5,"sato masahiko",$2a$10$qIU0kM/p1pa7KSIjF6YA4eORd2wL1Eo6TlvH./DmPs7D.xXQPEq7a,sato.masahiko@sample.com,1,0,"9999-12-31 23:59:00.000000","2014-08-05 22:34:22.818000"
6,"takahasi naoko",$2a$10$iXp/d4wXmfaLKTjQKBvik.kETgx4nQ.FL1NjYt4ALJOGSyVOSchW6,takahasi.naoko@test.co.jp,1,0,"2015-12-01 22:39:48.475000","2015-11-10 22:39:55.422000"
7,"kato hiroshi",$2a$10$g5dtFTtNBdJO30aHg50rluGNa2pEAzArcwYkYyCG91ElBZPs9sDi2,kato.hiroshi@sample.com,0,5,"2014-01-01 15:58:53.295000","2013-12-31 15:59:07.668000"
  • user_id = 5 ( sato masahiko ) のデータの ID の有効期限を 2015-12-31 22:34:14.8270009999-12-31 23:59:00.000000 へ変更します。

履歴

2015/12/31
初版発行。