Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その40 )( 貸出申請画面の作成11 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その39 )( 貸出申請画面の作成10 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 貸出申請画面の作成
- テストの作成 ( 3回目 )
- 貸出申請画面の作成
参照したサイト・書籍
mockMvc test Error Message
http://stackoverflow.com/questions/25288930/mockmvc-test-error-message- 今回のテストとは直接関係ありませんが、MockMvc で取得したコンテンツを出力する方法が書かれていました。
.andDo(MockMvcResultHandlers.print())
と書くと出力されます。
総称型の未検査キャスト
http://www.profaim.jp/lang-ref/java/generics/cast.php- テストクラスで Map.get の戻り値を
List<String>
でキャストしたら「無検査キャスト」の警告が出たので、その時の調査で参照しました。
- テストクラスで Map.get の戻り値を
目次
- テスト作成前に ExceptionHandlerAdvice クラス、WebappErrorController クラスの呼び出され方を確認してみました
- ExceptionHandlerAdvice クラスのテストの作成
- WebappErrorController クラスのテストの作成
- 全てのテストが成功するか確認する
- commit、Push、Pull Request、マージ
- 次回は。。。
手順
テスト作成前に ExceptionHandlerAdvice クラス、WebappErrorController クラスの呼び出され方を確認してみました
- @RequestMapping が付加された Controller クラスのメソッドが呼び出されて、そのメソッド内で例外が throw された場合、
- 存在しない URL にアクセスした場合、
MockMvc を使用した場合、@RequestMapping が付加された Controller クラスのメソッドが呼び出されてそのメソッド内で例外が throw された場合に ExceptionHandlerAdvice.handleException メソッドが呼び出される動作は再現できますが、存在しない URL にアクセスして WebappErrorController.index メソッドが呼び出される動作は再現できませんでした。
MockMvc で存在しない URL にアクセスすると単に 404 ( Not Found ) が返るだけで、WebappErrorController.index メソッドは呼び出されず、コンテンツは何も返ってきませんでした。
ExceptionHandlerAdvice クラスのテストの作成
src/main/java/ksbysample/webapp/lending/web の下の ExceptionHandlerAdviceTest.java で「Create Test」ダイアログを表示し、テストクラスを作成します。
src/test/java/ksbysample/webapp/lending/web の下に ExceptionHandlerAdviceTest.java が作成されますので、リンク先の内容 に変更します。
テストを実行します。ExceptionHandlerAdviceTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'ExceptionHandlerAdviceTest' with Coverage」を選択します。
テストが成功することが確認できます。
WebappErrorController クラスのテストの作成
MockMvc で存在しない URL にアクセスして、WebappErrorController.index メソッドを呼び出してテストするということが出来ないようなので、WebappErrorController.index メソッドを直接呼び出してテストするようにします。
テスト作成中に気づいた問題点を修正します。
src/main/java/ksbysample/webapp/lending/web の下の WebappErrorController.java で「Create Test」ダイアログを表示し、テストクラスを作成します。
src/test/java/ksbysample/webapp/lending/web の下に WebappErrorControllerTest.java が作成されますので、リンク先のその1の内容 に変更します。
テストを実行します。WebappErrorControllerTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'WebappErrorControllerTest' with Coverage」を選択します。
テストが成功することが確認できます。
全てのテストが成功するか確認する
最後に全てのテストが成功するか確認します。Project View のルートでコンテキストメニューを表示して「Run 'Tests in 'ksbysample...' with Coverage」を選択します。
テストが実行され、全て成功することが確認できます。
clean タスクの実行→「Rebuild Project」メニューの実行→build タスクの実行を行い、"BUILD SUCCESSFUL" のメッセージが出力されることも確認します。
build タスクを実行したところ、
注意:C:\project-springboot\ksbysample-webapp-lending\src\test\java\ksbysample\webapp\lending\web\WebappErrorControllerTest.javaの操作は、未チェックまたは安全ではありません。
というメッセージが出力されました。また LibraryHelperTest.java でも警告が出力されました。両方の原因を調査します。調査の前にテストクラスのコンパイル時に
-Xlint:all
が付くように build.gradle を リンク先の内容 に変更します。Gradle projects view から compileTestJava タスクを実行します。
以下の画像の結果が出力されました。
List<String> errorInfoList = (List<String>) model.get("errorInfoList");
で IntelliJ IDEA の自動補完で(List<String>)
のキャストを付けていたのですが、それだとダメだったようです。Object型から Generics型へのキャストをしているために警告が出るそうなので、テストメソッドに
@SuppressWarnings("unchecked")
を付けて警告が出ないようにします。src/test/java/ksbysample/webapp/lending/web の下の WebappErrorControllerTest.java を リンク先のその2の内容 に変更します。LibraryHelperTest.java の方の原因は
result = new Delegate() {
の実装のところで Delegate の定義はpublic interface Delegate<T>
となっているにもかかわらず<T>
の部分がなかったためでした。src/test/java/ksbysample/webapp/lending/helper/library の下の LibraryHelperTest.java を リンク先の内容 に修正します。調査中に ID の有効期限が切れてテストが失敗する現象が出ましたので、問題のあるデータを修正します。src/test/resources/testdata/base の下の user_info.csv を リンク先の内容 に変更します。
再度 clean タスクの実行→「Rebuild Project」メニューの実行→build タスクの実行を行い、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。
今度は無事 "BUILD SUCCESSFUL" のメッセージが出力されることが確認できました。
commit、Push、Pull Request、マージ
ここまでの変更内容を commit します。
細かい変更履歴が分かった方が後から見た時に分かりやすそうに思えたので、今回は rebase で1つにまとめずそのままマージします。
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¶mb=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.827000
→9999-12-31 23:59:00.000000
へ変更します。
履歴
2015/12/31
初版発行。