#author("2021-04-28T02:41:58+00:00","","")
#author("2021-04-28T02:42:10+00:00","","")
[[SpringBoot]]

- 4/5中田 : [[中田/Techメモ]]から[[SpringBoot]]に移動
- 4/2中田 : Thymeleaf追記
- 3/15中田 : PitCoinテストのまとめとして書いた
- 4/5中田

* Spring Boot Test [#i322aa14]

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

#contents

** はじめに [#ad97d481]

公式Docを読む時間がある人は[[この項目:https://docs.spring.io/spring-boot/docs/2.4.3/reference/html/spring-boot-features.html#boot-features-testing]]を自分で読むのが一番です。

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

PitCoinで実際にテストを書いてみたので、[[こちら:http://teamserv.cs.kobe-u.ac.jp:443/tnakata/pitcointest]]も参考になると思います。

** Spring Bootテスト全体の話 [#yb02716f]

*** テストで使用する技術 [#me700c2d]

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

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

- バニラJava
-- 標準のJavaの書き方です。
-- (例:OOP、アノテーション、ラムダ式、JPA)
- 頻出ライブラリ
-- Javaでよく使われるライブラリです。
-- (例:Jackson)
- テスト
-- テストの本体です。
-- (例:JUnit5、JUnit4)
- アサーション
-- テストで満たすべき条件を記述します。
-- (例:AssertJ)
- DI
-- Bean注入です。
-- (例:@Bean、@Autowired、コンポーネントスキャン)
- モッキング
-- 注入するBeanをモック(はりぼてクラス)にすることで、クラスを切り離してテストができます。これがDIの主目的です。
-- (例:Mockito)
- Auto-configuration
-- Springの主機能で、依存性を記述するだけで指定した範囲の機能が有効化され、Beanが構成されます。

*** Bootテストの特徴 [#kc0bd9fb]

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

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

- JUnit 5 : テストライブラリ
- AssertJ : アサーションライブラリ
- Mockito : モッキングフレームワーク

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

Auto-configurationの内容は、[[@SpringBootTest:https://docs.spring.io/spring-boot/docs/2.4.3/reference/html/appendix-auto-configuration-classes.html#auto-configuration-classes]]、[[スライス:https://docs.spring.io/spring-boot/docs/2.4.3/reference/html/appendix-test-auto-configuration.html]]を参照してください。

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


** 具体的なテスト手法 [#yb49ccc2]

*** Repository単体テスト(Spring Data JPA) [#y6b75efb]

[[公式Doc:https://docs.spring.io/spring-boot/docs/2.4.3/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-autoconfigured-jpa-test]]を参照。

 //公式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の結合テスト [#f06172db]

インメモリではなく、設定ファイルで定義した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の単体テスト [#vddb80ad]

 // 静的インポート(他のインポートは省略)
 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を付加することで、モックの生成を行っています。

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

- given().willReturn()の構文でモックメソッドの動作を定義し、
- テスト対象のServiceメソッドを動かし、
- アサーション

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

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

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

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

*** Service+Repository+DB結合テスト[#h6a24d85]

 // 静的インポート(他のインポートは省略)
 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クラス以下の結合テストです。上の例では特にモックは使用していません。

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

- TestEntityManagerでDBを初期化
- テスト対象のServiceを動かす
- 戻り値等をアサーション
- DBの中身をRepositoryで取得してアサーション

**** 結合テストクラスアノテーション [#qd3a2eb4]

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

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

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

- @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
-- 基本のコンポーネントスキャンを全て行う
-- 基本のAuto-configurationを全て行う(一部行わない)
-- Webサーバーを非起動にする(サーバーに関しては後述)
- @AutoConfigureTestEntityManager
-- TestEntityManagerのBeanを生成する
- @Transactional
-- テストごとにトランザクションをロールバックする

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

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

- @DataJpaTest
-- Repositoryをスキャンする
-- DataJpaまわりのAuto-configurationを行う
-- テストごとにトランザクションをロールバックする
-- TestEntityManagerのBeanを生成する
-- インメモリDBを利用する(下で打ち消し)
- @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
-- インメモリDBを利用しない(上の最後の項目の打ち消し)
- @Import(MyService.class)
-- 必要なServiceをスキャンする

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

*** Controller単体テスト(MVCテスト) [#f9290307]

[[公式Doc:https://docs.spring.io/spring-boot/docs/2.4.3/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-autoconfigured-mvc-tests]]を参照。

 // 静的インポート
 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(つまり@RestControllerも含む)、@ControllerAdviceなどのスキャン
- MockMvcのBean生成
- JacksonのObjectMapperのBean生成
- @MockBeanが使用可能
- Spring Securityの有効化(依存関係があれば)
- SpringのValidationの有効化(依存関係があれば)

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

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

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

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

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

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

Spring Securityのテストをしたい方は、自分は当分使う機会がないので検証していませんが、[[こちら:https://docs.spring.io/spring-boot/docs/2.4.3/reference/html/howto.html#howto-use-test-with-spring-security]]を参照してください。うまくいった場合は情報提供お願いします。

Thymeleafも検証可能です

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

*** 全体(Controller+Service+Repository+DB)結合テスト [#p1df6f6e]

 // 静的インポート
 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{
     // テスト内容
   }
 
 }

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

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

- サーバーを起動しなくともテスト結果に影響が出なさそうだから
- サーバーを起動するとMockMvcが使えなくなり、記法を変更するコストが大きいから
-- MockMvcの代替1 : TestWebClient
--- 利点 : サーバーの起動・非起動に関わらず使用可能
--- 欠点 : WebFluxとかいう未知技術, RepositoryがFlux対応でないとバグる?(要検証)
-- MockMvcの代替2 : TestRestTemplate
--- 利点 : RestTemplateと同様に使える
--- 欠点 : ''将来的にRestTemplateともども廃止予定''

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

- TestEntityManagerでDBを初期化する。
- MockMvcで疑似REST呼び出しを行い、アサーションを行う
- DBの中身をRepositoryで呼び出し、アサーションを行う

** テストで使用するライブラリの簡単な使い方 [#cd810d25]

*** JUnit5 [#w8214240]

 // テストメソッド
 @Test
 void testMethod(){
   // テスト内容
 }

 // 各テスト前に実行するメソッド
 @BeforeEach
 void beforeEach(){
   // DBの初期化等
 }

 class AllTests{
   // 入れ子クラスをテスト対象にする
   @Nested
   class HalfTests{
     @Test
     void testMethod(){
       // テスト内容
     }
   }
 }

 // テストクラスで様々な拡張機能を使用可能にする
 @ExtendWith(...Extension.class)
 public class TestClass {
   // テスト
 }

*** AssertJ [#i285d123]

 // アサーションメソッドを静的インポートする
 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) [#k763d9ab]

 // 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);

** 今後の積み残し [#m1c4c104]

- 外部API呼び出しのモック
- サービス間連携でのテスト

トップ   編集 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS