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

かんがるーさんの日記

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

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その21 )( 検索対象図書館登録画面の作成3 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その20 )( 検索対象図書館登録画面の作成2 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 検索対象図書館登録画面 ( 管理者のみ ) の作成
      • 今回はテストを作成します。
      • 一部のテストで JMockit を使用します。久しぶりに使おうとしたらライブラリの仕様が以前と違う?

参照したサイト・書籍

  1. JMockit - Mocking http://jmockit.org/tutorial/Mocking.html

目次

  1. 作成済のテストをひと通り実行してみる
  2. テスト未作成のクラスを洗い出す
  3. JMockitを利用可能にする
  4. TestDataResource クラスの変更、testdata/base へのデータの追加
  5. LibraryHelper クラスのテストの作成
  6. UrlAfterLoginHelper クラスのテストの作成
  7. AdminLibraryService クラスのテストの作成
  8. AdminLibraryController クラスのテストの作成
  9. 全てのテストが成功するか確認する
  10. commit、Push、Pull Request、マージ

手順

作成済のテストをひと通り実行してみる

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

    テストが実行されますが、いくつかのテストが失敗しました。原因を確認します。

    f:id:ksby:20150912003808p:plain

  2. 原因は以下の2点でした。修正します。

    • 現在のテストではログイン後の URL は必ず /loginsuccess にしていますが、管理権限 ( ROLE_ADMIN ) を持つユーザはログイン後に /admin/library へ遷移するように変更したためでした。
    • /webapi/library/getLibraryList のレスポンスの JSON で formalName → formal に名称変更していたところで失敗していました。
  3. 最初にテストでも使用する URL を定数として定義します。src/main/java/ksbysample/webapp/lending/config の下の Constant.javaリンク先の内容 に変更します。

  4. src/test/java/ksbysample/webapp/lending/web の下の LoginControllerTest.javaリンク先のその1、その2、その3の内容 に変更します。

  5. src/test/java/ksbysample/webapp/lending/webapi/library の下の LibraryControllerTest.javaリンク先の内容 に変更します。

  6. 定義した定数を反映できるところに反映します。Ctrl+SHIFT+F を押して「Find in Path」ダイアログを表示し、"/admin/library" が書かれている箇所を検索します。

  7. src/main/java/ksbysample/webapp/lending/security の下の RoleAwareAuthenticationSuccessHandler.javaリンク先のその1の内容 に変更します。

  8. src/main/java/ksbysample/webapp/lending/web/admin/library の下の AdminLibraryController.javaリンク先の内容 に変更します。

  9. 再度 Project View のルートでコンテキストメニューを表示して「Run 'Tests in 'ksbysample...' with Coverage」を選択し、テストを実行します。

    まだ失敗したテストがあるので原因を確認します。

    f:id:ksby:20150912140024p:plain

  10. 原因は LoginController クラスの index メソッドに実装した有効な remember-me Cookie がある場合にはログイン画面を表示させずに自動ログインさせる処理で、付与された権限に関係なく固定で /loginsuccess にリダイレクトさせていたためでした。権限に応じたログイン後画面にリダイレクトさせるようにします。

  11. src/main/java/ksbysample/webapp/lending/helper の下に url パッケージを作成します。

  12. src/main/java/ksbysample/webapp/lending/helper/url の下に UrlAfterLoginHelper.java を作成します。作成後、リンク先の内容 に変更します。

  13. src/main/java/ksbysample/webapp/lending/security の下の RoleAwareAuthenticationSuccessHandler.javaリンク先のその2の内容 に変更します。

  14. src/main/java/ksbysample/webapp/lending/web の下の LoginController.javaリンク先の内容 に変更します。

  15. 再度テストを実行し、今度は全て成功しました。

    f:id:ksby:20150912143340p:plain

  16. 一旦 commit します。

テスト未作成のクラスを洗い出す

  1. 今回は以下のクラスのテストを作成します。

    • 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を利用可能にする

  1. 一部のテストは DB のデータをわざわざ変更する手間をかける程のものではないので、モックを作成してテストするようにします。モックのライブラリには JMockit を使用します。

  2. build.gradle を リンク先の内容 に変更します。

  3. Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

  4. classpath 内での jmockit.jar の位置を JUnit より前になるようにします。メイン画面のメニューから「File」-「Project Structure...」を選択します。

  5. 「Project Structure」ダイアログが表示されます。画面左側のリストから「Project Settings」-「Modules」を選択します。

  6. 画面右側で「Dependencies」タブを選択した後、jmockit のライブラリを junit の上に移動します。移動後「OK」ボタンをクリックしてダイアログを閉じます。

    f:id:ksby:20150912173400p:plain

TestDataResource クラスの変更、testdata/base へのデータの追加

  1. バックアップ対象のテーブルを追加します。src/test/java/ksbysample/common/test の下の TestDataResource.javaリンク先の内容 に変更します。

  2. src/test/resources/testdata/base の下に library_forsearch.csv を作成します。作成後、リンク先の内容 に変更します。

  3. src/test/resources/testdata/base の下の table-ordering.txt を リンク先の内容 に変更します。

LibraryHelper クラスのテストの作成

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

    f:id:ksby:20150912173855p:plain

    src/test/java/ksbysample/webapp/lending/helper/library の下に LibraryHelperTest.java が作成されます。

  2. src/test/java/ksbysample/webapp/lending/helper/library の下の LibraryHelperTest.javaリンク先の内容 に変更します。

  3. テストを実行します。LibraryHelperTest クラスのクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'LibraryHelperTest' with Coverage」を選択します。

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

    f:id:ksby:20150912195635p:plain

UrlAfterLoginHelper クラスのテストの作成

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

    f:id:ksby:20150912200248p:plain

    src/test/java/ksbysample/webapp/lending/helper/url の下に UrlAfterLoginHelperTest.java が作成されます。

  2. UrlAfterLoginHelper.getUrlAfterLogin には Authentication インターフェースを持つオブジェクトを渡しますが、Authentication インターフェースの実装クラスには何があるのか IntelliJ IDEA の Diagram 生成機能で調べて見ると以下の画像のクラス構成になっていました。システム稼働時は UsernamePasswordAuthenticationToken クラスが使用されていると思われますが、今回はテストが実行できればよいので TestingAuthenticationToken クラスを使用します。

    f:id:ksby:20150912201840p:plain

  3. src/test/java/ksbysample/webapp/lending/helper/url の下の UrlAfterLoginHelperTest.javaリンク先の内容 に変更します。

  4. テストを実行します。UrlAfterLoginHelperTest クラスのクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'UrlAfterLoginHelperTest' with Coverage」を選択します。

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

    f:id:ksby:20150912233005p:plain

AdminLibraryService クラスのテストの作成

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

    f:id:ksby:20150912233729p:plain

    src/test/java/ksbysample/webapp/lending/web/admin/library の下に AdminLibraryServiceTest.java が作成されます。

  2. テストで使用するデータを作成します。src/test/resources の下に ksbysample/webapp/lending/web/admin/library ディレクトリを作成します。

  3. src/test/resources/ksbysample/webapp/lending/web/admin/library の下に SetSelectedLibraryForm_001.yaml を作成します。作成後、リンク先の内容 に変更します。

  4. src/test/resources/ksbysample/webapp/lending/web/admin/library の下に testdata/001 ディレクトリを作成します。

  5. src/test/resources/ksbysample/webapp/lending/web/admin/library/testdata/001 の下に table-ordering.txt, library_forsearch.csv を作成します。作成後、リンク先の内容 に変更します。

  6. src/test/resources/ksbysample/webapp/lending/web/admin/library の下に assertdata/001 ディレクトリを作成します。

  7. src/test/resources/ksbysample/webapp/lending/web/admin/library/assertdata/001 の下に table-ordering.txt, library_forsearch.csv を作成します。作成後、リンク先の内容 に変更します。

  8. src/test/java/ksbysample/webapp/lending/web/admin/library の下の AdminLibraryServiceTest.javaリンク先の内容 に変更します。

AdminLibraryController クラスのテストの作成

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

    f:id:ksby:20150915014530p:plain

    src/test/java/ksbysample/webapp/lending/web/admin/library の下に AdminLibraryControllerTest.java が作成されます。

  2. src/test/java/ksbysample/webapp/lending/web/admin/library の下の AdminLibraryControllerTest.javaリンク先の内容 に変更します。

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

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

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

    f:id:ksby:20150915045926p:plain

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

    f:id:ksby:20150915050316p:plain

commit、Push、Pull Request、マージ

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

  2. コマンドラインから以下のコマンドを実行して commit を1つにまとめます。

    > git rebase -i HEAD~7
    > git commit --amend -m "#25 検索対象図書館登録画面を作成しました。"

  3. 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_URLConstant.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));
        }
  • 次回から自動的にログインするをチェックすれば次はログインしていなくてもログイン後の画面にアクセスできる() メソッドの以下の点を変更します。
    • メソッド内の WebSecurityConfig.DEFAULT_SUCCESS_URLConstant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN へ変更します。
    • メソッド内の "/loginsuccess"Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN へ変更します。
    • .andExpect(view().name("loginsuccess")) を削除します。

■その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_URLConstant.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 は以下の順でテストを実行します。
    1. @TestDataLoader アノテーションに指定したテストデータを DB にロードします。
    2. adminLibraryService.deleteAndInsertLibraryForSearch でデータを更新します。
    3. tableDataAssert.assertEquals で library_forsearch テーブルのデータが CSV ファイルの内容と同じかチェックします。

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
初版発行。