# 10.2.2. レイヤごとのテスト実装

レイヤごとの単体テスト対象クラス、テスト方法およびその概要の一覧を以下に示す。

なお、本章で提示するテスト方法および実装はあくまで一例であり、実際はテスト方針に合わせたテスト方法および実装を検討いただきたい。

レイヤ | テスト方法 | 概要
--- | --- | ---
インフラストラクチャ層 | Spring Test標準機能のみを利用したテスト | Spring Testの標準的な機能を使用してデータアクセスのテストを行う。
インフラストラクチャ層 | Spring Test DBUnitを利用したテスト | DBUnitとSpring Test DBUnitの機能を使用してデータアクセスのテストを行う。
ドメイン層 | 依存クラスを利用したテスト | Spring TestのDI機能を使用して`Service`をインジェクションし、インフラストラクチャ層と結合し`Service`のテストを行う。
ドメイン層 | モックを利用したテスト | Mockitoを使用して依存するクラスをモック化し`Service`のテストを行う。
アプリケーション層 | StandaloneSetupを利用したテスト | Spring TestのDI機能を使用して`Controller`をインジェクションし、ドメイン層、インフラストラクチャ層と結合した状態で`Controller`のテストを行う。
アプリケーション層 | WebAppContextSetupを利用したテスト | Spring TestのMockMvcを使用して業務で作成した`spring-mvc.xml`と`applicationContext.xml`を適用し、`Controller`のテストを行う。
アプリケーション層 | モックを利用したテスト | Mockitoを使用して依存するクラスをモック化し`Controller`のテストを行う。
アプリケーション層 | Helperの単体テスト | `Helper`のテストを行う。テスト方法の詳細は`Service`のテスト方法を参照。

## 10.2.2.1. インフラストラクチャ層の単体テスト

本節では、開発ガイドラインのインフラストラクチャ層の単体テストについて説明する。

インフラストラクチャ層では、RepositoryからMyBatis（O/R Mapper）を利用したデータアクセスのテストを行う。
MyBatis3の使用方法の詳細については、MyBatis3を使ってRepositoryを実装を参照されたい。
MyBatisにより自動生成される`RepositoryImpl`はSpringのDIコンテナ上で実行されるため、テストには、本番同様のBean定義と、SpringのDI機能を提供するSpring Testの`SpringJUnit4ClassRunner`を使用する。
Spring Testの詳細はSpring Testを参照されたい。
テスト実行後のデータ検証方法には以下の2通りある。
どちらを使用するかは別途業務要件に合わせて検討いただきたい。

- テスト実行後のデータベースの状態をSELECT文を使用して取得し検証する。

- DBUnitとSpring Test DBUnitを使用して検証する。

本節では、SELECT文を使用した検証方法として`JdbcTemplate`を使用した場合を例に説明する。
`JdbcTemplate`とはSpring JDBCサポートのコアクラスである。JDBC APIではデータソースからコネクションの取得、`PreparedStatement`の作成、`ResultSet`の解析、コネクションの解放などを行う必要があるが、`JdbcTemplate`を使用することでこれらの処理の多くが隠蔽され、より簡単にデータアクセスを行うことができる。

Note

アプリケーションのレイヤ化では、`Repository`インターフェイスはドメイン層の成果物であるが、インフラストラクチャ層の単体テスト対象として紹介している。`Service`とのインターフェイスが正しいことは、ドメイン層の単体テストでも確認することを推奨する。

### 10.2.2.1.1. Repositoryの単体テスト

本節では、以下の`Repository`の単体テスト実装方法を説明する。

テスト方法 | 説明
--- | ---
Spring Test標準機能のみを利用したテスト | `JdbcTemplate`を使用してテスト結果の検証を行う。
Spring Test DBUnitを利用したテスト | DBUnit、Spring Test DBUnitの機能を使用してテスト結果の検証を行う。

ここでは、以下の成果物に対するテストを例に説明する。
なお、Repositoryの実装の詳細は、MyBatis3を使ってRepositoryを実装を参照されたい。

- `Repository`インタフェース（`MemberRepository`）の更新処理（`updateMemberLogin`メソッド）

- マッピングファイル（`MemberRepository.xml`）

以下に、テスト対象の実装例を示す。

- `MemberRepository.java`

    public interface MemberRepository {

        int updateMemberLogin(Member member);
    }

- `MemberRepository.xml`

    <mapper namespace="com.example.domain.repository.member.MemberRepository">

      <update id="updateMemberLogin" parameterType="Member">
        UPDATE member_login SET
            last_password = password,
            password = #{memberLogin.password}
        WHERE
            customer_no = #{membershipNumber}
      </update>

    </mapper>

#### 10.2.2.1.1.1. Spring Test標準機能のみを利用したテスト

Spring Testを使用した`Repository`の単体テストにおいて、作成するファイルを以下に示す。
なお、データベースのセットアップ方法についてはスキーマとテストデータのセットアップ（Spring Test標準機能のみを利用したテストの場合） を参照されたい。
また、Spring Testを使用して単体テストを行う際に使用する設定ファイルはテスト実装例で使用する設定ファイルを参照されたい。

作成するファイル名 | 説明
--- | ---
`MemberRepositoryTest.java` | `MemberRepository.java`のテストクラス。
`TestContextConfig.java` | Spring Testを使用して単体テストを行う際に必要な設定を補うための設定ファイル。
`setupMemberLogin.sql` | 単体テストで利用するデータベースのデータをセットアップするためのSQLファイル。

Note

単体テストで利用するSQLファイルの作成単位

ここでは、1テストメソッドに1つのSQLを作成している。実際の作成単位については、テスト方針や内容に応じて適宜検討されたい。なお、`@Sql`にSQLファイルパスを省略した場合、`@Sql`の指定場所に基づいてSQLファイルの検索が行われる。

詳細は、@SqlのSQLファイルパスの省略を参照されたい。

Spring Testを使用する場合の`Repository`のテストクラス作成方法を説明する。

以下に、データアクセスを利用してテストするために使用する設定ファイルを示す。

- `SampleInfraConfig.java`

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer bean = new MapperScannerConfigurer();
        bean.setBasePackage("com.example.domain.repository");
        return bean;
    }

    @Bean("sqlSessionFactory")
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setConfiguration(MybatisConfig.configuration());
        return bean;
    }

- `SampleEnvConfig.java`

    /**
     * DataSource.driverClassName property.
     */
    @Value("${database.driverClassName}")
    private String driverClassName;

    /**
     * DataSource.url property.
     */
    @Value("${database.url}")
    private String url;

    /**
     * DataSource.username property.
     */
    @Value("${database.username}")
    private String username;

    /**
     * DataSource.password property.
     */
    @Value("${database.password}")
    private String password;

    /**
     * DataSource.maxTotal property.
     */
    @Value("${cp.maxActive}")
    private Integer maxActive;

    /**
     * DataSource.maxIdle property.
     */
    @Value("${cp.maxIdle}")
    private Integer maxIdle;

    /**
     * DataSource.minIdle property.
     */
    @Value("${cp.minIdle}")
    private Integer minIdle;

    /**
     * DataSource.maxWaitMillis property.
     */
    @Value("${cp.maxWait}")
    private Integer maxWait;

    /**
     * Property databaseName.
     */
    @Value("${database}")
    private String database;

    /**
     * Configure {@link TransactionManager} bean.
     * @param dataSource Bean defined by #dataSource()
     * @see #dataSource()
     * @return Bean of configured {@link DataSourceTransactionManager}
     */
    @Bean("transactionManager")
    public TransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager bean = new DataSourceTransactionManager();
        bean.setDataSource(dataSource);
        return bean;
    }

    /**
     * Configure {@link ClockFactory} bean.
     * @return Bean of configured {@link DefaultClockFactory}
     */
    @Bean("dateFactory")
    public ClockFactory dateFactory() {
        return new DefaultClockFactory();
    }

    /**
     * Configure {@link DataSource} bean.
     * @return Bean of configured {@link BasicDataSource}
     */
    @Bean(name = "dataSource", destroyMethod = "close")
    public DataSource dataSource() {
        BasicDataSource bean = new BasicDataSource();
        bean.setDriverClassName(driverClassName);
        bean.setUrl(url);
        bean.setUsername(username);
        bean.setPassword(password);
        bean.setDefaultAutoCommit(false);
        bean.setMaxTotal(maxActive);
        bean.setMaxIdle(maxIdle);
        bean.setMinIdle(minIdle);
        bean.setMaxWait(Duration.ofMillis(maxWait));
        return bean;
    }

作成するファイル名 | 説明
--- | ---
`MemberRepositoryTest.java` | `MemberRepository.java`のテストクラス。
`test-context.xml` | Spring Testを使用して単体テストを行う際に必要な設定を補うための設定ファイル。
`setupMemberLogin.sql` | 単体テストで利用するデータベースのデータをセットアップするためのSQLファイル。

Note

単体テストで利用するSQLファイルの作成単位

ここでは、1テストメソッドに1つのSQLを作成している。実際の作成単位については、テスト方針や内容に応じて適宜検討されたい。なお、`@Sql`にSQLファイルパスを省略した場合、`@Sql`の指定場所に基づいてSQLファイルの検索が行われる。

詳細は、@SqlのSQLファイルパスの省略を参照されたい。

Spring Testを使用する場合の`Repository`のテストクラス作成方法を説明する。

