Spring Testの使い方を解説【3つのメリット+α】

Spring Testの使い方を解説【3つのメリット+α】

Spring Testについて知りたい人「Spring Testの使い方、Spring Testではどんなことができるのか、ハマったときの対処法についても知りたいです。よろしくおねがいします。」


そんな方向けになります。

本記事ではSpring Testを実際に使えるようになることを目的としています。


構成は以下です。

それでは、順に見ていきましょう。

著者情報

ちなみにですが、私は5年以上IT系エンジニアとして働いており、主にJavaを主戦場にしています。Webアプリケーションと業務系のアプリケーションの経験を持つごく普通のエンジニアです。
また、実際にSpringを使ったアプリケーション開発経験があります。

Spring Testの使い方を解説

Spring Testのモジュールを使用する場合は結合テストのテストケースを作成する場合です。
※単体テストを作成する場合はJUnit+モック化モジュール(Mockitoなど)でやります。

Spring Testを使用した結合テストの使い方は以下です。

  1. テスト用のライブラリをプロジェクトに追加(Maven)
  2. DIコンテナ管理コンポーネントテスト
  3. データベースアクセス処理テスト
  4. Spring MVCテスト



順に説明します。

今回はSpringBootを使用したアプリケーションテストの解説です。

【手順1】テスト用のライブラリをプロジェクトに追加(Maven)

Mavenプロジェクトの依存関係に以下を追加します。

(省略)
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
	<exclusions>
		<!-- JUnit4は除外 -->
		<exclusion>
			<groupId>org.junit.vintage</groupId>
			<artifactId>junit-vintage-engine</artifactId>
		</exclusion>
	</exclusions>
</dependency>
(省略)



spring-boot-starter-testを追加することで以下のライブラリも提供されます。

  • JUnit 5
  • Spring Test & Spring Boot Test
  • AssertJ
  • Hamcrest
  • Mockito
  • JSONassert
  • JsonPath


1つ1つバージョン指定で依存関係に追加する必要がないのでラクです。

個人的にはJava モックフレームワークのMockitoが使えるようになるのはいいなと思いました。(理由は絶対と言っていいほどよく使うから)

さらに上記の例ではJUnit4を使用しない設定にしています。つまり今回はJunit5を使うので注意してください。

【手順2】DIコンテナ管理コンポーネントテスト

今回はServiceクラスのUserServiceクラスのテストを作成したいと思います。

package com.example.spring_react.springreactapp.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.example.spring_react.springreactapp.mapper.UserMapper;
import com.example.spring_react.springreactapp.model.LoginUser;
import com.example.spring_react.springreactapp.model.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.core.userdetails.UserDetails;
import static org.mockito.BDDMockito.*;
/**
 * @SpringBootTestアノテーション:
 * Spring Bootを使用する場合に標準@ContextConfiguration アノテーションの代替として使用する。
 * webEnvironmentパラメータではモックWeb環境(デフォルト)や実際のWeb環境でテストするかどうかの指定が可能
 * 
 * @Importアノテーション:
 * 外部に@TestConfigurationアノテーションを付与したテスト用コンフィグレーションクラスを指定する場合
 * 今回はクラス内部に作成しているのでコメントアウト
 */
@SpringBootTest
//@Import(LocalContext.class)
public class UserServiceIntegrationTest {
    private User testUser;
    /**
     * 今回テスト対象クラス
     */
    @Autowired
    UserService userService;
    /**
     * @MockBeanアノテーション:
     * SpringBootがMockitoのモックを作成し、アプリケーションコンテキスト内に登録する
     * 
     * メモ:
     * nameパラメータにBean名を指定しないとエラーになる
     */
    @MockBean(name="userMapper")
    UserMapper userMapper;
    @BeforeEach
    public void setup(){
        this.testUser = new User();
        testUser.setId(1);
        testUser.setEmail("test@test");
        testUser.setName("testUser");
        testUser.setPassword("testPass");
        testUser.setAge(20);
        testUser.setGender(1);
        testUser.setRole(1);
        testUser.setEnable(true);
    }
    /**
     * UserServiceクラスのloadUserByUsernameメソッドのテスト
     */
    @Test
    public void loadUserByUsernameTest() {
        String email = "test@test";
        // モックの戻り値を設定している
        given(this.userMapper.findByEmail(email))
            .willReturn(this.testUser);
        // テスト対象メソッド実行
        UserDetails loginUser = userService.loadUserByUsername(email);
        // 戻り値判定
        assertEquals(loginUser, new LoginUser(this.testUser));
    }
    /**
     * @TestConfiguration:
     * テスト用のBean定義を設定する場合に指定する
     * 指定したBeanが上書きされる
     * 内部クラスに作成する場合はstaticクラス。
     * 外部クラスに作成する場合はstaticクラスではない。
     * 今回は不要のためコメントアウト
     */
    /*
    @TestConfiguration
    static class LocalContext {
        @Bean
        UserMapper userMapper() {
            return new UserMapper(){
                @Override
                public List<User> findAll() {
                    return null;
                }
                @Override
                public User findByEmail(String email) {
                    return null;
                }
                @Override
                public void save(User user) {
                }
            };
        }
    }
    */
}

【手順3】データベースアクセス処理テスト

今回はMyBatisを使用したRepositoryクラスのUserMapperクラスのテストを作成してみます。

package com.example.spring_react.springreactapp.mapper;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import com.example.spring_react.springreactapp.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.transaction.annotation.Transactional;
/**
 * @Transactionalアノテーション:
 * トランザクションの境界を全メソッドの実行前に移動する
 * メソッドごとに設定も可能。
 * propagationパラメータでトランザクション作成に関する設定をすることができる
 * 
 * @Commitアノテーション:
 * メソッド実行後にコミットする。
 * 指定しない場合はデフォルトでロールバックされる。
 * 
 */
@SpringBootTest
@Transactional
@Commit
public class UserMapperIntegrationTest {
    
    @Autowired
    UserMapper userMapper;
    /**
     * メモ:アプリケーション実行時にdata.sqlで13件のデータを作成している
     */
    @Test
    public void findAllTest(){
        List<User> list = userMapper.findAll();
        assertEquals(13, list.size());;
    }
    /**
     * @Sqlアノテーション:
     * 指定したSQLを実行することが可能。
     * クラスに指定した場合は全メソッド前に実行される。
     * メソッドに指定した場合は指定したメソッドのみで実行され、
     * クラスで指定したSQLは実行されない
     * 
     * @Rollbackアノテーション:
     * メソッドとクラスで指定可能
     * メソッドに指定した場合はメソッド終了時にロールバックする
     * クラスに指定した場合はすべてのメソッド終了時にロールバックする
     * 
     * メモ:
     * delete_user.sqlではuserテーブルのデータをすべて削除
     * data_test.sqlではデータを3件追加している
     * 
     */
    @Test
    @Sql({"/delete_user.sql", "/data_test.sql"})
    @Rollback
    public void saveTest(){
        User user = new User();
        user.setId(1);
        user.setEmail("test@test");
        user.setName("testUser");
        user.setPassword("testPass");
        user.setAge(20);
        user.setGender(1);
        user.setRole(1);
        user.setEnable(true);
        userMapper.save(user);
        List<User> list = userMapper.findAll();
        assertEquals(4, list.size());
    }
}

【手順4】Spring MVCテスト

Spring MVCのテストではControllerクラスに対するテストを実施します。

package com.example.spring_react.springreactapp.controller;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
/**
 * @AutoConfigureMockMvcアノテーション:
 * MockMvc mockMvcの初期化処理を自動化する
 */
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerIntegrationTest {
    /**
     * MockMvcクラス:
     * Spring MVCのコントローラのテストを簡単にするモッククラス
     * 下位レベルのサーブレットコンテナーの動作に依存するコードはMockMvcクラスを使用してテストできない
     * その場合はWebTestClientクラスを使用する。(今回は割愛)
     */
    @Autowired
    MockMvc mockMvc;
    
    /**
     * @WithAnonymousUserアノテーション:
     * 匿名ユーザでアクセスする
     * @throws Exception
     */
    @Test
    @WithAnonymousUser
    public void usersTest1() throws Exception {
        // モックを使用してテスト対象のURL(ユーザ一覧画面:/user/list)にアクセスする
        // Spring SecurityのDB認証の設定のためログイン画面のURLにリダイレクトすることの確認
        mockMvc.perform(get("/user/list"))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl("http://localhost/login"));
    }
    /**
     * @WithUserDetailsアノテーション:
     * Beanに登録されているUserDetailServiceから取得したユーザで認証情報を構築
     * 
     * @WithMockUserアノテーション:
     * モックユーザで認証情報を構築
     * カスタマイズユーザを使用している場合は、
     * カスタマイズしたユーザ情報を取得できずエラーになるので@WithUserDetailsアノテーションを使用する
     */
    @Test
    //@WithMockUser(roles = "USER", username = "testuser")
    @WithUserDetails(value = "admin@admin")
    public void usersTest2() throws Exception {
        // 認証情報を取得
        // テストする際はデバックモードでこのパラメータ情報を見て作成していくのがいいかも
        Authentication auth = SecurityContextHolder
                 .getContext().getAuthentication();
        String message = "class = " + auth.getClass() + "\n" +
                         "name = " + auth.getName() + "\n" +
                         "credentials = " + auth.getCredentials() + "\n" +
                         "authorities = " + auth.getAuthorities() + "\n" +
                         "principal = " + auth.getPrincipal() + "\n" +
                         "details = " + auth.getDetails();
        // 今回はただ標準出力しているだけ(ここまで本来は不要)
        System.out.println(message);
        // モックを使用してテスト対象のURL(ユーザ一覧画面:/user/list)にアクセスする
        MvcResult mvcResult = mockMvc.perform(get("/user/list")).andReturn();
        // ユーザ認証許可する設定のためアクセス可能であることを確認
        assertEquals("contents/user/list", mvcResult.getModelAndView().getViewName());
    }
}



いきなりの感想ですが、やっぱりSpring Securityが絡むと急に難しくなります。。

Spring MVCのテスト確認なのに、Spring Securityを使ってDB認証する設定にしていたので単純にはうまく行かずハマりました(笑)

でも、実際に実務で作成するアプリケーションではセキュリティ関連の設定は必須だと思うので、実践向けの使用例を示せたので結果オーライだと思っています。

とにかくですが、初心者には敷居高すぎなのでどうにかならないのかなと思いました。

Spring Testの3つのメリット

私が考えるSpring Testを使うメリットは以下です。

  • テスティングフレームワーク(JUnit)上でDIコンテナを起動することが可能。よって、アプリケーション起動時のDIコンテナ内コンポーネント間のテストが可能になる。(つまり、CT実施が可能)
  • 今回解説したSpring MVCの動作確認やトランザクションを管理する機能など、CTを自動化するための機能がたくさん用意されている。
  • JUnit4&5にも対応している(今回はJUnit5で試しました)



つまり、結合テストを自動化できるのが非常に大きなメリットであるということです。

一般的には単体テストは自動化して、結合テストは人が実際に行うことがほとんどかと思います。(むしろ私はこのパターンしか経験したことがありません)

人がやるということは、人によってスピード感も違いますし、ミスも発生します。そのため、エラーの原因の切り分けが難しくなってしまいます。

よって、結合テストの観点も自動化できることはコスト削減に直結するので、大きなメリットになります。

余談

そもそもですがSpring Frameworkを採用することによって、クラス間が疎結合になるので単体テスト自体も非常にラクに作成できます。

なので今回のようにSpring Testを使用しなくても、Junit + Mockitoを使用した単体テストを簡単に実施可能です。

これはSpring Frameworkを使う大きなメリットです。(このことも忘れないようにしましょう)

以下、今回作成したServiceクラスのテストを単体テスト(Junit + Mockito)に置き換えてみました。(参考までに)

package com.example.spring_react.springreactapp.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import com.example.spring_react.springreactapp.mapper.UserMapper;
import com.example.spring_react.springreactapp.model.LoginUser;
import com.example.spring_react.springreactapp.model.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.core.userdetails.UserDetails;
@ExtendWith(MockitoExtension.class)
public class UserServiceUnitTest {
    private User testUser;
    @InjectMocks
    UserService userService;
    @Mock
    UserMapper userMapper;
    @BeforeEach
    public void setup(){
        this.testUser = new User();
        testUser.setId(1);
        testUser.setEmail("test@test");
        testUser.setName("testUser");
        testUser.setPassword("testPass");
        testUser.setAge(20);
        testUser.setGender(1);
        testUser.setRole(1);
        testUser.setEnable(true);
    }
    @Test
    public void loadUserByUsernameTest(){
        String email = "test@test";
        when(this.userMapper.findByEmail(email)).thenReturn(this.testUser);
        UserDetails loginUser = userService.loadUserByUsername(email);
        assertEquals(loginUser, new LoginUser(this.testUser));
    }
}

テスト作成時にハマったときの対処法

Spring Testも実際に使ってみると敷居が高い気がします。

なので、ハマったときの対処法について解説します。

ずばり、シンプルですが以下だと思います。

公式のドキュメントを見る



これ1択かと思います。

理由はバージョンが変わったタイミングで大きく実装方法が変わるからです。

当たり前のことを言っていますが、今回の例で言うと、JUnit5が登場してからSpringでもJUnit4からJunit5に対応する変更がありました。そのため、今までの記述ではうまくいきません。

インターネットで検索してもまだJUnit4を使った使用例が多いのが現実です。

なので、使っているバージョンの公式のドキュメントを最終的には見ることをおすすめします。

以下、本と公式ドキュメントの切り分けも記述しておきます。

  • 本:概要を掴むために使用、はじめの実装時の検索用としても可(日本語でわかりやすいため)
  • 公式ドキュメント:本の実装ではうまくいかなかった場合に使用(最新情報が記載されているため)



以上、Spring Testについてまとめました。

参考になれば幸いです。


人気記事①:
【厳選4冊+α】Spring Framework初心者におすすめな本

人気記事②:現役エンジニアがおすすめするプログラミングスクール5社:無料あり