Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その21 )( 検索対象図書館登録画面の作成3 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その20 )( 検索対象図書館登録画面の作成2 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 検索対象図書館登録画面 ( 管理者のみ ) の作成
- 今回はテストを作成します。
- 一部のテストで JMockit を使用します。久しぶりに使おうとしたらライブラリの仕様が以前と違う?
- 検索対象図書館登録画面 ( 管理者のみ ) の作成
参照したサイト・書籍
目次
- 作成済のテストをひと通り実行してみる
- テスト未作成のクラスを洗い出す
- JMockitを利用可能にする
- TestDataResource クラスの変更、testdata/base へのデータの追加
- LibraryHelper クラスのテストの作成
- UrlAfterLoginHelper クラスのテストの作成
- AdminLibraryService クラスのテストの作成
- AdminLibraryController クラスのテストの作成
- 全てのテストが成功するか確認する
- commit、Push、Pull Request、マージ
手順
作成済のテストをひと通り実行してみる
最初に作成済のテストが全て成功するのか確認します。Project View のルートでコンテキストメニューを表示して「Run 'Tests in 'ksbysample...' with Coverage」を選択します。
テストが実行されますが、いくつかのテストが失敗しました。原因を確認します。
原因は以下の2点でした。修正します。
- 現在のテストではログイン後の URL は必ず /loginsuccess にしていますが、管理権限 ( ROLE_ADMIN ) を持つユーザはログイン後に /admin/library へ遷移するように変更したためでした。
- /webapi/library/getLibraryList のレスポンスの JSON で formalName → formal に名称変更していたところで失敗していました。
最初にテストでも使用する URL を定数として定義します。src/main/java/ksbysample/webapp/lending/config の下の Constant.java を リンク先の内容 に変更します。
src/test/java/ksbysample/webapp/lending/web の下の LoginControllerTest.java を リンク先のその1、その2、その3の内容 に変更します。
src/test/java/ksbysample/webapp/lending/webapi/library の下の LibraryControllerTest.java を リンク先の内容 に変更します。
定義した定数を反映できるところに反映します。Ctrl+SHIFT+F を押して「Find in Path」ダイアログを表示し、"/admin/library" が書かれている箇所を検索します。
src/main/java/ksbysample/webapp/lending/security の下の RoleAwareAuthenticationSuccessHandler.java を リンク先のその1の内容 に変更します。
src/main/java/ksbysample/webapp/lending/web/admin/library の下の AdminLibraryController.java を リンク先の内容 に変更します。
再度 Project View のルートでコンテキストメニューを表示して「Run 'Tests in 'ksbysample...' with Coverage」を選択し、テストを実行します。
まだ失敗したテストがあるので原因を確認します。
原因は LoginController クラスの index メソッドに実装した有効な remember-me Cookie がある場合にはログイン画面を表示させずに自動ログインさせる処理で、付与された権限に関係なく固定で /loginsuccess にリダイレクトさせていたためでした。権限に応じたログイン後画面にリダイレクトさせるようにします。
src/main/java/ksbysample/webapp/lending/helper の下に url パッケージを作成します。
src/main/java/ksbysample/webapp/lending/helper/url の下に UrlAfterLoginHelper.java を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/security の下の RoleAwareAuthenticationSuccessHandler.java を リンク先のその2の内容 に変更します。
src/main/java/ksbysample/webapp/lending/web の下の LoginController.java を リンク先の内容 に変更します。
再度テストを実行し、今度は全て成功しました。
一旦 commit します。
テスト未作成のクラスを洗い出す
今回は以下のクラスのテストを作成します。
- ksbysample.webapp.lending.helper.library.LibraryHelper
- ksbysample.webapp.lending.helper.url.UrlAfterLoginHelper
- ksbysample.webapp.lending.web.admin.library.AdminLibraryController
- ksbysample.webapp.lending.web.admin.library.AdminLibraryService
※他にもテスト未作成のクラスはありますが、上のものだけ作成します。
※IntelliJ IDEA でテスト未作成のクラスを検出してくれる機能がないか探したのですが見つからず。。。 今回は1つずつ確認していきました。
JMockitを利用可能にする
一部のテストは DB のデータをわざわざ変更する手間をかける程のものではないので、モックを作成してテストするようにします。モックのライブラリには JMockit を使用します。
build.gradle を リンク先の内容 に変更します。
Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
classpath 内での jmockit.jar の位置を JUnit より前になるようにします。メイン画面のメニューから「File」-「Project Structure...」を選択します。
「Project Structure」ダイアログが表示されます。画面左側のリストから「Project Settings」-「Modules」を選択します。
画面右側で「Dependencies」タブを選択した後、jmockit のライブラリを junit の上に移動します。移動後「OK」ボタンをクリックしてダイアログを閉じます。
TestDataResource クラスの変更、testdata/base へのデータの追加
バックアップ対象のテーブルを追加します。src/test/java/ksbysample/common/test の下の TestDataResource.java を リンク先の内容 に変更します。
src/test/resources/testdata/base の下に library_forsearch.csv を作成します。作成後、リンク先の内容 に変更します。
src/test/resources/testdata/base の下の table-ordering.txt を リンク先の内容 に変更します。
LibraryHelper クラスのテストの作成
src/main/java/ksbysample/webapp/lending/helper/library の下の LibraryHelper.java で「Create Test」ダイアログを表示し、テストクラスを作成します。
src/test/java/ksbysample/webapp/lending/helper/library の下に LibraryHelperTest.java が作成されます。
src/test/java/ksbysample/webapp/lending/helper/library の下の LibraryHelperTest.java を リンク先の内容 に変更します。
テストを実行します。LibraryHelperTest クラスのクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'LibraryHelperTest' with Coverage」を選択します。
テストが成功することが確認できます。
UrlAfterLoginHelper クラスのテストの作成
src/main/java/ksbysample/webapp/lending/helper/url の下の UrlAfterLoginHelper.java で「Create Test」ダイアログを表示し、テストクラスを作成します。
src/test/java/ksbysample/webapp/lending/helper/url の下に UrlAfterLoginHelperTest.java が作成されます。
UrlAfterLoginHelper.getUrlAfterLogin には Authentication インターフェースを持つオブジェクトを渡しますが、Authentication インターフェースの実装クラスには何があるのか IntelliJ IDEA の Diagram 生成機能で調べて見ると以下の画像のクラス構成になっていました。システム稼働時は UsernamePasswordAuthenticationToken クラスが使用されていると思われますが、今回はテストが実行できればよいので TestingAuthenticationToken クラスを使用します。
src/test/java/ksbysample/webapp/lending/helper/url の下の UrlAfterLoginHelperTest.java を リンク先の内容 に変更します。
テストを実行します。UrlAfterLoginHelperTest クラスのクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'UrlAfterLoginHelperTest' with Coverage」を選択します。
テストが成功することが確認できます。
AdminLibraryService クラスのテストの作成
src/main/java/ksbysample/webapp/lending/web/admin/library の下の AdminLibraryService.java で「Create Test」ダイアログを表示し、テストクラスを作成します。
src/test/java/ksbysample/webapp/lending/web/admin/library の下に AdminLibraryServiceTest.java が作成されます。
テストで使用するデータを作成します。src/test/resources の下に ksbysample/webapp/lending/web/admin/library ディレクトリを作成します。
src/test/resources/ksbysample/webapp/lending/web/admin/library の下に SetSelectedLibraryForm_001.yaml を作成します。作成後、リンク先の内容 に変更します。
src/test/resources/ksbysample/webapp/lending/web/admin/library の下に testdata/001 ディレクトリを作成します。
src/test/resources/ksbysample/webapp/lending/web/admin/library/testdata/001 の下に table-ordering.txt, library_forsearch.csv を作成します。作成後、リンク先の内容 に変更します。
src/test/resources/ksbysample/webapp/lending/web/admin/library の下に assertdata/001 ディレクトリを作成します。
src/test/resources/ksbysample/webapp/lending/web/admin/library/assertdata/001 の下に table-ordering.txt, library_forsearch.csv を作成します。作成後、リンク先の内容 に変更します。
src/test/java/ksbysample/webapp/lending/web/admin/library の下の AdminLibraryServiceTest.java を リンク先の内容 に変更します。
AdminLibraryController クラスのテストの作成
src/main/java/ksbysample/webapp/lending/web/admin/library の下の AdminLibraryController.java で「Create Test」ダイアログを表示し、テストクラスを作成します。
src/test/java/ksbysample/webapp/lending/web/admin/library の下に AdminLibraryControllerTest.java が作成されます。
src/test/java/ksbysample/webapp/lending/web/admin/library の下の AdminLibraryControllerTest.java を リンク先の内容 に変更します。
全てのテストが成功するか確認する
最後に全てのテストが成功するか確認します。Project View のルートでコンテキストメニューを表示して「Run 'Tests in 'ksbysample...' with Coverage」を選択します。
テストが実行され、全て成功することが確認できます。
clean タスクの実行→「Rebuild Project」メニューの実行→build タスクの実行を行い、"BUILD SUCCESSFUL" のメッセージが出力されることも確認します。
commit、Push、Pull Request、マージ
ここまでの変更内容を commit します。
コマンドラインから以下のコマンドを実行して commit を1つにまとめます。
> git rebase -i HEAD~7
> git commit --amend -m "#25 検索対象図書館登録画面を作成しました。"GitHub へ Push、1.0.x-make-admin-library -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-make-admin-library ブランチを削除、をします。
ソースコード
Constant.java
package ksbysample.webapp.lending.config; public class Constant { /* * URL一覧 */ public static final String URL_ADMIN_LIBRARY = "/admin/library"; /* * ログイン後ページのURL */ public static final String URL_AFTER_LOGIN_FOR_ROLE_ADMIN = URL_ADMIN_LIBRARY; }
- URL_ADMIN_LIBRARY, URL_AFTER_LOGIN_FOR_ROLE_ADMIN を追加します。
LoginControllerTest.java
■その1
@Test public void 有効なユーザ名とパスワードを入力すればログインに成功する() throws Exception { // ログイン前にはログイン後の画面にアクセスできない mvc.noauth.perform(get(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN)) .andExpect(status().isFound()) .andExpect(redirectedUrl("http://localhost/")) .andExpect(unauthenticated()); // ログインする MvcResult result = mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "taro") ) .andExpect(status().isFound()) .andExpect(redirectedUrl(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN)) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)) .andReturn(); HttpSession session = result.getRequest().getSession(); assertThat(session).isNotNull(); // ログインしたのでログイン後の画面にアクセスできる mvc.noauth.perform(get(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN).session((MockHttpSession) session)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(model().hasNoErrors()) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)); // ログアウトする mvc.noauth.perform(get("/logout").session((MockHttpSession) session)) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")) .andExpect(unauthenticated()); // ログアウトしたのでログイン後の画面にアクセスできない mvc.noauth.perform(get(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN).session((MockHttpSession) session)) .andExpect(status().isFound()) .andExpect(redirectedUrl("http://localhost/")) .andExpect(unauthenticated()); }
有効なユーザ名とパスワードを入力すればログインに成功する()
メソッドの以下の点を変更します。- メソッド内の
WebSecurityConfig.DEFAULT_SUCCESS_URL
→Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN
へ変更します。 - ビュー名までチェックするのはやり過ぎな感じがしたので
.andExpect(view().name("loginsuccess"))
を削除します。
- メソッド内の
■その2
@Test public void 次回から自動的にログインするをチェックすれば次はログインしていなくてもログイン後の画面にアクセスできる() throws Exception { // ログイン前にはログイン後の画面にアクセスできない mvc.noauth.perform(get(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN)) .andExpect(status().isFound()) .andExpect(redirectedUrl("http://localhost/")) .andExpect(unauthenticated()); // 「次回から自動的にログインする」をチェックしてログインし、remember-me Cookie を生成する MockServletContext servletContext = new MockServletContext(); org.springframework.mock.web.MockHttpServletRequest request = formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "taro") .buildRequest(servletContext); request.addParameter("remember-me", "true"); SimpleRequestBuilder simpleRequestBuilder = new SimpleRequestBuilder(request); MvcResult result = mvc.noauth.perform(simpleRequestBuilder) .andExpect(status().isFound()) .andExpect(redirectedUrl(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN)) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)) .andReturn(); Cookie[] cookie = result.getResponse().getCookies(); // remember-me Cookie を引き継いでログイン後の画面にアクセスするとアクセスできる mvc.noauth.perform(get(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN).cookie(cookie)) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(model().hasNoErrors()) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)); // ログイン画面にアクセスしても有効な remember-me Cookie があればログイン後の画面にリダイレクトする mvc.noauth.perform(get("/").cookie(cookie)) .andExpect(status().isFound()) .andExpect(redirectedUrl(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN)) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)); }
次回から自動的にログインするをチェックすれば次はログインしていなくてもログイン後の画面にアクセスできる()
メソッドの以下の点を変更します。
■その3
@Test public void 有効なユーザ名とパスワードを入力すればログインに成功する() throws Exception { mvc.noauth.perform(formLogin() .user("id", mvc.MAILADDR_TANAKA_TARO) .password("password", "taro") ) .andExpect(status().isFound()) .andExpect(redirectedUrl(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN)) .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO)); }
有効なユーザ名とパスワードを入力すればログインに成功する()
メソッドの以下の点を変更します。- メソッド内の
WebSecurityConfig.DEFAULT_SUCCESS_URL
→Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN
へ変更します。
- メソッド内の
LibraryControllerTest.java
@Test public void 正しい都道府県を指定した場合には図書館一覧が返る() throws Exception { mvc.noauth.perform(get("/webapi/library/getLibraryList?pref=東京都")) .andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=UTF-8")) .andExpect(jsonPath("$.errcode", is(0))) .andExpect(jsonPath("$.errmsg", is(""))) .andExpect(jsonPath("$.content[0].address", startsWith("東京都"))) .andExpect(jsonPath("$.content[?(@.formal=='国立国会図書館東京本館')]").exists()); }
正しい都道府県を指定した場合には図書館一覧が返る()
メソッドの以下の点を変更します。$.content[?(@.formalName=='国立国会図書館東京本館')]
→$.content[?(@.formal=='国立国会図書館東京本館')]
へ変更します。
RoleAwareAuthenticationSuccessHandler.java
■その1
package ksbysample.webapp.lending.security; import ksbysample.webapp.lending.config.WebSecurityConfig; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.SavedRequest; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class RoleAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private RequestCache requestCache = new HttpSessionRequestCache(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { // ログイン画面以外のURLを指定してアクセスされていた場合には、処理を SavedRequestAwareAuthenticationSuccessHandler へ委譲する SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest != null) { super.onAuthenticationSuccess(request, response, authentication); return; } // 特定の権限を持っている場合には対応するURLへリダイレクトする String targetUrl = WebSecurityConfig.DEFAULT_SUCCESS_URL; GrantedAuthority roleAdmin = new SimpleGrantedAuthority("ROLE_ADMIN"); if (authentication.getAuthorities().contains(roleAdmin)) { // 管理権限 ( ROLE_ADMIN ) を持っている場合には検索対象図書館登録画面へ遷移させる targetUrl = Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN; } clearAuthenticationAttributes(request); getRedirectStrategy().sendRedirect(request, response, targetUrl); } }
targetUrl = "/admin/library";
→targetUrl = Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN;
へ変更します。
■その2
package ksbysample.webapp.lending.security; import ksbysample.webapp.lending.helper.url.UrlAfterLoginHelper; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.SavedRequest; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class RoleAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private RequestCache requestCache = new HttpSessionRequestCache(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { // ログイン画面以外のURLを指定してアクセスされていた場合には、処理を SavedRequestAwareAuthenticationSuccessHandler へ委譲する SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest != null) { super.onAuthenticationSuccess(request, response, authentication); return; } String targetUrl = UrlAfterLoginHelper.getUrlAfterLogin(authentication); clearAuthenticationAttributes(request); getRedirectStrategy().sendRedirect(request, response, targetUrl); } }
- targetUrl を決める処理は UrlAfterLoginHelper.getUrlAfterLogin を呼び出すように変更します。
AdminLibraryController.java
package ksbysample.webapp.lending.web.admin.library; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @PreAuthorize("hasAuthority('ROLE_ADMIN')") @RequestMapping("/admin/library") public class AdminLibraryController { @Autowired private AdminLibraryService adminLibraryService; @RequestMapping public String index() { return "admin/library/library"; } @RequestMapping("/addSearchLibrary") public String addSearchLibrary(SetSelectedLibraryForm setSelectedLibraryForm) { adminLibraryService.deleteAndInsertLibraryForSearch(setSelectedLibraryForm); return "redirect:" + Constant.URL_ADMIN_LIBRARY; } }
return "redirect:/admin/library";
→return "redirect:" + Constant.URL_ADMIN_LIBRARY;
へ変更します。
UrlAfterLoginHelper.java
package ksbysample.webapp.lending.helper.url; import ksbysample.webapp.lending.config.Constant; import ksbysample.webapp.lending.config.WebSecurityConfig; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; public class UrlAfterLoginHelper { public static String getUrlAfterLogin(Authentication authentication) { String targetUrl = WebSecurityConfig.DEFAULT_SUCCESS_URL; // 特定の権限を持っている場合には対応するURLへリダイレクトする GrantedAuthority roleAdmin = new SimpleGrantedAuthority("ROLE_ADMIN"); if (authentication.getAuthorities().contains(roleAdmin)) { // 管理権限 ( ROLE_ADMIN ) を持っている場合には検索対象図書館登録画面へ遷移させる targetUrl = Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN; } return targetUrl; } }
LoginController.java
@RequestMapping public String index(HttpServletRequest request, HttpServletResponse response) { // 有効な remember-me Cookie が存在する場合にはログイン画面を表示させず自動ログインさせる TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices(WebSecurityConfig.REMEMBERME_KEY, userDetailsService); rememberMeServices.setCookieName("remember-me"); Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response); if (rememberMeAuth != null) { SecurityContextHolder.getContext().setAuthentication(rememberMeAuth); return "redirect:" + WebSecurityConfig.DEFAULT_SUCCESS_URL; } return "login"; }
return "redirect:" + WebSecurityConfig.DEFAULT_SUCCESS_URL;
→return "redirect:" + UrlAfterLoginHelper.getUrlAfterLogin(rememberMeAuth);
へ変更します。
build.gradle
dependencies { def jdbcDriver = "org.postgresql:postgresql:9.4-1201-jdbc41" // spring-boot-gradle-plugin によりバージョン番号が自動で設定されるもの // Appendix E. Dependency versions ( http://docs.spring.io/spring-boot/docs/current/reference/html/appendix-dependency-versions.html ) 参照 compile("org.springframework.boot:spring-boot-starter-web") compile("org.springframework.boot:spring-boot-starter-thymeleaf") compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity3") compile("org.springframework.boot:spring-boot-starter-data-jpa") compile("org.springframework.boot:spring-boot-starter-velocity") compile("org.springframework.boot:spring-boot-starter-mail") compile("org.springframework.boot:spring-boot-starter-security") compile("org.springframework.boot:spring-boot-starter-redis") compile("org.codehaus.janino:janino") testCompile("org.springframework.boot:spring-boot-starter-test") // (ここから) gradle でテストを実行した場合に spring-security-test-4.0.1.RELEASE.jar しか classpath に指定されず // テストが失敗したため、3.2.7.RELEASE を明記している testCompile("org.springframework.security:spring-security-core:3.2.7.RELEASE") testCompile("org.springframework.security:spring-security-web:3.2.7.RELEASE") // (ここまで) ------------------------------------------------------------------------------------------------------ testCompile("org.springframework.security:spring-security-test:4.0.1.RELEASE") testCompile("org.yaml:snakeyaml") // spring-boot-gradle-plugin によりバージョン番号が自動で設定されないもの compile("${jdbcDriver}") compile("org.seasar.doma:doma:2.3.1") compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16") compile("org.apache.commons:commons-lang3:3.4") compile("org.projectlombok:lombok:1.16.4") compile("com.google.guava:guava:18.0") compile("org.springframework.session:spring-session:1.0.1.RELEASE") compile("org.simpleframework:simple-xml:2.7.1") compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.6.1") compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.5.3") testCompile("org.dbunit:dbunit:2.5.1") testCompile("com.icegreen:greenmail:1.4.1") testCompile("org.assertj:assertj-core:3.1.0") testCompile("com.jayway.jsonpath:json-path:2.0.0") testCompile("org.jmockit:jmockit:1.19") // for Doma-Gen domaGenRuntime("org.seasar.doma:doma-gen:2.3.1") domaGenRuntime("${jdbcDriver}") }
testCompile("org.jmockit:jmockit:1.19")
を追加します。
TestDataResource.java
@Component public class TestDataResource extends ExternalResource { private final String TESTDATA_DIR = "src/test/resources/testdata/base"; private final String BACKUP_FILE_NAME = "ksbylending_backup"; private final List<String> BACKUP_TABLES = Arrays.asList( "user_info" , "user_role" , "library_forsearch" );
- BACKUP_TABLES の配列に "library_forsearch" を追加します。
testdata/base/table-ordering.txt, library_forsearch.csv
■table-ordering.txt
user_info user_role library_forsearch
- library_forsearch を追加します。
■library_forsearch.csv
systemid,formal
- 初期データは何も登録しないようにします。
LibraryHelperTest.java
package ksbysample.webapp.lending.helper.library; import ksbysample.webapp.lending.Application; import ksbysample.webapp.lending.dao.LibraryForsearchDao; import ksbysample.webapp.lending.entity.LibraryForsearch; import mockit.Delegate; import mockit.Injectable; import mockit.NonStrictExpectations; import mockit.Tested; 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.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class LibraryHelperTest { @Tested private LibraryHelper libraryHelper; @Injectable private LibraryForsearchDao libraryForsearchDao; @Test public void testGetSelectedLibrary_図書館が選択されていない場合() throws Exception { new NonStrictExpectations() {{ libraryForsearchDao.selectSelectedLibrary(); result = null; }}; String result = libraryHelper.getSelectedLibrary(); assertThat(result).isEqualTo("※図書館が選択されていません"); } @Test public void testGetSelectedLibrary_図書館が選択されている場合() throws Exception { new NonStrictExpectations() {{ libraryForsearchDao.selectSelectedLibrary(); result = new Delegate() { LibraryForsearch aDelegateMethod() { LibraryForsearch libraryForsearch = new LibraryForsearch(); libraryForsearch.setSystemid("System_Id"); libraryForsearch.setFormal("図書館名"); return libraryForsearch; } }; }}; String result = libraryHelper.getSelectedLibrary(); assertThat(result).isEqualTo("選択中:図書館名"); } }
- DB にデータをセットして取得するのではなく、libraryForsearchDao をモックにして selectSelectedLibrary メソッドの戻り値を変更してテストするようにしています。
private LibraryHelper libraryHelper;
は通常 @Autowired アノテーションを付加しますが、今回は内部のフィールドにモッククラスをインジェクションさせるので @Tested アノテーションを付加して JMockit にインスタンスを生成してもらいます。@Tested 以外に @Autowired も付加すると Spring の DIコンテナに生成された LibraryHelper クラスのシングルインスタンスにモックがインジェクションされてしまい他のテストが正常に動作しなくなるので要注意です。
UrlAfterLoginHelperTest.java
package ksbysample.webapp.lending.helper.url; import ksbysample.webapp.lending.Application; import ksbysample.webapp.lending.config.Constant; import ksbysample.webapp.lending.config.WebSecurityConfig; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class UrlAfterLoginHelperTest { @Test public void testGetUrlAfterLogin_管理権限がある場合() throws Exception { Authentication authentication = new TestingAuthenticationToken("test", "test", "ROLE_ADMIN", "ROLE_USER"); String url = UrlAfterLoginHelper.getUrlAfterLogin(authentication); assertThat(url).isEqualTo(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN); } @Test public void testGetUrlAfterLogin_ユーザ権限しかない場合() throws Exception { Authentication authentication = new TestingAuthenticationToken("test", "test", "ROLE_USER"); String url = UrlAfterLoginHelper.getUrlAfterLogin(authentication); assertThat(url).isEqualTo(WebSecurityConfig.DEFAULT_SUCCESS_URL); } }
SetSelectedLibraryForm_001.yaml
!!ksbysample.webapp.lending.web.admin.library.SetSelectedLibraryForm systemid: Tokyo_Test formal: テスト図書館
testdata/001/table-ordering.txt, library_forsearch.csv
■table-ordering.txt
library_forsearch
■library_forsearch.csv
systemid,formal Kanagawa_Sample,図書館サンプル
assertdata/001/table-ordering.txt, library_forsearch.csv
■table-ordering.txt
library_forsearch
■library_forsearch.csv
systemid,formal Tokyo_Test,テスト図書館
AdminLibraryServiceTest.java
package ksbysample.webapp.lending.web.admin.library; import ksbysample.common.test.TableDataAssert; import ksbysample.common.test.TestDataLoader; import ksbysample.common.test.TestDataLoaderResource; import ksbysample.common.test.TestDataResource; import ksbysample.webapp.lending.Application; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.csv.CsvDataSet; 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.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.yaml.snakeyaml.Yaml; import javax.sql.DataSource; import java.io.File; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class AdminLibraryServiceTest { // テストデータ private SetSelectedLibraryForm setSelectedLibraryForm_001 = (SetSelectedLibraryForm) new Yaml().load(getClass().getResourceAsStream("SetSelectedLibraryForm_001.yaml")); @Rule @Autowired public TestDataResource testDataResource; @Rule @Autowired public TestDataLoaderResource testDataLoaderResource; @Autowired private DataSource dataSource; @Autowired private AdminLibraryService adminLibraryService; @Test @TestDataLoader("src/test/resources/ksbysample/webapp/lending/web/admin/library/testdata/001") public void testDeleteAndInsertLibraryForSearch() throws Exception { adminLibraryService.deleteAndInsertLibraryForSearch(setSelectedLibraryForm_001); IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/lending/web/admin/library/assertdata/001")); TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource); tableDataAssert.assertEquals("library_forsearch", null); } }
- testDeleteAndInsertLibraryForSearch は以下の順でテストを実行します。
AdminLibraryControllerTest.java
package ksbysample.webapp.lending.web.admin.library; import ksbysample.common.test.*; import ksbysample.webapp.lending.Application; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.csv.CsvDataSet; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.yaml.snakeyaml.Yaml; import javax.sql.DataSource; import java.io.File; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @RunWith(Enclosed.class) public class AdminLibraryControllerTest { @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 検索対象図書館登録画面の初期表示のテスト { @Rule @Autowired public TestDataResource testDataResource; @Rule @Autowired public TestDataLoaderResource testDataLoaderResource; @Rule @Autowired public SecurityMockMvcResource mvc; @Test public void 管理権限を持つユーザは検索対象図書館登録画面を表示できる_図書館未選択時() throws Exception { mvc.authTanakaTaro.perform(get("/admin/library")) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(model().hasNoErrors()) .andExpect(xpath("//p[@class='navbar-text noselected-library']").string("※図書館が選択されていません")); } @Test @TestDataLoader("src/test/resources/ksbysample/webapp/lending/web/admin/library/testdata/001") public void 管理権限を持つユーザは検索対象図書館登録画面を表示できる_図書館選択時() throws Exception { mvc.authTanakaTaro.perform(get("/admin/library")) .andExpect(status().isOk()) .andExpect(content().contentType("text/html;charset=UTF-8")) .andExpect(model().hasNoErrors()) .andExpect(xpath("//p[@class='navbar-text selected-library']").string("選択中:図書館サンプル")); } @Test public void 管理権限のないユーザは検索対象図書館登録画面を表示できない() throws Exception { mvc.authSuzukiHanako.perform(get("/admin/library")) .andExpect(status().isForbidden()); } } @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public static class 検索ボタンクリック時のテスト { // テストデータ private SetSelectedLibraryForm setSelectedLibraryForm_001 = (SetSelectedLibraryForm) new Yaml().load(getClass().getResourceAsStream("SetSelectedLibraryForm_001.yaml")); @Rule @Autowired public TestDataResource testDataResource; @Rule @Autowired public TestDataLoaderResource testDataLoaderResource; @Autowired private DataSource dataSource; @Rule @Autowired public SecurityMockMvcResource mvc; @Test @TestDataLoader("src/test/resources/ksbysample/webapp/lending/web/admin/library/testdata/001") public void 管理権限を持つユーザが検索ボタンをクリックすると図書館を登録できる() throws Exception { mvc.authTanakaTaro.perform(TestHelper.postForm("/admin/library/addSearchLibrary", this.setSelectedLibraryForm_001).with(csrf())) .andExpect(status().isFound()) .andExpect(redirectedUrl("/admin/library")) .andExpect(model().hasNoErrors()); IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/lending/web/admin/library/assertdata/001")); TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource); tableDataAssert.assertEquals("library_forsearch", null); } } }
管理権限を持つユーザが検索ボタンをクリックすると図書館を登録できる()
テストメソッドでは.with(csrf())
を忘れないようにしましょう。これがないと POST でリクエストを送信した時に 403 が返ります。( 忘れていてちょっと苦労しました )
履歴
2015/09/15
初版発行。