以下に、データアクセスを利用してテストするために使用する設定ファイルを示す。

- `sample-infra.xml`

    <import resource="classpath:/META-INF/spring/sample-env.xml" />

    <!-- define the SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
      <property name="dataSource" ref="dataSource" />
      <property name="configLocation" value="classpath:/META-INF/mybatis/mybatis-config.xml" />
    </bean>

    <!-- scan for Mappers -->
    <mybatis:scan base-package="com.example.domain.repository" />

- `sample-env.xml`

    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
      <property name="driverClassName" value="org.postgresql.Driver" />
      <property name="url" value="jdbc:postgresql://localhost:5432/sample" />
      <property name="username" value="sample" />
      <property name="password" value="xxxx" />
      <property name="defaultAutoCommit" value="false" />
      <property name="maxTotal" value="96" />
      <property name="maxIdle" value="16" />
      <property name="minIdle" value="0" />
      <property name="maxWait" >
        <bean class="java.time.Duration" factory-method="ofMillis">
          <constructor-arg value="60000" />
        </bean>
      </property>
    </bean>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
      <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="dateFactory" class="org.terasoluna.gfw.common.time.DefaultClockFactory" />

以下に、Spring Testを使用した`Repository`のテスト作成方法について説明する。
ここでは、テスト用のスキーマは作成済みであることを前提に、`@Sql`アノテーションを使用して`MemberLogin`テーブルをセットアップし、`MemberLogin`のパスワード「ABCDE」が新しいパスワード「FGHIJ」に更新されることを更新後の`MemberLogin`テーブルを取得して確認している。

- `MemberRepositoryTest.java`

    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = { TestContextConfig.class, SampleEnvConfig.class, SampleInfraConfig.class }) // (1)
    @Transactional // (2)
    public class MemberRepositoryTest {

        @Inject
        MemberRepository target; // (3)

        @Inject
        JdbcTemplate jdbctemplate; // (4)

        @Test
        @Sql(scripts = "classpath:META-INF/sql/setupMemberLogin.sql", config = @SqlConfig(encoding = "utf-8"))
        public void testUpdateMemberLogin() {

            // (5)
            // setup test data
            MemberLogin memberLogin = new MemberLogin();
            memberLogin.setPassword("FGHIJ");
            Member member = new Member();
            member.setMembershipNumber("0000000001");
            member.setMemberLogin(memberLogin);

            // (6)
            // run the test
            int updateCounts = target.updateMemberLogin(member);

            // (7)
            MemberLogin updateMemberLogin = getMemberLogin("0000000001");

            // (8)
            // assertion
            assertThat(updateCounts, is(1));
            assertThat(updateMemberLogin.getPassword(), is("FGHIJ"));
            assertThat(updateMemberLogin.getLastPassword(), is("ABCDE"));
        }

        private Member getMemberLogin(String customerNo) {

            MemberLogin memberLogin = jdbctemplate.queryForObject(
                    "SELECT * FROM member_login WHERE customer_no=?",
                    new RowMapper<MemberLogin>() {

                        public MemberLogin mapRow(ResultSet rs,
                                    int rowNum) throws SQLException {

                                MemberLogin mapMemberLogin = new MemberLogin();

                                mapMemberLogin.setPassword(rs.getString(
                                        "password"));
                                mapMemberLogin.setLastPassword(rs.getString(
                                        "last_password"));
                                mapMemberLogin.setLoginDateTime(rs.getDate(
                                        "login_date_time"));
                                mapMemberLogin.setLoginFlg(rs.getBoolean(
                                        "login_flg"));

                                return mapMemberLogin;
                        }
                    }, customerNo);

            return memberLogin;
        }

項番 | 説明
--- | ---
(1) | `MemberRepository`クラスを動作させるために必要なアプリケーションが保持する`TestContextConfig.class`、`SampleEnvConfig.class`と`SampleInfraConfig.class`を読み込む。
(2) | `@Transactional`アノテーションを付与すると、テスト実行開始から終了まで一トランザクションとなり、デフォルトではテスト終了後にロールバックされる。クラスレベルでアノテーションを定義すると、全テストメソッドに対して`@Transactional`アノテーションが有効になる。
(3) | テスト対象である`MemberRepository`クラスをインジェクションする。
(4) | `JdbcTemplate`クラスをインジェクションする。
(5) | テスト対象メソッドを実行するためのテストデータを作成する。
(6) | テスト対象メソッドを実行する。
(7) | 更新後のデータベースの情報を取得する。`org.springframework.jdbc.core.RowMapper<T>`を使用することで、データベースから取得した`ResultSet`を特定のPOJOクラスにマッピングすることができる。
(8) | 更新件数、更新結果を確認する。

- `MemberRepositoryTest.java`

    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = {
            "classpath:META-INF/spring/sample-infra.xml",   // (1)
            "classpath:META-INF/spring/test-context.xml" }) // (1)
    @Transactional // (2)
    public class MemberRepositoryTest {

        @Inject
        MemberRepository target; // (3)

        @Inject
        JdbcTemplate jdbctemplate; // (4)

        @Test
        @Sql(scripts = "classpath:META-INF/sql/setupMemberLogin.sql", config = @SqlConfig(encoding = "utf-8"))
        public void testUpdateMemberLogin() {

            // (5)
            // setup test data
            MemberLogin memberLogin = new MemberLogin();
            memberLogin.setPassword("FGHIJ");
            Member member = new Member();
            member.setMembershipNumber("0000000001");
            member.setMemberLogin(memberLogin);

            // (6)
            // run the test
            int updateCounts = target.updateMemberLogin(member);

            // (7)
            MemberLogin updateMemberLogin = getMemberLogin("0000000001");

            // (8)
            // assertion
            assertThat(updateCounts, is(1));
            assertThat(updateMemberLogin.getPassword(), is("FGHIJ"));
            assertThat(updateMemberLogin.getLastPassword(), is("ABCDE"));
        }

        private Member getMemberLogin(String customerNo) {

            MemberLogin memberLogin = jdbctemplate.queryForObject(
                    "SELECT * FROM member_login WHERE customer_no=?",
                    new RowMapper<MemberLogin>() {

                        public MemberLogin mapRow(ResultSet rs,
                                    int rowNum) throws SQLException {

                                MemberLogin mapMemberLogin = new MemberLogin();

                                mapMemberLogin.setPassword(rs.getString(
                                        "password"));
                                mapMemberLogin.setLastPassword(rs.getString(
                                        "last_password"));
                                mapMemberLogin.setLoginDateTime(rs.getDate(
                                        "login_date_time"));
                                mapMemberLogin.setLoginFlg(rs.getBoolean(
                                        "login_flg"));

                                return mapMemberLogin;
                        }
                    }, customerNo);

            return memberLogin;
        }

項番 | 説明
--- | ---
(1) | `MemberRepository`クラスを動作させるために必要なアプリケーションが保持する`sample-infra.xml`と`test-context.xml`を読み込む。
(2) | `@Transactional`アノテーションを付与すると、テスト実行開始から終了まで一トランザクションとなり、デフォルトではテスト終了後にロールバックされる。クラスレベルでアノテーションを定義すると、全テストメソッドに対して`@Transactional`アノテーションが有効になる。
(3) | テスト対象である`MemberRepository`クラスをインジェクションする。
(4) | `JdbcTemplate`クラスをインジェクションする。
(5) | テスト対象メソッドを実行するためのテストデータを作成する。
(6) | テスト対象メソッドを実行する。
(7) | 更新後のデータベースの情報を取得する。`org.springframework.jdbc.core.RowMapper<T>`を使用することで、データベースから取得した`ResultSet`を特定のPOJOクラスにマッピングすることができる。
(8) | 更新件数、更新結果を確認する。

Note

テスト時のトランザクションをロールバックさせない方法

`@Transactional`アノテーションをテストケースに指定した場合、デフォルトでテストメソッド実行後にロールバックされる。

後続のテストでテストデータを使用するなどの目的でロールバックをさせたくない場合は、`@Transactional`アノテーションに加えて`@Rollback(false)`アノテーションまたは`@Commit`アノテーションを指定することで、テスト時のトランザクションをコミットすることができる。

Warning

Spring Framework 4.2 以降の@TransactionConfigurationについて

Spring Framework 4.2 以降、クラスレベルで`@Rollback`または`@Commit`の設定が可能となった。

これに伴い`@TransactionConfiguration`が非推奨となった。但し、Spring Framework 4.2 より前のバージョンでクラスレベルでロールバックをする場合は`@TransactionConfiguration(defaultRollback = true)`を設定すること。

#### 10.2.2.1.1.2. Spring Test DBUnitを利用したテスト

データアクセスにDBUnitを使用する場合の`Repository`の単体テスト実装方法について説明する。
なお、ここではDBUnitのデータ定義ファイルにExcel形式（.xlsx）のファイルを使用した場合を例に説明する。
データ定義ファイルとデータベースのセットアップ方法については、テストデータのセットアップ（Spring Test DBUnitを利用したテスト場合）を参照されたい。
また、DBUnitにSpring Test DBUnitの機能を組み合わせて使用するには、`@TestExecutionListeners`アノテーションを使って、`com.github.springtestdbunit.TransactionDbUnitTestExecutionListener`を登録する必要がある。
登録方法ついては、TestExecutionListenerの登録を参照されたい。

DBUnitを利用した`Repository`の単体テストにおいて、作成するファイルを以下に示す。

作成するファイル名 | 説明
--- | ---
`MemberRepositoryDbunitTest.java` | `MemberRepository.java`のテストクラス(DBUnitと連携する場合)
`XlsDataSetLoader.java` | Excel形式に対応する`DataSetLoader`インタフェースの実装クラス。 実装方法については、テストデータのセットアップ（Spring Test DBUnitを利用したテスト場合）を参照されたい。
`expected_testUpdateMemberLogin.xlsx` | テストの期待結果検証用ファイル
`setup_MemberLogin.xlsx` | テストデータセットアップ用ファイル
`TestContextConfig.java` | Spring Testを使用して単体テストを行う際に使用する設定ファイル。Spring Test標準機能のみを利用したテストで作成した設定ファイルと同じものを使用する。

Note

単体テストで利用するExcelファイルの作成単位

ここでは、１テストメソッドにデータセットアップ用のファイルと期待結果検証用のファイルをそれぞれ１つずつ作成している。

実際の作成単位については、テスト方針や内容に応じて適宜検討されたい。

DBUnitを使用する場合の`Repository`のテストクラス作成方法を説明する。

ここでは、テスト用のスキーマは作成済みであることを前提に、`@DatabaseSetup`アノテーションを使用して`MemberLogin`テーブルをセットアップし、`MemberLogin`のパスワード「ABCDE」が新しいパスワード「FGHIJ」に更新されることを`@ExpectedDatabase`アノテーションを使用して確認している。

以下に、Spring TestとDBUnitを使用した`Repository`のテスト作成方法を説明する。

- `MemberRepositoryDbunitTest.java`

    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = { TestContextConfig.class, SampleEnvConfig.class, SampleInfraConfig.class }) // (1)
    @TestExecutionListeners({
            DirtiesContextBeforeModesTestExecutionListener.class,
            DependencyInjectionTestExecutionListener.class,
            DirtiesContextTestExecutionListener.class,
            TransactionDbUnitTestExecutionListener.class})
    @Transactional
    @DbUnitConfiguration(dataSetLoader = XlsDataSetLoader.class)
    public class MemberRepositoryDbunitTest {

        @Inject
        MemberRepository target;

        @Test
        @DatabaseSetup("classpath:META-INF/dbunit/setup_MemberLogin.xlsx")
        @ExpectedDatabase( // (2)
                value = "classpath:META-INF/dbunit/expected_testUpdateMemberLogin.xlsx",
                assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED)
        public void testUpdate() {

            // setup
            MemberLogin memberLogin = new MemberLogin();
            memberLogin.setPassword("FGHIJ");
            Member member = new Member();
            member.setMembershipNumber("0000000001");
            member.setMemberLogin(memberLogin);

            // run the test
            int updateCounts = target.updateMemberLogin(member);

            // assertion
            assertThat(updateCounts, is(1));
        }
    }

項番 | 説明
--- | ---
(1) | `MemberRepository`クラスを動作させるために必要な設定ファイル（アプリケーションが保持する`SampleInfraConfig.class`とそれを補う`TestContextConfig.class`）を読み込む。
(2) | `@ExpectedDatabase`アノテーションにテストの期待結果検証用ファイルを指定することでテストメソッド実行後にDBUnitによってテーブルと期待結果データファイルが自動で比較検証される。 `@DatabaseSetup`アノテーション同様に、クラスレベルとメソッドレベルで付与できる。 ファイルフォーマットはテストセットアップ用データファイルと同じである。`assertionMode`属性には、以下の値が設定可能である。 `DEFAULT`（または指定なし）：全てのテーブルとカラムの一致を比較する。 `NON_STRICT`：期待結果データファイルに存在しないテーブル、カラムが実際のデータベースに存在しても無視する。 `NON_STRICT_UNORDERED`：`NON_STRICT`モードに加え、行の順序についても無視する。

作成するファイル名 | 説明
--- | ---
`MemberRepositoryDbunitTest.java` | `MemberRepository.java`のテストクラス(DBUnitと連携する場合)
`XlsDataSetLoader.java` | Excel形式に対応する`DataSetLoader`インタフェースの実装クラス。実装方法については、テストデータのセットアップ（Spring Test DBUnitを利用したテスト場合）を参照されたい。
`expected_testUpdateMemberLogin.xlsx` | テストの期待結果検証用ファイル
`setup_MemberLogin.xlsx` | テストデータセットアップ用ファイル
`test-context.xml` | Spring Testを使用して単体テストを行う際に使用する設定ファイル。Spring Test標準機能のみを利用したテストで作成した設定ファイルと同じものを使用する。

Note

単体テストで利用するExcelファイルの作成単位

ここでは、１テストメソッドにデータセットアップ用のファイルと期待結果検証用のファイルをそれぞれ１つずつ作成している。

実際の作成単位については、テスト方針や内容に応じて適宜検討されたい。

DBUnitを使用する場合の`Repository`のテストクラス作成方法を説明する。

ここでは、テスト用のスキーマは作成済みであることを前提に、`@DatabaseSetup`アノテーションを使用して`MemberLogin`テーブルをセットアップし、`MemberLogin`のパスワード「ABCDE」が新しいパスワード「FGHIJ」に更新されることを`@ExpectedDatabase`アノテーションを使用して確認している。

以下に、Spring TestとDBUnitを使用した`Repository`のテスト作成方法を説明する。

- `MemberRepositoryDbunitTest.java`

    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = {
            "classpath:META-INF/spring/sample-infra.xml",   // (1)
            "classpath:META-INF/spring/test-context.xml" }) // (1)
    @TestExecutionListeners({
            DirtiesContextBeforeModesTestExecutionListener.class,
            DependencyInjectionTestExecutionListener.class,
            DirtiesContextTestExecutionListener.class,
            TransactionDbUnitTestExecutionListener.class})
    @Transactional
    @DbUnitConfiguration(dataSetLoader = XlsDataSetLoader.class)
    public class MemberRepositoryDbunitTest {

        @Inject
        MemberRepository target;

        @Test
        @DatabaseSetup("classpath:META-INF/dbunit/setup_MemberLogin.xlsx")
        @ExpectedDatabase( // (2)
                value = "classpath:META-INF/dbunit/expected_testUpdateMemberLogin.xlsx",
                assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED)
        public void testUpdate() {

            // setup
            MemberLogin memberLogin = new MemberLogin();
            memberLogin.setPassword("FGHIJ");
            Member member = new Member();
            member.setMembershipNumber("0000000001");
            member.setMemberLogin(memberLogin);

            // run the test
            int updateCounts = target.updateMemberLogin(member);

            // assertion
            assertThat(updateCounts, is(1));
        }
    }

項番 | 説明
--- | ---
(1) | `MemberRepository`クラスを動作させるために必要な設定ファイル（アプリケーションが保持する`sample-infra.xml`とそれを補う`test-context.xml`）を読み込む。
(2) | `@ExpectedDatabase`アノテーションにテストの期待結果検証用ファイルを指定することでテストメソッド。実行後にDBUnitによってテーブルと期待結果データファイルが自動で比較検証される。 `@DatabaseSetup`アノテーション同様に、クラスレベルとメソッドレベルで付与できる。 ファイルフォーマットはテストセットアップ用データファイルと同じである。`assertionMode`属性には、以下の値が設定可能である。 `DEFAULT`（または指定なし）：全てのテーブルとカラムの一致を比較する。 `NON_STRICT`：期待結果データファイルに存在しないテーブル、カラムが実際のデータベースに存在しても無視する。 `NON_STRICT_UNORDERED`：`NON_STRICT`モードに加え、行の順序についても無視する。

Warning

外部キー制約のあるテーブル

外部キー制約のあるテーブルに対し、DBUnitを用いてデータベースを初期化すると、参照条件によってはエラーが発生するため、参照整合性を保つようにデータセットの順序を指定する必要があることに注意されたい。

Note

シーケンスの検証方法

シーケンスは、トランザクションをロールバックしても進んだ値は戻らないという特徴を持つ。そのため、シーケンスから採番したカラムを持つレコードをDBUnitで検証する場合、以下のいずれかの対応を行う必要がある。

- シーケンスから採番したカラムは検証対象外とする

- 明示的にシーケンスの初期化を行うSQLを実行し、テストの実施前に初期化する

- テスト実行時にシーケンスの値を確認し、確認した値を基準値として検証を行う

## 10.2.2.2. ドメイン層の単体テスト

本節では、開発ガイドラインのドメイン層の単体テストについて説明する。

ドメイン層では、`Service`の業務ロジックと`@Transactional`のテストを行う。
`Service`をインジェクションし、インフラストラクチャ層を結合してテストを行う場合は、`Repository`のテスト実装方法と同様にBean定義と、Spring Testの`SpringJUnit4ClassRunner`を使用してテストを行う。
Spring Testの詳細はSpring Testを参照されたい。

### 10.2.2.2.1. Serviceの単体テスト

本節では、以下の`Service`のテスト実装方法を説明する。

テスト方法 | 説明
--- | ---
依存クラスを利用したテスト | `Service`をインジェクションし、インフラストラクチャ層と結合してテストを行う。
モックを利用したテスト | `Service`の実装クラスが依存するクラスをすべてモック化してテストを行う。

ここでは、以下の成果物に対するテストを例に説明する。
なお、Serviceの実装の詳細は、Serviceの実装を参照されたい。

- Serviceの実装クラス（`TicketReserveServiceImpl`）

以下に、テスト対象の実装例を示す。

- `TicketReserveServiceImpl.java`

    @Service
    @Transactional
    public class TicketReserveServiceImpl implements TicketReserveService {

        @Inject
        ReservationRepository reservationRepository;

        @Override
        public TicketReserveDto registerReservation(Reservation reservation)
                throws BusinessException {

            List<ReserveFlight> reserveFlightList = reservation.getReserveFlightList();

            // repository access
            int reservationInsertCount = reservationRepository.insert(reservation);
            if (reservationInsertCount != 1) {
                throw new SystemException(LogMessages.E_AR_A0_L9002.getCode(),
                        LogMessages.E_AR_A0_L9002.getMessage(reservationInsertCount, 1));
            }

            String reserveNo = reservation.getReserveNo();

            Date paymentDate = reserveFlightList.get(0).getFlight().getDepartureDate();

            return new TicketReserveDto(reserveNo, paymentDate);
        }
    }

以下に、テスト対象が使用するマッピングファイルを示す。

- `ReservationRepository.xml`

    <mapper namespace="com.example.domain.repository.reservation.ReservationRepository">

      <insert id="insert" parameterType="Reservation">
        <selectKey keyProperty="reserveNo" resultType="String" order="BEFORE">
          SELECT TO_CHAR(NEXTVAL('sq_reservation_1'), 'FM0999999999')
        </selectKey>
        INSERT INTO reservation
        (
            reserve_no,
            reserve_date,
            total_fare,
            rep_family_name,
            rep_given_name,
            rep_age,
            rep_gender,
            rep_tel,
            rep_mail,
            rep_customer_no
        )
        VALUES
        (
            #{reserveNo},
            #{reserveDate},
            #{totalFare},
            #{repFamilyName},
            #{repGivenName},
            #{repAge},
            #{repGender.code},
            #{repTel},
            #{repMail},
            NULLIF(#{repMember.membershipNumber}, '')
        )
      </insert>

#### 10.2.2.2.1.1. 依存クラスを利用したテスト

`Service`をインジェクションし、インフラストラクチャ層を結合して行う。`Service`のテストにおいて、作成するファイルを以下に示す。

作成するファイル名 | 説明
--- | ---
`TicketReserveServiceImplTest.java` | `TicketReserveServiceImpl.java`のテストクラス
`TestContextConfig.java` | テスト実装例で使用する設定ファイルで定義した設定ファイルを使用する。

テスト対象のServiceの実装クラスをインジェクションしてインフラストラクチャ層と結合してテストを行う場合のテスト作成方法を説明する。

以下に、テスト時に読み込む設定ファイルを示す。

- `SampleDomainConfig.java`

    @Configuration
    @EnableTransactionManagement
    @ComponentScan(basePackages = {"com.example.domain"})
    public class SampleDomainConfig {

        /**
         * Configure messages logging AOP.
         * @param exceptionLogger Bean defined by ApplicationContextConfig#exceptionLogger
         * @see com.example.config.app.ApplicationContextConfig#exceptionLogger(ExceptionCodeResolver)
         * @return Bean of configured {@link ResultMessagesLoggingInterceptor}
         */
        @Bean
        public ResultMessagesLoggingInterceptor resultMessagesLoggingInterceptor(ExceptionLogger exceptionLogger) {
            ResultMessagesLoggingInterceptor bean = new ResultMessagesLoggingInterceptor();
            bean.setExceptionLogger(exceptionLogger);
            return bean;
        }

        /**
         * Configure messages logging AOP advisor.
         * @param resultMessagesLoggingInterceptor Bean defined by #resultMessagesLoggingInterceptor
         * @see #resultMessagesLoggingInterceptor(ExceptionLogger)
         * @return Advisor configured for PointCut
         */
        @Bean
        public Advisor resultMessagesLoggingInterceptorAdvisor(ResultMessagesLoggingInterceptor resultMessagesLoggingInterceptor) {
           AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
           pointcut.setExpression("@within(org.springframework.stereotype.Service)");
           return new DefaultPointcutAdvisor(pointcut, resultMessagesLoggingInterceptor);
        }

    }

以下に、テスト実装例を示す。

テスト対象の`TicketReserveServiceImpl#registerReservation()`メソッドを実行し、戻り値を確認している。なお、データベースの状態の検証方法はRepositoryの単体テストを参照されたい。

- `TicketReserveServiceImplTest.java`

    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = { TestContextConfig.class, SampleEnvConfig.class, SampleInfraConfig.class,
              SampleDomainConfig.class }) // (1)
    @Transactional
    public class TicketReserveServiceImplTest {

        @Inject
        TicketReserveService target;

        @Inject
        private JdbcTemplate jdbcTemplate;

        @Test
        @Sql(statements = "ALTER SEQUENCE sq_reservation_1 RESTART WITH 1") // (2)
        public void testRegisterReservation() {

            // setup
            Reservation inputReservation = new Reservation();
            inputReservation.setTotalFare(39200);
            inputReservation.setReserveNo("0000000001");
            // omitted

            // run the test
            TicketReserveDto actTicketReserveDto = target.registerReservation(
                    reservation);

            // assertion
            assertThat(actTicketReserveDto.getReserveNo(), is("0000000001"));
            // omitted
        }
    }

項番 | 説明
--- | ---
(1) | `TicketReserveServiceImpl`クラスを動作させるために必要な設定ファイル（アプリケーションが保持する`SampleDomainConfig.class`とそれを補う`TestContextConfig.class`）を読み込む。
(2) | `@Sql`の`statements`属性を使用することでSQL文を直接指定することもできる。ここではテストメソッド実行前にシーケンスの初期化を行っている。

Warning

テスト時のトランザクション管理

テストケースに`@Transactional`アノテーションを付与すると、テスト実行開始から終了まで一トランザクションとなる。そのため、テストケースから`@Transactional`アノテーションを付与した`Service`クラスを呼び出した場合、テストケースからトランザクションが引き継がれる点に注意すること。

例えば、トランザクションの伝播方法がデフォルト（`REQUIRED`）の場合、テストケースで開始したトランザクションでテスト対象の処理が行われ、コミット/ロールバックのタイミングもテスト終了時になる。

トランザクションの伝播方法については「宣言型トランザクション管理」で必要となる情報を参照されたい。

作成するファイル名 | 説明
--- | ---
`TicketReserveServiceImplTest.java` | `TicketReserveServiceImpl.java`のテストクラス
`test-context.xml` | テスト実装例で使用する設定ファイルで定義した設定ファイルを使用する。

テスト対象のServiceの実装クラスをインジェクションしてインフラストラクチャ層と結合してテストを行う場合のテスト作成方法を説明する。

以下に、テスト時に読み込む設定ファイルを示す。

- `sample-domain.xml`

    <context:component-scan base-package="com.example.domain" />
    <tx:annotation-driven />

    <import resource="classpath:META-INF/spring/sample-infra.xml" />
    <import resource="classpath:META-INF/spring/sample-codelist.xml" />

    <bean id="resultMessagesLoggingInterceptor"
      class="org.terasoluna.gfw.common.exception.ResultMessagesLoggingInterceptor">
      <property name="exceptionLogger" ref="exceptionLogger" />
    </bean>

    <aop:config>
      <aop:advisor advice-ref="resultMessagesLoggingInterceptor"
        pointcut="@within(org.springframework.stereotype.Service)" />
    </aop:config>

以下に、テスト実装例を示す。テスト対象の`TicketReserveServiceImpl#registerReservation()`メソッドを実行し、戻り値を確認している。

なお、データベースの状態の検証方法はRepositoryの単体テストを参照されたい。

- `TicketReserveServiceImplTest.java`

    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = {
            "classpath:META-INF/spring/sample-domain.xml", // (1)
            "classpath:META-INF/spring/test-context.xml"}) // (1)
    @Transactional
    public class TicketReserveServiceImplTest {

        @Inject
        TicketReserveService target;

        @Inject
        private JdbcTemplate jdbcTemplate;

        @Test
        @Sql(statements = "ALTER SEQUENCE sq_reservation_1 RESTART WITH 1") // (2)
        public void testRegisterReservation() {

            // setup
            Reservation inputReservation = new Reservation();
            inputReservation.setTotalFare(39200);
            inputReservation.setReserveNo("0000000001");
            // omitted

            // run the test
            TicketReserveDto actTicketReserveDto = target.registerReservation(
                    reservation);

            // assertion
            assertThat(actTicketReserveDto.getReserveNo(), is("0000000001"));
            // omitted
        }
    }

項番 | 説明
--- | ---
(1) | `TicketReserveServiceImpl`クラスを動作させるために必要な設定ファイル（アプリケーションが保持する`sample-domain.xml`とそれを補う`test-context.xml`）を読み込む。
(2) | `@Sql`の`statements`属性を使用することでSQL文を直接指定することもできる。ここではテストメソッド実行前にシーケンスの初期化を行っている。

Warning

テスト時のトランザクション管理

テストケースに`@Transactional`アノテーションを付与すると、テスト実行開始から終了まで一トランザクションとなる。そのため、テストケースから`@Transactional`アノテーションを付与した`Service`クラスを呼び出した場合、テストケースからトランザクションが引き継がれる点に注意すること。

例えば、トランザクションの伝播方法がデフォルト（`REQUIRED`）の場合、テストケースで開始したトランザクションでテスト対象の処理が行われ、コミット/ロールバックのタイミングもテスト終了時になる。

トランザクションの伝播方法については「宣言型トランザクション管理」で必要となる情報を参照されたい。

#### 10.2.2.2.1.2. モックを利用したテスト

`Service`の依存クラスをすべてモック化して行う`Service`の単体テストにおいて、作成するファイルを以下に示す。

作成するファイル名 | 説明
--- | ---
`TicketReserveServiceImplMockTest.java` | `TicketReserveServiceImpl.java`のテストクラス（モックを使用する場合）

テスト対象の`Service`の実装クラスが依存するクラスをモック化する場合のテスト作成方法を説明する。
ここでは、`ReservationRepository#insert()`メソッドをモック化し、テスト対象の`TicketReserveServiceImpl#registerReservation()`メソッドでモック化したメソッドが呼び出されることとテスト対象の戻り値を確認している。

- `TicketReserveServiceImplMockTest.java`

    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;
    import static org.mockito.Mockito.verify;
    import static org.mockito.Mockito.when;

    public class TicketReserveServiceImplMockTest {

        @Rule // (1)
        public MockitoRule mockito = MockitoJUnit.rule();

        @Mock // (2)
        ReservationRepository reservationRepository;

        @InjectMocks // (3)
        private TicketReserveServiceImpl target;

        @Test
        public void testRegisterReservation() {

            // setup
            Reservation inputReservation = new Reservation();
            inputReservation.setTotalFare(39200);
            inputReservation.setReserveNo("0000000001");
            // omitted

            when(reservationRepository.insert(inputReservation)).thenReturn(1); // (4)

            // run the test
            TicketReserveDto ticketReserveDto = target.registerReservation(inputReservation);

            // assertion
            verify(reservationRepository).insert(inputReservation); // (5)
            assertThat(ticketReserveDto.getReserveNo(), is("0000000001"));
            // omitted
        }
    }

項番 | 説明
--- | ---
(1) | モックの初期化とインジェクションをアノテーションベースで行うための宣言。 詳細はモックの生成を参照されたい。
(2) | `@Mock`アノテーションを付与することで、`TicketReserveServiceImpl`が依存している`MemberRepository`をモック化している。 詳細はモックの生成を参照されたい。
(3) | `@InjectMocks`アノテーションを付与することで、自動的にモックオブジェクトが代入される。 詳細はモックの生成を参照されたい。
(4) | `ReservationRepository`の`insert`メソッドについて、引数が`inputReservation`の場合、返り値として”`1`“を返すように設定する。 メソッドのモック化については、メソッドのモック化を参照されたい。
(5) | `ReservationRepository`の`insert`メソッドについて、引数に`inputReservation`が渡されて1回呼び出されたことを検証する。 モック化したメソッドの検証については、モック化したメソッドの検証を参照されたい。

## 10.2.2.3. アプリケーション層の単体テスト

### 10.2.2.3.1. アプリケーション層の単体テスト対象

本節では、開発ガイドラインのアプリケーション層の単体テストについて説明する。

アプリケーション層では、`Controller`と`Helper`のロジックを確認するためのテストを行う。
`Controller`については以下の項目を確認する。

- @RequestMapping(リクエストパス、HTTPメソッド、リクエストパラメータ)

- 返却されるView名

`View`については、本来アプリケーション層に含まれるが、本ガイドラインでは対象外とする。

Spring Testは`Controller`クラスをテストするためのサポートクラス(`org.springframework.test.web.servlet.MockMvc`など)を用意している。
`Controller`は`MockMVC`を使用して疑似リクエストを送信してテストをするため、`MockMVC`を提供する
Spring Testの`SpringJUnit4ClassRunner`を使用する。
`MockMvc`は`Controller`に疑似リクエストを送信する仕組みを持ち、デプロイしたアプリケーションを模したテストを行うことができる。
`MockMVC`の詳細はMockMvcとはを参照されたい。

Note

Formのバリデーションテスト

`Form`のテストは、本来`Controller`と組み合わせて実際の動作に近い形で行う必要があるが、`Validation`の全パターンを`Controller`と組み合わせるとテストの負担が大きくなる。そのため、単純な`Validation`の確認であれば、`Controller`と切り離して `Form`単体で`Validation`の確認を行うこともできる。

テスト方法はテスト対象のFormを使用してBean Validationで実装したValidatorの単体テストを実施すればよい。

### 10.2.2.3.2. Controllerの単体テスト

ここでは、以下の`Controller`の単体テスト実装方法を説明する。

テスト方法 | 説明
--- | ---
StandaloneSetupを利用したテスト | Spring Testが提供するデフォルトのコンテキストを使用し指定した設定ファイルを読み込むことでテストを行う。
WebAppContextSetupを利用したテスト | 実際に使用する`applicationContext.xml`と`spring-mvc.xml`を使用してテストを行う。
モックを利用したテスト | `Controller`が依存するクラスをすべてモック化してテストを行う。

ここでは、以下の成果物に対するテストを例に説明する。`Controller`の実装の詳細は、Controllerの実装を参照されたい。

- `Controller`クラス（TicketSearchController）

- `Controller`クラス（MemberRegisterController）

なお、インジェクションとモック化を組み合わせてテストを行いたい場合は、適宜以下に説明する実装方法を組み合わせて実装されたい。

#### 10.2.2.3.2.1. StandaloneSetupを利用したテスト

`Controller`の依存クラスが利用できモック化する必要がない場合の`Controller`の単体テストにおいて、`StandaloneSetup`で作成するファイルを以下に示す。

作成するファイル名 | 説明
--- | ---
`MemberRegisterControllerStandaloneTest.java` | `MemberRegisterController.java`のテストクラス
`SpringMvcTestConfig.java` | アプリケーション層に依存するコンポーネントを読み込むための`component-scan`をテスト用に抽出した設定ファイル。
`TestContextConfig.java` | `Controller`をドメイン層、インフラストラクチャ層と結合してテストを行う場合に使用する設定ファイル。

`SpringMvcConfig.java`を使ってテストをすることが望ましいが、Spring Testが作成したコンテキストとSpring MVCが作成したコンテキストが衝突しテスト実行ができないことがある。そのため対応策として、テストに必要な設定のみ抽出し、テスト用の設定ファイルを用意する。

以下に、必要な設定のみ抽出した設定ファイルを示す。

- `SpringMvcTestConfig.java`

    @ComponentScan(basePackages = { "com.example.app"})
    @Configuration
    public class SpringMvcTestConfig{

    }

`ServiceImpl`クラスなどテスト対象の`Controller`クラスが依存するクラスをインジェクションする場合のテスト作成方法を説明する。なお、テストでデータアクセスする場合の検証方法はRepositoryの単体テストを、呼び出すドメイン層のロジックを確認する方法はServiceの単体テストを参照されたい。

以下に、テスト対象となる`Controller`の実装例を示す。

- `MemberRegisterController.java`

    @Controller
    @RequestMapping("member/register")
    @TransactionTokenCheck("member/register")
    public class MemberRegisterController {

        @TransactionTokenCheck(type = TransactionTokenType.IN)
        @PostMapping
        public String register(@Validated MemberRegisterForm memberRegisterForm,
            BindingResult result, Model model, RedirectAttributes redirectAttributes) {

            if (result.hasErrors()) {
                throw new BadRequestException(result);
            }

            // omitted

            return "redirect:/member/register?complete";
        }
    }

ここでは、テスト対象の`MemberRegisterController`クラスの`register`メソッドを呼び出し、リクエストマッピングと返却されるViewおよびリダイレクトされること（`testRegisterConfirm01`）、不正な入力値を送信したときに`BadRequestException`がthrowされていること（`testRegisterConfirm02`）の確認を行う。

以下に、`ServiceImpl`クラスなどテスト対象の`Controller`クラスが依存するクラスをインジェクションする場合のテスト作成方法を説明する。なお、テストでデータアクセスする場合の検証方法はRepositoryの単体テストを参照されたい。

- `MemberRegisterControllerStandaloneTest.java`

    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = { ApplicationContextConfig.class, TestContextConfig.class, SpringMvcTestConfig.class }) // (1)
    public class MemberRegisterControllerStandaloneTest {

        @Inject
        MemberRegisterController target;

        MockMvc mockMvc;

        @Before
        public void setUp() {

            // setup
            mockMvc = MockMvcBuilders.standaloneSetup(target).alwaysDo(log()).build(); // (2)
        }

        @Test
        public void testRegisterConfirm01() throws Exception {

            // setup and run the test
            mockMvc.perform(post("/member/register")
                        // omitted
                        .param("password", "testpassword")          // (3)
                        .param("reEnterPassword", "testpassword"))) // (3)
                        // assert
                        .andExpect(status().is(302))                                  // (4)
                        .andExpect(view().name("redirect:/member/register?complete")) // (4)
                        .andExpect(model().hasNoErrors());                            // (4)
        }

        @Test
        public void testRegisterConfirm02() throws Exception {

            try {
                // setup and run the test
                mockMvc.perform(post("/member/register")
                        // omitted
                        .param("password", "testpassword")
                        .param("reEnterPassword", "")) // (5)
                        // assert
                        .andExpect(status().is(400))
                        .andExpect(view().name("common/error/badRequest-error"))
                        .andReturn();

                fail("test failure!");
            } catch (Exception e) {

                // assert
                assertThat(e, is(instanceOf(ServletException.class)));         // (6)
                assertThat(e.getCause(), is(instanceOf(BadRequestException.class))); // (6)
            }
        }
    }

項番 | 説明
--- | ---
(1) | `MemberRegisterController`クラスが依存する`Service`、`Repository`を動作させるために必要な設定ファイル（アプリケーションが保持する`ApplicationContextConfig.class`とそれを補う`TestContextConfig.class`、 `SpringMvcTestConfig.class`）を読み込む。`TestContextConfig.class`は、テスト実装例で使用する設定ファイルを使用している。
(2) | 読み込んだBean定義から生成した`Controller`を使用して、`MockMvc`をセットアップする。セットアップの詳細についてはMockMvcのセットアップを参照されたい。
(3) | `MemberRegisterController`クラスの`registerConfirm`メソッドを呼び出すため、`/member/register`に対してPOSTメソッドでリクエストを送信する。リクエストパラメータには`Form`の情報を設定する。リクエストデータの設定方法についてはリクエストデータの設定を、リクエスト送信の実装方法についてはリクエスト送信の実装を参照されたい。
(4) | `perform`メソッドから返却された`ResultActions`の`andExpect`メソッドで取得した`MvcResult`を使用して実行結果の妥当性を検証する。検証方法の詳細については実行結果検証の実装を参照されたい。
(5) | 不正な入力値を送信する。
(6) | `SystemExceptionResolver`を有効にしていないため、例外ハンドリングされずに`ServletException`がサーブレットコンテナに通知される。`ServletException`の`getCause`メソッドにより取得された例外から、`Controller`で期待した例外がthrowされていることを検証する。

作成するファイル名 | 説明
--- | ---
`MemberRegisterControllerStandaloneTest.java` | `MemberRegisterController.java`のテストクラス
`spring-mvc-test.xml` | アプリケーション層に依存するコンポーネントを読み込むための`component-scan`をテスト用に抽出した設定ファイル。
`test-context.xml` | `Controller`をドメイン層、インフラストラクチャ層と結合してテストを行う場合に使用する設定ファイル。

`spring-mvc.xml`を使ってテストをすることが望ましいが、Spring Testが作成したコンテキストとSpring MVCが作成したコンテキストが衝突しテスト実行ができないことがある。そのため対応策として、テストに必要な設定のみ抽出し、テスト用の設定ファイルを用意する。

以下に、必要な設定のみ抽出した設定ファイルを示す。

- `spring-mvc-test.xml`

    <context:component-scan base-package="com.example.app" />

`ServiceImpl`クラスなどテスト対象の`Controller`クラスが依存するクラスをインジェクションする場合のテスト作成方法を説明する。なお、テストでデータアクセスする場合の検証方法はRepositoryの単体テストを、呼び出すドメイン層のロジックを確認する方法はServiceの単体テストを参照されたい。

以下に、テスト対象となる`Controller`の実装例を示す。

- `MemberRegisterController.java`

    @Controller
     @RequestMapping("member/register")
     @TransactionTokenCheck("member/register")
     public class MemberRegisterController {

        @TransactionTokenCheck(type = TransactionTokenType.IN)
        @PostMapping
        public String register(@Validated MemberRegisterForm memberRegisterForm,
            BindingResult result, Model model, RedirectAttributes redirectAttributes) {

            if (result.hasErrors()) {
                throw new BadRequestException(result);
            }

            // omitted

            return "redirect:/member/register?complete";
        }
    }

ここでは、テスト対象の`MemberRegisterController`クラスの`register`メソッドを呼び出し、リクエストマッピングと返却されるViewおよびリダイレクトされること（`testRegisterConfirm01`）、不正な入力値を送信したときに`BadRequestException`がthrowされていること（`testRegisterConfirm02`）の確認を行う。

以下に、`ServiceImpl`クラスなどテスト対象の`Controller`クラスが依存するクラスをインジェクションする場合のテスト作成方法を説明する。なお、テストでデータアクセスする場合の検証方法はRepositoryの単体テストを参照されたい。

- `MemberRegisterControllerStandaloneTest.java`

    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = {
            "classpath:META-INF/spring/applicationContext.xml", // (1)
            "classpath:META-INF/spring/test-context.xml",       // (1)
            "classpath:META-INF/spring/spring-mvc-test.xml"})   // (1)
    public class MemberRegisterControllerStandaloneTest {

        @Inject
        MemberRegisterController target;

        MockMvc mockMvc;

        @Before
        public void setUp() {

            // setup
            mockMvc = MockMvcBuilders.standaloneSetup(target).alwaysDo(log()).build(); // (2)
        }

        @Test
        public void testRegisterConfirm01() throws Exception {

            // setup and run the test
            mockMvc.perform(post("/member/register")
                        // omitted
                        .param("password", "testpassword")          // (3)
                        .param("reEnterPassword", "testpassword"))) // (3)
                        // assert
                        .andExpect(status().is(302))                                  // (4)
                        .andExpect(view().name("redirect:/member/register?complete")) // (4)
                        .andExpect(model().hasNoErrors());                            // (4)
        }

        @Test
        public void testRegisterConfirm02() throws Exception {

            try {
                // setup and run the test
                mockMvc.perform(post("/member/register")
                        // omitted
                        .param("password", "testpassword")
                        .param("reEnterPassword", "")) // (5)
                        // assert
                        .andExpect(status().is(400))
                        .andExpect(view().name("common/error/badRequest-error"))
                        .andReturn();

                fail("test failure!");
            } catch (Exception e) {

                // assert
                assertThat(e, is(instanceOf(ServletException.class)));         // (6)
                assertThat(e.getCause(), is(instanceOf(BadRequestException.class))); // (6)
            }
        }
    }

項番 | 説明
--- | ---
(1) | `MemberRegisterController`クラスが依存する`Service`、`Repository`を動作させるために必要な設定ファイル（アプリケーションが保持する`applicationContext.xml`とそれを補う`test-context.xml`、 `spring-mvc-test.xml`）を読み込む。`test-context.xml`は、テスト実装例で使用する設定ファイルを使用している。
(2) | 読み込んだBean定義から生成した`Controller`を使用して、`MockMvc`をセットアップする。セットアップの詳細についてはMockMvcのセットアップを参照されたい。
(3) | `MemberRegisterController`クラスの`registerConfirm`メソッドを呼び出すため、 `/member/register`に対してPOSTメソッドでリクエストを送信する。リクエストパラメータには`Form`の情報を設定する。リクエストデータの設定方法についてはリクエストデータの設定を、リクエスト送信の実装方法についてはリクエスト送信の実装を参照されたい。
(4) | `perform`メソッドから返却された`ResultActions`の`andExpect`メソッドで取得した`MvcResult`を使用して実行結果の妥当性を検証する。検証方法の詳細については実行結果検証の実装を参照されたい。
(5) | 不正な入力値を送信する。
(6) | `SystemExceptionResolver`を有効にしていないため、例外ハンドリングされずに`ServletException`がサーブレットコンテナに通知される。`ServletException`の`getCause`メソッドにより取得された例外から、`Controller`で期待した例外がthrowされていることを検証する。

#### 10.2.2.3.2.2. WebAppContextSetupを利用したテスト

`Controller`の依存クラスが利用できモック化する必要がない場合の`Controller`の単体テストにおいて、`WebAppContextSetup`で作成するファイルを以下に示す。

作成するファイル名 | 説明
--- | ---
`MemberRegisterControllerWebAppContextTest.java` | `MemberRegisterController.java`のテストクラス

StandaloneSetupを利用したテストの例では、パスへのリクエストや`Controller`が返す`View`名などは確認できるが、`TransactionTokenInterceptor`や`SystemExceptionResolver`といったSpringに追加して利用する機能は適用されていないため、トランザクショントークンチェックが正しく設定されているか、エラーページへの遷移が正しいかを判断することはできない。
そのような場合は、`MockMvc`を`webAppContextSetup`でセットアップすることにより、Springに追加して利用する`Interceptor`や`ExceptionResolver`などをテスト時に自動で適用させることができる。
ここでは、StandaloneSetupを利用したテストで説明したテストと、`@TransactionTokenCheck`アノテーション、`SystemExceptionResolver`が有効になった場合のテストとを比べた時の相違点について説明する。

以下に、テスト対象となる`Controller`の実装例を示す。

- `MemberRegisterController.java`

    @Controller
    @RequestMapping("member/register")
    @TransactionTokenCheck("member/register")
    public class MemberRegisterController {

        @TransactionTokenCheck(type = TransactionTokenType.BEGIN) // (1)
        @PostMapping(params = "confirm")
        public String registerConfirm(@Validated MemberRegisterForm memberRegisterForm,
            BindingResult result, Model model) {

            // omitted

            return "C1/memberRegisterConfirm";
        }

        @TransactionTokenCheck(type = TransactionTokenType.IN) // (1)
        @RequestMapping(method = RequestMethod.POST)
        public String register(@Validated MemberRegisterForm memberRegisterForm,
            BindingResult result, Model model, RedirectAttributes redirectAttributes) {

            if (result.hasErrors()) {
                throw new BadRequestException(result); // (2)
            }

            // omitted

            return "redirect:/member/register?complete";
        }
    }

項番 | 説明
--- | ---
(1) | `@TransactionTokenCheck`アノテーションを設定することで不正なリクエストを無効にする。 トランザクショントークンチェックについては、トランザクショントークンチェックについてを参照されたい。
(2) | リクエスト時に検証エラーがある場合は改ざんとみなしてエラーをthrowする。

初めに、`@TransactionTokenCheck`を有効にした場合におけるテスト作成方法の相違点について説明する。
なお、テストでデータアクセスする場合の検証方法はRepositoryの単体テストを参照されたい。

- `MemberRegisterControllerWebAppContextTest.java`

    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextHierarchy({ @ContextConfiguration(classes = { ApplicationContextConfig.class, SpringSecurityConfig.class }),
          @ContextConfiguration(classes = { SpringMvcConfig.class }) }) // (1)
    @WebAppConfiguration                                                        // (1)
    public class MemberRegisterControllerWebAppContextTest {

        @Inject
        WebApplicationContext webApplicationContext; // (2)

        MockMvc mockMvc;

        @Before
        public void setUp() {

            // setup
            mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) // (2)
                    .alwaysDo(log()).build();
        }

        @Test
        public void testRegisterConfirm01() throws Exception {

            // setup and run the test
            MvcResult mvcResult = mockMvc.perform(post("/member/register") // (3)
                        .param("confirm", "")                              // (3)
                        // omitted
                        .param("password", "testpassword")                 // (3)
                        .param("reEnterPassword", "testpassword"))         // (3)
                        // assert
                        .andExpect(status().isOk())
                        .andExpect(view().name("C1/memberRegisterConfirm"))
                        .andReturn();

            TransactionToken actTransactionToken = (TransactionToken) mvcResult.getRequest()
                    .getAttribute(TransactionTokenInterceptor.NEXT_TOKEN_REQUEST_ATTRIBUTE_NAME); // (4)

            MockHttpSession mockSession = (MockHttpSession) mvcResult.getRequest().getSession();  // (5)

            // setup and run the test
            mockMvc.perform(post("/member/register")              // (6)
                        // omitted
                        .param("password", "testpassword")        // (6)
                        .param("reEnterPassword", "testpassword") // (6)
                        .param(TransactionTokenInterceptor.TOKEN_REQUEST_PARAMETER,
                                actTransactionToken.getTokenString()) // (6)
                        .session(mockSession)) // (6)
                        // assert
                        .andExpect(status().isFound())                                   // (7)
                        .andExpect(view().name("redirect:/member/register?complete")); // (7)
        }
    }

項番 | 説明
--- | ---
(1) | 業務でカスタムした`Interceptor`や`ExceptionResolver`などを動作させるために`SpringMvcConfig.class`を読み込む。
(2) | 読み込んだBean定義から生成したWebアプリケーションコンテキストを使用して、`MockMvc`をセットアップする。
(3) | トランザクショントークンを生成するために、`@TransactionTokenCheck(type = TransactionTokenType.BEGIN)`が設定されたメソッドに対してリクエストを送信する。
(4) | BEGINしたリクエスト（`registerConfirm`メソッド）からINのリクエスト（`register`メソッド）にトランザクショントークンを引き継ぐため、リクエスト属性からトランザクショントークンを取得する。
(5) | サーバ側は発行したトランザクショントークンをセッションに保持するため、次のリクエストでも同じセッションを参照する必要があるが、`MockMvc`では１リクエストごとに新規セッションが使われてしまうため、明示的に同じセッションを使用するよう指定する。
(6) | 再度、リクエストパス（`/member/register`）に対してPOSTメソッドでリクエストを送信する。リクエストパラメータには`Form`の情報、(4)で取得したトランザクショントークンを設定し、セッションには(5)で取得したセッションを設定する。
(7) | トランザクショントークンチェックの設定が正しいことを確認するために、トークンチェックエラーになっていないことを検証する。

    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextHierarchy({@ContextConfiguration(                                   // (1)
            "classpath:META-INF/spring/applicationContext.xml"),                // (1)
            @ContextConfiguration("classpath:META-INF/spring/spring-mvc.xml")}) // (1)
    @WebAppConfiguration                                                        // (1)
    public class MemberRegisterControllerWebAppContextTest {

        @Inject
        WebApplicationContext webApplicationContext; // (2)

        MockMvc mockMvc;

        @Before
        public void setUp() {

            // setup
            mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) // (2)
                    .alwaysDo(log()).build();
        }

        @Test
        public void testRegisterConfirm01() throws Exception {

            // setup and run the test
            MvcResult mvcResult = mockMvc.perform(post("/member/register") // (3)
                        .param("confirm", "")                              // (3)
                        // omitted
                        .param("password", "testpassword")                 // (3)
                        .param("reEnterPassword", "testpassword"))         // (3)
                        // assert
                        .andExpect(status().isOk())
                        .andExpect(view().name("C1/memberRegisterConfirm"))
                        .andReturn();

            TransactionToken actTransactionToken = (TransactionToken) mvcResult.getRequest()
                    .getAttribute(TransactionTokenInterceptor.NEXT_TOKEN_REQUEST_ATTRIBUTE_NAME); // (4)

            MockHttpSession mockSession = (MockHttpSession) mvcResult.getRequest().getSession();  // (5)

            // setup and run the test
            mockMvc.perform(post("/member/register")              // (6)
                        // omitted
                        .param("password", "testpassword")        // (6)
                        .param("reEnterPassword", "testpassword") // (6)
                        .param(TransactionTokenInterceptor.TOKEN_REQUEST_PARAMETER,
                                actTransactionToken.getTokenString()) // (6)
                        .session(mockSession)) // (6)
                        // assert
                        .andExpect(status().isFound())                                   // (7)
                        .andExpect(view().name("redirect:/member/register?complete")); // (7)
        }
    }

項番 | 説明
--- | ---
(1) | 業務でカスタムした`Interceptor`や`ExceptionResolver`などを動作させるために`spring-mvc.xml`を読み込む。
(2) | 読み込んだBean定義から生成したWebアプリケーションコンテキストを使用して、`MockMvc`をセットアップする。
(3) | トランザクショントークンを生成するために、`@TransactionTokenCheck(type = TransactionTokenType.BEGIN)`が設定されたメソッドに対してリクエストを送信する。
(4) | BEGINしたリクエスト（`registerConfirm`メソッド）からINのリクエスト（`register`メソッド）にトランザクショントークンを引き継ぐため、リクエスト属性からトランザクショントークンを取得する。
(5) | サーバ側は発行したトランザクショントークンをセッションに保持するため、次のリクエストでも同じセッションを参照する必要があるが、`MockMvc`では１リクエストごとに新規セッションが使われてしまうため、明示的に同じセッションを使用するよう指定する。
(6) | 再度、リクエストパス（`/member/register`）に対してPOSTメソッドでリクエストを送信する。リクエストパラメータには`Form`の情報、(4)で取得したトランザクショントークンを設定し、セッションには(5)で取得したセッションを設定する。
(7) | トランザクショントークンチェックの設定が正しいことを確認するために、トークンチェックエラーになっていないことを検証する。

次に、`SystemExceptionResolver`を有効にした場合におけるテスト作成方法の相違点を説明する。

以下に、`SystemExceptionResolver`の定義例を示す。

- `SpringMvcConfig.java`

    @Bean
    public SystemExceptionResolver systemExceptionResolver(ExceptionCodeResolver exceptionCodeResolver) {
        SystemExceptionResolver bean = new SystemExceptionResolver();
        bean.setOrder(3);

        Properties exceptionMappings = new Properties();
        exceptionMappings.setProperty("InvalidTransactionTokenException", "common/error/token-error");
        exceptionMappings.setProperty("BadRequestException", "common/error/badRequest-error");
        exceptionMappings.setProperty("Exception", "common/error/system-error");
        bean.setExceptionMappings(exceptionMappings);

        Properties statusCodes = new Properties();
        statusCodes.setProperty("common/error/token-error", String.valueOf(HttpStatus.CONFLICT.value()));
        statusCodes.setProperty("common/error/badRequest-error", String.valueOf(HttpStatus.BAD_REQUEST.value()));
        bean.setStatusCodes(statusCodes);

        bean.setDefaultStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
        bean.setExceptionCodeResolver(exceptionCodeResolver);
        bean.setPreventResponseCaching(true);

        return bean;
    }

以下に、テスト作成方法の相違点について説明する。

- `MemberRegisterControllerWebAppContextTest.java`

    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextHierarchy({ @ContextConfiguration(classes = { ApplicationContextConfig.class, SpringSecurityConfig.class }),
          @ContextConfiguration(classes = { SpringMvcConfig.class }) })
    @WebAppConfiguration
    public class MemberRegisterControllerWebAppContextTest {

        @Inject
        WebApplicationContext webApplicationContext;

        MockMvc mockMvc;

        @Before
        public void setUp() {

            // setup
            mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                    .alwaysDo(log()).build();
        }

        @Test
        public void testRegisterConfirm02() throws Exception {

            // omitted

            // setup and run the test
            mvcResult = mockMvc.perform(post("/member/register")
                        .param("password", "testpassword")
                        .param("reEnterPassword", "") // (1)
                        .param(TransactionTokenInterceptor.TOKEN_REQUEST_PARAMETER,
                                actTransactionToken.getTokenString()) // (2)
                        .session(mockSession)) // (2)
                        // assert
                        .andExpect(status().isBadRequest())                      // (3)
                        .andExpect(view().name("common/error/badRequest-error")) // (3)
                        .andReturn();

            // assert
            Exception exception = mvcResult.getResolvedException();                   // (4)
            assertThat(exception, is(instanceOf(BadRequestException.class)));         // (4)
            assertThat(exception.getMessage(), is(
                "[e.ar.a0.L9001] 不正リクエスト(パラメータ改竄) org.springframework.validation.BeanPropertyBindingResult: 1 errors\n"
                        + "Error in object 'memberRegisterForm': codes [e.ar.c1.5001.memberRegisterForm,e.ar.c1.5001]; arguments []; default message [null]")); // (4)
        }
    }

項番 | 説明
--- | ---
(1) | `Form`の情報を不正な値にすることで、`register`メソッドの内でエラーをthrowさせている。
(2) | 前述と同様に、生成したトランザクショントークン情報を設定する。
(3) | ここでは`SystemExceptionResolver`が有効になっているため、定義したエラーのステータスコード、エラーページの遷移先が正しく設定されていることを検証する。
(4) | `SystemExceptionResolver`で例外ハンドリングされたエラーから、期待したエラーがthrowされていることを検証する。

- `spring-mvc.xml`

    <bean class="org.terasoluna.gfw.web.exception.SystemExceptionResolver">
      <property name="order" value="3" />
      <property name="exceptionMappings">
        <map>
          <entry key="InvalidTransactionTokenException" value="common/error/token-error" />
          <entry key="BadRequestException" value="common/error/badRequest-error" />
          <entry key="Exception" value="common/error/system-error" />
        </map>
      </property>
      <property name="statusCodes">
        <map>
          <entry key="common/error/token-error" value="409" />
          <entry key="common/error/badRequest-error" value="400" />
        </map>
      </property>
      <property name="excludedExceptions">
        <array>
        </array>
      </property>
      <property name="defaultStatusCode" value="500" />
      <property name="exceptionCodeResolver" ref="exceptionCodeResolver" />
      <property name="preventResponseCaching" value="true" />
    </bean>

以下に、テスト作成方法の相違点について説明する。

- `MemberRegisterControllerWebAppContextTest.java`

    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextHierarchy({@ContextConfiguration(
            "classpath:META-INF/spring/applicationContext.xml"),
            @ContextConfiguration("classpath:META-INF/spring/spring-mvc.xml")})
    @WebAppConfiguration
    public class MemberRegisterControllerWebAppContextTest {

        @Inject
        WebApplicationContext webApplicationContext;

        MockMvc mockMvc;

        @Before
        public void setUp() {

            // setup
            mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                    .alwaysDo(log()).build();
        }

        @Test
        public void testRegisterConfirm02() throws Exception {

            // omitted

            // setup and run the test
            mvcResult = mockMvc.perform(post("/member/register")
                        .param("password", "testpassword")
                        .param("reEnterPassword", "") // (1)
                        .param(TransactionTokenInterceptor.TOKEN_REQUEST_PARAMETER,
                                actTransactionToken.getTokenString()) // (2)
                        .session(mockSession)) // (2)
                        // assert
                        .andExpect(status().isBadRequest())                      // (3)
                        .andExpect(view().name("common/error/badRequest-error")) // (3)
                        .andReturn();

            // assert
            Exception exception = mvcResult.getResolvedException();                   // (4)
            assertThat(exception, is(instanceOf(BadRequestException.class)));         // (4)
            assertThat(exception.getMessage(), is(
                "[e.ar.a0.L9001] 不正リクエスト(パラメータ改竄) org.springframework.validation.BeanPropertyBindingResult: 1 errors\n"
                        + "Error in object 'memberRegisterForm': codes [e.ar.c1.5001.memberRegisterForm,e.ar.c1.5001]; arguments []; default message [null]")); // (4)
        }
    }

項番 | 説明
--- | ---
(1) | `Form`の情報を不正な値にすることで、`register`メソッドの内でエラーをthrowさせている。
(2) | 前述と同様に、生成したトランザクショントークン情報を設定する。
(3) | ここでは`SystemExceptionResolver`が有効になっているため、定義したエラーのステータスコード、エラーページの遷移先が正しく設定されていることを検証する。
(4) | `SystemExceptionResolver`で例外ハンドリングされたエラーから、期待したエラーがthrowされていることを検証する。

Note

Sessionを利用する場合

`Controller`クラスがSessionを利用している場合は`org.springframework.mock.web.MockHttpSession`を使ってテストを行う。

- `MockHttpSession`を利用したテストメソッドの例

      public class SessionControllerTest {

          // (1)
          MockHttpSession mockSession = new MockHttpSession();

          // omitted

          @Test
          public void testSession() throws Exception {
              String formName = "todoForm";

              TodoForm form = new TodoForm();
              String todoId = "1111";
              String todoTitle = "test";

              form.setTodoId(todoId);
              form.setTodoTitle(todoTitle);

              // (2)
              mockSession.setAttribute(formName, form);

              // (3)
              ResultActions results = mockMvc.perform(post("/todo/operation")
                  .param("create", "create")
                  .param("todoId", todoId)
                  .param("todoTitle", todoTitle)
                  .session(mockSession));

              // (4)
              results.andExpect(request().sessionAttribute(formName, isA(TodoForm.class)));

              // omitted

              // (5)
              results = mockMvc.perform(get("/todo/create").param("redo", "redo"));
              results.andExpect(request().sessionAttribute(formName, isA(TodoForm.class)));

              // omitted
          }
      }

  項番 | 説明
  --- | ---
  (1) | セッションのモックオブジェクトを生成する。クラスの詳細については、MockHttpSession のJavadocを参照されたい。
  (2) | 生成したセッションのモックオブジェクトに、格納したいオブジェクトをセットする。
  (3) | `MockMvcRequestBuilders`の`post`メソッドでリクエストのモックを生成し、生成したリクエストに`session`メソッドでセッションのモックを登録する。
  (4) | (2)でセットしたオブジェクトが、セッションスコープに格納されていることを確認する。
  (5) | 再度リクエストを発行し、セッションスコープに格納したオブジェクトが保持されているか確認する。

#### 10.2.2.3.2.3. モックを利用したテスト

`Controller`の依存クラスをモック化する必要がある場合の`Controller`の単体テストにおいて、作成するファイルを以下に示す。

作成するファイル名 | 説明
--- | ---
`TicketSearchControllerMockTest.java` | `TicketSearchController.java`のテストクラス

テスト対象の`Controller`クラスが依存するクラスを、モック化する場合のテスト作成方法を説明する。

以下に、テスト対象となる`Controller`の実装例を示す。

- `TicketSearchController.java`

    @Controller
    @RequestMapping("ticket/search")
    public class TicketSearchController {

        @Inject
        TicketSearchHelper ticketSearchHelper;

        @GetMapping(params = "form")
        public String searchForm(Model model) {

            model.addAttribute(ticketSearchHelper.createDefaultTicketSearchForm());

            model.addAttribute(ticketSearchHelper.createFlightSearchOutputDto());

            model.addAttribute("isInitialSearchUnnecessary", true);

            return "B1/flightSearch";
        }
    }

以下に、`Controller`のテスト実装例を示す。

- `TicketSearchControllerMockTest.java`

    import static org.mockito.Mockito.verify;
    import static org.mockito.Mockito.when;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

    public class TicketSearchControllerMockTest {

        @Rule // (1)
        public MockitoRule mockito = MockitoJUnit.rule();

        @InjectMocks // (2)
        TicketSearchController target;

        @Mock // (3)
        TicketSearchHelper ticketSearchHelper;

        MockMvc mockMvc;

        @Before
        public void setUp() {

            // setup
            TicketSearchForm ticketSearchForm = new TicketSearchForm();
            ticketSearchForm.setFlightType(FlightType.RT);
            ticketSearchForm.setDepAirportCd("HND");
            // omitted

            when(ticketSearchHelper.createDefaultTicketSearchForm()).thenReturn(ticketSearchForm); // (4)

            mockMvc = MockMvcBuilders.standaloneSetup(target).alwaysDo(log()).build();
        }

        @Test
        public void testSearchForm() throws Exception {

            // setup and run the test
            MvcResult mvcResult = mockMvc.perform(get("/ticket/search").param("form", ""))
                        // assert
                        .andExpect(status().isOk())
                        .andExpect(view().name("B1/flightSearch"))
                        .andReturn();

            // assert
            verify(ticketSearchHelper).createDefaultTicketSearchForm(); // (5)

            // omitted
        }
    }

項番 | 説明
--- | ---
(1) | モックの初期化とインジェクションをアノテーションベースで行うための宣言。 詳細はモックの生成を参照されたい。
(2) | `@InjectMocks`アノテーションを付与することで、自動的にモックオブジェクトが代入される。 詳細はモックの生成を参照されたい。
(3) | `@Mock`アノテーションを付与することで、`TicketSearchController`が依存している`TicketSearchHelper`をモック化している。 詳細はモックの生成を参照されたい。
(4) | すべてのテストメソッドにおいて、`ticketSearchHelper`の`createDefaultTicketSearchForm`メソッドの返り値として`createMockForm`メソッドの返り値を設定する。メソッドのモック化については、メソッドのモック化を参照されたい。
(5) | `ticketSearchHelper`の`createDefaultTicketSearchForm`メソッドについて1回呼び出されたことを検証する。 モック化したメソッドの検証については、モック化したメソッドの検証を参照されたい。

### 10.2.2.3.3. Helperの単体テスト

`Helper`の単体テストは、`Service`と同様の実装でテストすることができる。
実装方法については、Serviceの単体テストを参照されたい。