SpringBoot

Spring Boot Test

Spring Bootでのテストの仕方をまとめたかった

はじめに

公式Docを読む時間がある人はこの項目を自分で読むのが一番です。

テストについて学ぶには、Spring Framework、Spring Bootについてのややディープな理解が必須なので、不安な人は中田/Techメモ/SpringBoot等を参照してください。

PitCoinで実際にテストを書いてみたので、こちらも参考になると思います。

Spring Bootテスト全体の話

テストで使用する技術

Spring Bootでテストを行うためには、様々な知識が必要です。

Spring Frameworkのテスト = (バニラJava + 頻出ライブラリ) + (テスト + アサーション) + (DI + モッキング) + Auto-configuration

Bootテストの特徴

Spring Bootのテストは、Spring Frameworkのテストをまとめたうえで指向性を持たせたものです。

build.gradleに testImplementation 'org.springframework.boot:spring-boot-starter-test' という依存を追加することで、Spring Frameworkのテストライブラリに加え、以下のような外部ライブラリがいくつかimportして利用できるようになります。

テストの種類に応じてアノテーションを変更することで、コンポーネントスキャンとAuto-configurationの範囲を指定(スライス)することができます。(@DataJpaTest、@WebMvcTestなど)

Auto-configurationの内容は、@SpringBootTestスライスを参照してください。

@...Testのアノテーションは、 JUnit5の@ExtendWith(SpringExtension.class)を含み、Spring拡張機能が利用可能になります。JUnit4を利用する場合は@RunWith(...)を追加してください。

具体的なテスト手法

Repository単体テスト(Spring Data JPA)

公式Docを参照。

//公式Docより
@DataJpaTest
class ExampleRepositoryTests {

    @Autowired
    private TestEntityManager entityManager; 

    @Autowired
    private UserRepository repository;

    @Test
    void testExample() throws Exception {
        this.entityManager.persist(new User("sboot", "1234"));
        User user = this.repository.findByUsername("sboot");
        assertThat(user.getUsername()).isEqualTo("sboot");
        assertThat(user.getVin()).isEqualTo("1234");
    }

}

Repository単体テストでは、インメモリのDBを利用します。

そのため、テスト用に依存を追加しておく必要があります。(例:H2を利用する場合は、testImplementation 'com.h2database:h2' をbuild.gradleに追記)

@DataJpaTestは、@RepositoryをスキャンしてBeanを自動構成するため、RepositoryのAutowiredが可能です。その他のコンポーネント(@Serviceなど)はデフォルトではスキャンされません。

また、TestEntityManager(標準JavaのJPAにおけるEntityManagerのSpring Boot Testラップ版)のBeanも利用可能なため、Repositoryの機能を使わずにデータベースの初期化が可能になります(perisist()、clear()など)。

@DataJpaTestは@Transactionalを含むアノテーションであるため、テストごとにトランザクションがロールバックされます。

Repository+DBの結合テスト

インメモリではなく、設定ファイルで定義したDBに対してテストを実行したい場合は、以下のようにアノテーションを追記します。

// 公式Docより
@DataJpaTest
@AutoConfigureTestDatabase(replace=Replace.NONE)
class ExampleRepositoryTests {

    // ...

}

この時利用するデータベース設定は、application.propetiesから読み込まれます。

src/test/resources -> src/main/resources の順で走査され、src/test以下にapplication.propetiesが存在しない場合はsrc/mainのapplication.propetiesを参照します。

本番・開発用のDBを汚染しないように、テスト用データベース設定の作成を忘れないように注意が必要です。

Serviceの単体テスト

// 静的インポート(他のインポートは省略)
import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;

// モックを有効化
@ExtendWith(MockitoExtension.class)
public class MyServiceUnitTests {
  
  // Repositoryをモックに
  @Mock
  private MyRepository repository;
  // モックRepositoryを注入してServiceを構成
  @InjectMocks
  private MyService service;
  
  @Test
  void testMethod(){

    // Serviceで呼び出されるRepositoryメソッドの動作を定義
    given(repository.myRepositoryMethod(a)).willReturn(b);

    // Serviceを呼び出す(内部で上で定義した動作が動く)
    final returnValue = service.myServiceMethod(a);

    // 適当なアサーションを行う
    asertThat(returnValue.getName()).isEqualTo(b);

  }

}

Service単体テストでは、テスト対象のService内で使用する他のService、Repositoryクラスをモックにします。

上の例では、@ExtendWith(MockitoExtension.class)でモックBeanの生成を有効化し(Mockito機能)、モックにするクラスに@Mockを、モックを注入したいインスタンスに@InjectMocksを付加することで、モックの生成を行っています。

テストメソッドの中では、

という手順でテストを行っています。

また、今回はAuto-configurationの必要がなく(モック以外で特にBeanや機能を使用していない)、コンポーネントスキャンも不要なため(@Autowiredを使用していない)、@...Testは使用していません。

必要な機能を付加したい場合は@AutoConfigure...を、自作クラスをAutowiredしたい場合は@Impot(...)を付加します。

もしくは、@...Testを利用してもよいですが、その場合はアノテーション内に@ExtendWith(SpringExtension.class)が含まれており、その内部でモックBeanの生成が有効化されるため、@ExtendWith(MockitoExtension.class)は不要です。

Service+Repository+DB結合テスト

// 静的インポート(他のインポートは省略)
import static org.assertj.core.api.Assertions.*;

