かんがるーさんの日記

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

Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その15 )( 登録画面作成4 )

概要

Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その14 )( 気になった点を修正 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 登録画面 ( 入力→確認→完了 ) のテストクラスを書きます。

ソフトウェア一覧

参考にしたサイト

  1. Preview Spring Security Test: Web Security
    http://spring.io/blog/2014/05/23/preview-spring-security-test-web-security

    • spring-security-test を使用したテストの書き方が記載されています。
  2. Integration Testing of Spring MVC Applications: Forms
    http://www.petrikainulainen.net/programming/spring-framework/integration-testing-of-spring-mvc-applications-forms/

    • MockMvc でフォームのデータを送信する実装方法の参考にしました。
  3. SPRING MVC INTEGRATION TESTING: ASSERT THE GIVEN MODEL ATTRIBUTE(S) HAVE GLOBAL ERRORS
    http://blog.codeleak.pl/2014/08/spring-mvc-test-assert-given-model-attribute-global-errors.html

    • Bean Validation のエラーチェックを実装する際に参考にしました。
    • FieldErrorsMatchers クラスを実装する際の参考にしました。
  4. コンピュータクワガタ - Spring MVC 3.2のSpring MVC Testを触った
    http://kuwalab.hatenablog.jp/entry/20130402/p1

    • MockMvc でのテストの書き方を参考にしました。
  5. Integration Testing of Spring MVC Applications: Controllers
    http://www.petrikainulainen.net/programming/spring-framework/integration-testing-of-spring-mvc-applications-controllers/

  6. 情報科学屋さんを目指す人のメモ - Java:クラスの全フィールドの名前と値の一覧を出力する方法
    http://did2memo.net/2013/09/10/java-reflection-how-to-display-all-java-field/

    • FieldErrorsMatchers クラスを実装する際の参考にしました。
  7. キャスレー技術ブログ - 【Java】Lombokで冗長コードを削減しよう
    http://www.casleyconsulting.co.jp/blog-engineer/java/%E3%80%90java%E3%80%91lombok%E3%81%A7%E5%86%97%E9%95%B7%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E5%89%8A%E6%B8%9B%E3%81%97%E3%82%88%E3%81%86/

    • @AllArgsConstructor, @NoArgsConstructor の使い方を参照しました。

手順

language level の設定変更

今回のテストクラスを作成する前に、現在の Project の language level が 8 ( Lambdas, type annotations etc. ) ではなく 6 ( @Override in interfaces ) になっていることに気づいたので変更します。

  1. IntelliJ IDEA のメニューから「File」-「Project Structure...」を選択します。

  2. 「Project Structure」ダイアログが表示されます。「Project language level」で「8 - Lambdas, type annotations etc.」を選択して「OK」ボタンをクリックします。

    f:id:ksby:20150306015634p:plain

テストクラス作成前の準備

  1. IntelliJ IDEA 上で 1.0.x-testcountryinput ブランチを作成します。

  2. テスト開始前に country テーブルのバックアップを取得し、テスト終了後にリストアされるようにします。src/test/java/ksbysample/webapp/basic/test の下の TestDataResource.javaリンク先の内容 に変更します。

  3. 今回のテストで使用する認証用・非認証用の MockMvc クラスのインスタンスを CountryControllerTest クラス内で生成せず、JUnit の ExternalResource を継承したクラスの中で生成します。こうすることで他のテストクラスでも共通で利用できるようにします。

    src/test/java/ksbysample/webapp/basic/test の下に SecurityMockMvcResource.java を新規作成します。作成後、リンク先の内容 に変更します。

  4. 標準ではフィールドのエラーの内容をチェックするクラス・メソッドがないようでしたので、チェック用のクラスを作成します。src/test/java/ksbysample/webapp/basic/test の下に FieldErrorsMatchers.java を新規作成します。作成後、リンク先の内容 に変更します。

    ※FieldErrorsMatchers クラスは、SPRING MVC INTEGRATION TESTING: ASSERT THE GIVEN MODEL ATTRIBUTE(S) HAVE GLOBAL ERRORS の記事の中にある GlobalErrorsMatchers クラス、及び org.springframework.test.web.servlet.result.ModelResultMatchers クラスを参考にして作成しました。

  5. Flash スコープのデータを Form クラスを渡してチェックできるようにしたいので、チェック用のクラスを作成します。src/test/java/ksbysample/webapp/basic/test の下に CustomFlashAttributeResultMatchers.java を新規作成します。作成後、リンク先の内容 に変更します。

  6. テストで使用するヘルパークラスを作成します。src/test/java/ksbysample/webapp/basic/test の下に TestHelper.java を新規作成します。作成後、リンク先の内容 に変更します。

登録画面 ( 入力→確認→完了 ) のテストクラスの作成

  1. CountryForm クラスにインストタンス生成時に値をセットするためのコンストラクタを作成します。src/main/java/ksbysample/webapp/basic/web の下の CountryForm.java をエディタで開き、リンク先の内容 に変更します。

  2. src/test/java/ksbysample/webapp/basic/web の下の CountryControllerTest.java をエディタで開き、リンク先の内容 に変更します。

    • テストデータを作成していて、MySQL絡みで以下の問題に気づきました。後で Bean Validation の定義を修正しましょう。
      • surfaceArea は MySQL では float(10,2) と定義されているのですが、小数点以下を .01 で登録しても .00 になります。0.01~0.50 は 0.00 に、0.51~0.99 が 1.00 になるようです。小数点を保存したいなら decimail(10,2) とする必要があるようで、今のままだと保存されないようです。
      • population は int(11) と定義されていたので 11桁の整数が入るのかと思ったのですが、int 型なので有効値は -2147483648 から 2147483647 でした。int(11) の 11 って意味がないですね。。。
  3. Project View の一番上の階層の ksbysample-webapp-basic を選択した後コンテキストメニューを表示し、「Run 'Tests in 'ksbysample...' with Coverage」を選択してテストを実行し、@Ignore アノテーションを付加していないテストが全て成功することを確認します。

    CountryController クラスの Coverage も 100% になっていることを確認します。

    f:id:ksby:20150308090927p:plain

commit、GitHub へ Push、1.0.x-testcountryinput -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-testcountryinput ブランチを削除

  1. commit の前に build タスクを実行し、BUILD SUCCESSFUL が表示されることを確認します。

  2. commit、GitHub へ Push、1.0.x-testcountryinput -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-testcountryinput ブランチを削除、をします。

    ※commit 時に Code Analysis のダイアログが表示されますが、Javadoc のコメントを記入していないのと、TestDataResource クラスの以前にも出力された Warning なので、無視して「Commit」ボタンをクリックします。

考察

  • テストで使用する MockMvc クラスのインスタンスは、Web で見かけるサンプルは個々のテストクラス内で生成している例が多いですが、開発では JUnit の ExternalResource を継承したクラスの中で生成して各テストクラスで利用するようにした方が使いやすいと思います。ExternalResource は使い方が分かると結構便利ですね。

  • 発生したエラー内容もテストメソッド内でチェックしたかったので FieldErrorsMatchers クラスを作成してみましたが、Web で MockMvc を使用したサンプルをいろいろ見た限りでは標準で用意されているメソッドを使用してエラーが発生しているフィールドのチェックを行うくらいで、発生したエラー内容のチェックまでやっているものはありませんでした。現時点ではエラー内容をチェックする方が個人的には好きですが、標準で用意されていないことからそこまでやる必要はないのかもしれません。

次回は。。。

  • 検索/一覧画面 ( MyBatis-Spring版 ) のテストクラスを書きます。

ソースコード

TestDataResource.java

    @Override
    protected void before() throws Throwable {
        IDatabaseConnection conn = null;
        try {
            conn = new DatabaseConnection(dataSource.getConnection());

            // バックアップを取得する
            QueryDataSet partialDataSet = new QueryDataSet(conn);
            partialDataSet.addTable("user");
            partialDataSet.addTable("user_role");
            partialDataSet.addTable("country");
            backupFile = File.createTempFile("world_backup", "xml");
            FlatXmlDataSet.write(partialDataSet, new FileOutputStream(backupFile));

            // テストデータに入れ替える
            IDataSet dataset = new CsvDataSet(new File("src/test/resources/testdata"));
            DatabaseOperation.CLEAN_INSERT.execute(conn, dataset);
        }
        finally {
            if (conn != null) conn.close();
        }
    }
  • partialDataSet.addTable("country"); を追加します。

SecurityMockMvcResource.java

package ksbysample.webapp.basic.test;

import org.junit.rules.ExternalResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import javax.servlet.Filter;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

@Component
public class SecurityMockMvcResource extends ExternalResource {

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private Filter springSecurityFilterChain;

    public MockMvc auth;

    public MockMvc nonauth;

    @Override
    protected void before() throws Throwable {
        // 認証ユーザ用MockMvc ( user = test, role = USER )
        this.auth = MockMvcBuilders.webAppContextSetup(this.context)
                .defaultRequest(get("/").with(user("test").roles("USER")))
                .addFilter(springSecurityFilterChain)
                .build();

        // 非認証用MockMvc
        this.nonauth = MockMvcBuilders.webAppContextSetup(this.context)
                .addFilter(springSecurityFilterChain)
                .build();
    }

}
  • 2種類の MockMvc ( 認証ユーザ用の auth、非認証用ユーザ用の nonahth ) を生成します。
  • どちらの MockMvc にも .addFilter(springSecurityFilterChain) を記述し、Spring Security が適用された状態にします。
  • 認証ユーザ用の auth では .defaultRequest(get("/").with(user("test").roles("USER"))) でユーザIDとロールを設定します。ログインする訳ではないのでパスワードは不要です。

FieldErrorsMatchers.java

package ksbysample.webapp.basic.test;

import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.result.ModelResultMatchers;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.servlet.ModelAndView;

import java.util.ArrayList;
import java.util.List;

import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertThat;
import static org.springframework.test.util.AssertionErrors.assertTrue;

public class FieldErrorsMatchers extends ModelResultMatchers {

    private FieldErrorsMatchers() {
    }

    public static FieldErrorsMatchers fieldErrors() {
        return new FieldErrorsMatchers();
    }

    public ResultMatcher hasFieldError(String name, String fieldName, String error) {
        return mvcResult -> {
            BindingResult bindingResult = getBindingResult(mvcResult.getModelAndView(), name);
            List<FieldError> fieldErrorList = bindingResult.getFieldErrors(fieldName);
            List<String> fieldErrorListAsCode = new ArrayList<>();
            for (FieldError fe : fieldErrorList) {
                fieldErrorListAsCode.add(fe.getCode());
            }
            assertThat("Expected error code '" + error + "'", fieldErrorListAsCode, hasItem(error));
        };
    }

    private BindingResult getBindingResult(ModelAndView mav, String name) {
        BindingResult bindingResult = (BindingResult) mav.getModel().get(BindingResult.MODEL_KEY_PREFIX + name);
        assertTrue("No BindingResult for attribute: " + name, bindingResult != null);
        return bindingResult;
    }

}
  • テストの処理内で .andExpect(fieldErrors().hasFieldError([モデル名], [フィールド名], [エラーになる Bean Validation 名 ( 例えば @NotNull なら NotNull を指定します )])) のフォーマットで使用します。

CustomFlashAttributeResultMatchers.java

package ksbysample.webapp.basic.test;

import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.result.FlashAttributeResultMatchers;

import java.lang.reflect.Field;

import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.util.MatcherAssertionErrors.assertThat;

public class CustomFlashAttributeResultMatchers extends FlashAttributeResultMatchers {

    public static CustomFlashAttributeResultMatchers flashEx() {
        return new CustomFlashAttributeResultMatchers();
    }

    public ResultMatcher attributes(final String name, Object form) {
        return mvcResult -> {
            for (Field field : form.getClass().getDeclaredFields()) {
                field.setAccessible(true);
                assertThat("Flash attribute", mvcResult.getFlashMap().get(name), hasProperty(field.getName(), is(field.get(form))));
            }
        };
    }

}

TestHelper.java

package ksbysample.webapp.basic.test;

import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;

import java.lang.reflect.Field;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

public class TestHelper {

    /**
     * Formクラスをパラメータに持つMockHttpServletRequestBuilderクラスのインスタンスを生成する
     * @param urlTemplate
     * @param form
     * @return
     * @throws IllegalAccessException
     */
    public static MockHttpServletRequestBuilder postForm(String urlTemplate, Object form) throws IllegalAccessException {
        MockHttpServletRequestBuilder request = post(urlTemplate).contentType(MediaType.APPLICATION_FORM_URLENCODED);
        for (Field field : form.getClass().getDeclaredFields()) {
            field.setAccessible(true);
            if (field.get(form) == null) {
                request = request.param(field.getName(), "");
            }
            else {
                request = request.param(field.getName(), field.get(form).toString());
            }
        }
        return request;
    }

    /**
     * EntityクラスとFormクラスの値が同じかチェックする
     * @param entity
     * @param form
     * @throws IllegalAccessException
     */
    public static void assertEntityByForm(Object entity, Object form) throws IllegalAccessException {
        for (Field entityField : entity.getClass().getDeclaredFields()) {
            entityField.setAccessible(true);
            try {
                Field formField = form.getClass().getDeclaredField(entityField.getName());
                formField.setAccessible(true);
                assertThat(entityField.get(entity), is(formField.get(form)));
            }
            catch (NoSuchFieldException ignored) {}
        }
    }

}

CountryForm.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CountryForm {
  • Lombok が提供する @AllArgsConstructor, @NoArgsConstructor アノテーションを付加します。@AllArgsConstructor だけだと引数なしのコンストラクタがなくなり他のクラスでコンパイルエラーが発生するため、@NoArgsConstructor も付加します。

CountryControllerTest.java

package ksbysample.webapp.basic.web;

import ksbysample.webapp.basic.Application;
import ksbysample.webapp.basic.domain.Country;
import ksbysample.webapp.basic.service.CountryRepository;
import ksbysample.webapp.basic.test.SecurityMockMvcResource;
import ksbysample.webapp.basic.test.TestDataResource;
import ksbysample.webapp.basic.test.TestHelper;
import org.apache.commons.lang3.StringUtils;
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.springframework.test.web.servlet.MvcResult;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.servlet.ModelAndView;

import java.math.BigDecimal;
import java.util.List;

import static ksbysample.webapp.basic.test.CustomFlashAttributeResultMatchers.flashEx;
import static ksbysample.webapp.basic.test.FieldErrorsMatchers.fieldErrors;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
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.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

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

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Rule
    @Autowired
    public SecurityMockMvcResource secmvc;

    @Autowired
    private CountryRepository countryRepository;

    // テストデータ
    private CountryForm countryFormSuccess = new CountryForm(
            StringUtils.repeat("x", 3)          // code
            , StringUtils.repeat("x", 52)       // name
            , "Asia"                            // continent
            , "Eastern Asia"                    // region
            , new BigDecimal("12345678.00")     // surfaceArea
            , 2147483647L                       // population
            , StringUtils.repeat("x", 45)       // localName
            , StringUtils.repeat("x", 45)       // governmentForm
            , StringUtils.repeat("x", 2)        // code2
    );
    private CountryForm countryFormEmpty = new CountryForm(
            ""                                  // code
            , ""                                // name
            , ""                                // continent
            , ""                                // region
            , null                              // surfaceArea
            , null                              // population
            , ""                                // localName
            , ""                                // governmentForm
            , ""                                // code2
    );
    private CountryForm countryFormSizeDigitsCheck = new CountryForm(
            StringUtils.repeat("x", 4)          // code
            , StringUtils.repeat("x", 53)       // name
            , "test"                            // continent
            , StringUtils.repeat("x", 27)       // region
            , new BigDecimal("12345678.000")    // surfaceArea
            , 123456789012L                     // population
            , StringUtils.repeat("x", 46)       // localName
            , StringUtils.repeat("x", 46)       // governmentForm
            , StringUtils.repeat("x", 3)        // code2
    );
    private CountryForm countryFormValidateError1 = new CountryForm(
            "JP2"                               // code
            , "Japan"                           // name
            , "Europe"                          // continent
            , "Eastern Asia"                    // region
            , new BigDecimal("1")               // surfaceArea
            , 2L                                // population
            , "Nippon"                          // localName
            , "test"                            // governmentForm
            , "JP"                              // code2
    );
    private CountryForm countryFormValidateError2 = new CountryForm(
            "JP2"                               // code
            , "Japan"                           // name
            , "Asia"                            // continent
            , "Southern Europe"                 // region
            , new BigDecimal("1")               // surfaceArea
            , 2L                                // population
            , "Nippon"                          // localName
            , "test"                            // governmentForm
            , "JP"                              // code2
    );

    @Test
    public void testInput() throws Exception {
        // 非認証時はログイン画面にリダイレクトされることを確認する
        secmvc.nonauth.perform(get("/country/input"))
                .andExpect(status().isFound())
                .andExpect(redirectedUrl("http://localhost/"));

        // 認証時は登録画面(入力)が表示されることを確認する
        secmvc.auth.perform(get("/country/input"))
                .andExpect(status().isOk())
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(view().name("country/input"));
    }

    @Test
    public void testInputBack() throws Exception {
        // 非認証時は403エラー(Forbidden)が返ることを確認する
        secmvc.nonauth.perform(post("/country/input/back"))
                .andExpect(status().isForbidden());

        // 認証時はフォームのデータがFlashスコープにセットされて、登録画面(入力)へリダイレクトされることを確認する
        secmvc.auth.perform(TestHelper.postForm("/country/input/back", this.countryFormSuccess)
                        .with(csrf())
        )
                .andExpect(status().isFound())
                .andExpect(redirectedUrl("/country/input"))
                .andExpect(flashEx().attributes("countryForm", this.countryFormSuccess));
    }

    @Test
    public void testConfirm() throws Exception {
        // 非認証時は403エラー(Forbidden)が返ることを確認する
        secmvc.nonauth.perform(post("/country/confirm"))
                .andExpect(status().isForbidden());

        // 認証時のテスト
        // NotBlank/NotNullの入力チェックのテスト
        MvcResult result = secmvc.auth.perform(TestHelper.postForm("/country/confirm", this.countryFormEmpty)
                        .with(csrf())
        )
                .andExpect(status().isOk())
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(view().name("country/input"))
                .andExpect(xpath("/html/head/title").string("Countryデータ登録画面(入力)"))
                .andExpect(model().hasErrors())
                .andExpect(model().errorCount(11))
                .andExpect(fieldErrors().hasFieldError("countryForm", "code", "NotBlank"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "name", "NotBlank"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "continent", "NotBlank"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "continent", "Pattern"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "region", "NotBlank"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "surfaceArea", "NotNull"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "population", "NotNull"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "localName", "NotBlank"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "governmentForm", "NotBlank"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "code2", "NotBlank"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "code2", "countryForm.code2.equalCode"))
                .andReturn();
        // 発生しているfield errorを全て出力するには以下のようにする
        ModelAndView mav = result.getModelAndView();
        BindingResult br = (BindingResult) mav.getModel().get(BindingResult.MODEL_KEY_PREFIX + "countryForm");
        List<FieldError> listFE = br.getFieldErrors();
        for (FieldError fe : listFE) {
            System.out.println("★★★ " + fe.getField() + " : " + fe.getCode() + " : " + fe.getDefaultMessage());
        }

        // Size/Pattern/Digitsの入力チェックのテスト
        secmvc.auth.perform(TestHelper.postForm("/country/confirm", this.countryFormSizeDigitsCheck)
                        .with(csrf())
        )
                .andExpect(status().isOk())
                .andExpect(model().hasErrors())
                .andExpect(model().errorCount(9))
                .andExpect(fieldErrors().hasFieldError("countryForm", "code", "Size"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "name", "Size"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "continent", "Pattern"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "region", "Size"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "surfaceArea", "Digits"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "population", "Digits"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "localName", "Size"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "governmentForm", "Size"))
                .andExpect(fieldErrors().hasFieldError("countryForm", "code2", "Size"))
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(view().name("country/input"))
                .andExpect(xpath("/html/head/title").string("Countryデータ登録画面(入力)"));

        // countryForm.continent.notAsia の入力チェックのテスト
        secmvc.auth.perform(TestHelper.postForm("/country/confirm", this.countryFormValidateError1)
                        .with(csrf())
        )
                .andExpect(status().isOk())
                .andExpect(model().hasErrors())
                .andExpect(model().errorCount(1))
                .andExpect(fieldErrors().hasFieldError("countryForm", "continent", "countryForm.continent.notAsia"))
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(view().name("country/input"))
                .andExpect(xpath("/html/head/title").string("Countryデータ登録画面(入力)"));

        // countryForm.region.notAsiaPattern の入力チェックのテスト
        secmvc.auth.perform(TestHelper.postForm("/country/confirm", this.countryFormValidateError2)
                        .with(csrf())
        )
                .andExpect(status().isOk())
                .andExpect(model().hasErrors())
                .andExpect(model().errorCount(1))
                .andExpect(fieldErrors().hasFieldError("countryForm", "region", "countryForm.region.notAsiaPattern"))
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(view().name("country/input"))
                .andExpect(xpath("/html/head/title").string("Countryデータ登録画面(入力)"));

        // データは問題なくても CSRFトークンがない場合には403エラー(Forbidden)が返ることを確認する
        secmvc.auth.perform(TestHelper.postForm("/country/confirm", this.countryFormSuccess))
                .andExpect(status().isForbidden());

        // データが問題なくCSRFトークンもある場合には登録画面(確認)が表示されることを確認する
        secmvc.auth.perform(TestHelper.postForm("/country/confirm", this.countryFormSuccess)
                        .with(csrf())
        )
                .andExpect(status().isOk())
                .andExpect(model().hasNoErrors())
                .andExpect(model().errorCount(0))
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(view().name("country/confirm"))
                .andExpect(xpath("/html/head/title").string("Countryデータ登録画面(確認)"));
    }

    @Test
    public void testUpdate() throws Exception {
        // 非認証時は403エラー(Forbidden)が返ることを確認する
        secmvc.nonauth.perform(post("/country/update"))
                .andExpect(status().isForbidden());

        // 認証時のテスト
        // データは問題なくても CSRFトークンがない場合には403エラー(Forbidden)が返ることを確認する
        secmvc.auth.perform(TestHelper.postForm("/country/update", this.countryFormSuccess))
                .andExpect(status().isForbidden());

        // CSRFトークンがあってもデータに問題がある場合には400エラー(Bad Request)が返ることを確認する
        secmvc.auth.perform(TestHelper.postForm("/country/update", this.countryFormSizeDigitsCheck)
                        .with(csrf())
        )
                .andExpect(status().isBadRequest());

        // データが問題なくCSRFトークンもある場合にはDBにデータが登録され、登録画面(完了)へリダイレクトされることを確認する
        secmvc.auth.perform(TestHelper.postForm("/country/update", this.countryFormSuccess)
                        .with(csrf())
        )
                .andExpect(status().isFound())
                .andExpect(redirectedUrl("/country/complete"))
                .andExpect(model().hasNoErrors())
                .andExpect(model().errorCount(0));

        Country country = countryRepository.findOne("xxx");
        assertThat(country, is(notNullValue()));
        TestHelper.assertEntityByForm(country, this.countryFormSuccess);
    }

    @Test
    public void testComplete() throws Exception {
        // 非認証時は403エラー(Forbidden)が返ることを確認する
        secmvc.nonauth.perform(post("/country/complete"))
                .andExpect(status().isForbidden());

        // 認証時は登録画面(完了)が表示されることを確認する
        secmvc.auth.perform(post("/country/complete")
                        .with(csrf())
        )
                .andExpect(status().isOk())
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(view().name("country/complete"))
                .andExpect(xpath("/html/head/title").string("Countryデータ登録画面(完了)"));
    }

}
  • いろいろ修正したのでポイントだけ記載します。
  • public SecurityMockMvcResource secmvc; を追加しています。テストで使用する MockMvc はこの中で定義されています。
  • DB に登録したデータを確認するために private CountryRepository countryRepository; を追加しています。テストなので実装中のクラスは使用せずに DbUnit 等を使用した方がよいのかもしれませんが、今回は簡単に対応するために Repository クラスを利用するようにしました。
  • テストで必要となるパターン分のデータを CountryForm クラス型のフィールドで定義しています。
  • perform メソッド内でデータを渡して画面を呼び出す場合には TestHelper.postForm を利用して、Form クラスを渡せるようにしています。
  • Bean Validation が実行されるところでは、エラーがない場合には .andExpect(model().hasNoErrors()) を、エラーがある場合には .andExpect(model().hasErrors()) を記述しています。また .andExpect(model().errorCount([入力チェックエラー数])) も記述して入力チェックエラー数もチェックするようにしています。
  • 入力チェックエラー数が 1以上ある場合には、入力チェックエラー数分 .andExpect(fieldErrors().hasFieldError(...)) を記述してエラー内容をチェックしています。

履歴

2015/03/08
初版発行。