// クラスアノテーションの説明は下記
/*
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@AutoConfigureTestEntityManager
@Transactional
 */
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(MyService.class)
public class MyServiceIntegrationTests {

  // モックではなく全てAutowired
  @Autowired
  private TestEntityManager entityManager;

  @Autowired
  private MyRepository repository;

  @Autowired
  private MyService service;

  @Test
  void testMethod(){
    // テスト内容
  }

}

Serviceクラス以下の結合テストです。上の例では特にモックは使用していません。

テストの手順の一例は次の通りです。

* 結合テストクラスアノテーション

結合テストのアノテーションは、テストの方針によって付加の仕方が変わってきます。

この話はService+Repository+DBに限らず、部分的な結合テスト全般で応用できると思います。

一つ目は、ほぼ全体をスキャンしてから必要に応じて機能を付加・制限していくスタイルです。

結合の規模が大きい場合は便利ですが、機能の制限が面倒であったり、余分なAuto-configurationによってテスト時間が増える可能性があります。

二つ目は、最大公約数的な@...Testを付加してから必要に応じて機能を付加・制限していくスタイルです。

結合の規模が小さい場合や必要な機能が少ない場合は便利ですが、アノテーションが膨大になる可能性があります。

Controller単体テスト(MVCテスト)

公式Docを参照。

// 静的インポート
import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

// MockMvcの有効化など
@WebMvcTest(MyController.class)
public class MyControllerUnitTests {

  @Autowired
  private MockMvc mvc;

  @Autowired
  private ObjectMapper mapper;

  // Spring Boot版モック生成
  @MockBean
  private MyService service;
  
  @Test
  void testMethod() throws Exception {  // MockMvcはExceptionを投げる

    // モックメソッド定義
    given(service.myMethod(a)).willReturn(b);

    // REST呼び出しとアサーションを同時に行う
    mvc
        .perform(post("/this/is/path/")
                       .content(mapper.writeValueAsString(new MyForm(a)))
                       .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.result.name").value(b));

  }

}

Controller単体テストでは、MockMvcを用いることでサーバーを立ち上げずにテストが可能です。

@WebMvcTest(...)によって、以下が行われます。

また、引数として渡した単一のControllerに対して、モックBeanの注入が可能になります。

@MockBeanはSpring Boot版のモック生成アノテーションで、@WebMvcTestとの連携等が可能です。

上の例では、Controller内で呼び出すServiceをモックにしています。

MockMvcは、performでAPIを疑似的に叩き、andExpectで戻り値をアサーションできます。

例では、引数としてJSONをボディに持つPOSTを叩き、戻り値のステータスとJSONの個別の値を検証しています。

詳細やGET等の他の方法はここでは省略します。

Spring Securityのテストをしたい方は、自分は当分使う機会がないので検証していませんが、こちらを参照してください。うまくいった場合は情報提供お願いします。

Thymeleafも検証可能です

.andExpect(view().name("hoge"))
.andExpect(model().attribute("name", name));

全体(Controller+Service+Repository+DB)結合テスト

// 静的インポート
import static org.assertj.core.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

// もろもろのクラスアノテーション
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureTestEntityManager
@Transactional
public class PitCoinUserControllerIntegrationTests {

  // モックなし
  @Autowired
  private MockMvc mvc;

  @Autowired
  private TestEntityManager entityManager;

  @Autowired
  private ObjectMapper mapper;

  @Autowired
  private AccountRepository accounts;

  @Test
  void testMethod() throws Exception{
    // テスト内容
  }

}

はじめに注意として、これは"全体"結合テストですが、サーバーの起動は行っていません。

その理由は以下の通りです。

全体結合テストの手順の例は以下の通りです。

テストで使用するライブラリの簡単な使い方

JUnit5

// テストメソッド
@Test
void testMethod(){
  // テスト内容
}
// 各テスト前に実行するメソッド
@BeforeEach
void beforeEach(){
  // DBの初期化等
}
class AllTests{
  // 入れ子クラスをテスト対象にする
  @Nested
  class HalfTests{
    @Test
    void testMethod(){
      // テスト内容
    }
  }
}
// テストクラスで様々な拡張機能を使用可能にする
@ExtendWith(...Extension.class)
public class TestClass {
  // テスト
}

AssertJ

// アサーションメソッドを静的インポートする
import static org.assertj.core.api.Assertions.*;
// aとbが一致するかチェック
assertThat(a).isEqualTo(b);
// 真偽値をチェック
assertThat(a).isTrue();
assertThat(b).isFalse();
// 例外チェック
assertThatThrownBy(() -> {
  // 例外が発生する処理
}).
  isInstanceOfSatisfying(
    /* 正しい例外クラス */,
    (e) ->{
      // 発生した例外eが満たすべき条件のアサーションを記述する
    }
  );

Mockito(BDD)

// BDDMockメソッドの静的インポート
import static org.mockito.BDDMockito.*;
//JUnit5でモックの作成・注入を可能にする
@ExtendWith(MockitoExtension.class)
public class TestClass{
  // モックを使ったテスト
}
// モック
@Mock
private SubService sub;
// 内部でモックを利用する(=モックを注入される)
@InjectMocks
private Service service;
// 注入先での動作を定義する
// service.myMethod(a)が呼ばれるとbを返す
given(service.myMethod(a)).willReturn(b);
// service.myMethod()の引数にMyClass型が渡されるとcを返す
given(service.myMethod(any(MyClass.class))).willReturn(c);

今後の積み残し


トップ   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS