Upgrade to Pro — share decks privately, control downloads, hide ads and more …

今こそ知りたいSpring Test / Spring Fest 2020

Ryo Shindo
December 17, 2020

今こそ知りたいSpring Test / Spring Fest 2020

Spring Fest 2020 Beginner Track
https://springfest2020.springframework.jp/

Ryo Shindo

December 17, 2020
Tweet

More Decks by Ryo Shindo

Other Decks in Programming

Transcript

  1. 1 Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    今こそ知りたいSpring Test Spring Fest 2020 Acroquest Technology株式会社 進藤 遼(@shindo_ryo)
  2. 自己紹介 • 進藤 遼 • Acroquest Technology株式会社 • 日本Springユーザ会 スタッフ

    • Twitter: @shindo_ryo • 好きなSpring Projectは Spring Cloud Sleuth Copyright © Acroquest Technology Co., Ltd. All rights reserved. 2
  3. 目次 Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    3 1. Spring Testの基礎 2. モックテスト 3. DBテスト 4. Spring Webテスト 5. Spring Bootテスト 6. その他
  4. Spring Testとは Copyright © Acroquest Technology Co., Ltd. All rights

    reserved. 5 1. Springアプリケーションに対するインテグレーションテストを サポートするモジュール 2. JUnit, TestNGといったテスティングフレームワークと連携する ※デフォルトでJUnit 5 3. Spring MVCのテスト、DBテストなどを容易にする ユーティリティを多数提供
  5. Spring Testのインストール Copyright © Acroquest Technology Co., Ltd. All rights

    reserved. 6 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> デフォルトで設定済み
  6. SpringのDIの仕組み Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    7 DIコンテナ (ApplicationContext) Bean Bean 使う new new 依存オブジェクトを注入
  7. SpringのDIの仕組み Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    8 @Service public class FooService { private final BarService barService; public FooService(BarService barService) { this.barService = barService; } public String foobar() { return "foo" + barService.bar(); } } @Service public class BarService { private final MessageSource messageSource; public BarService(MessageSource messageSource) { this.messageSource = messageSource; } public String name() { return messageSource.getMessage( "name", null, Locale.getDefault()); } } DIコンテナが依存オブジェクトを注入
  8. 通常のユニットテスト Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    9 public class FooServiceTest { FooService service; @BeforeEach void beforeEach() { service = //何らかの方法で初期化 } @Test void testHello() { assertEquals("Hi, Spring Fest 2020", service.hello()); } }
  9. Spring Testを使わないと・・・ Copyright © Acroquest Technology Co., Ltd. All rights

    reserved. 10 1. 全てのインスタンスを自前でnew ➢ controller層からrepository層まで統合しようとすると 非常に多くのBeanが必要になる 2. 正しくDIできることをテストできない ➢ Beanが不足、または重複していても気づけない
  10. Spring Testを使うと・・・ Copyright © Acroquest Technology Co., Ltd. All rights

    reserved. 11 @ExtendWith(SpringExtension.class) ・・・① @ContextConfiguration( ・・・② locations = “classpath:/application-context.xml”) public class FooServiceTest { @Autowired ・・・③ FooService service; @Test void testHello() { assertEquals("Hi, Spring Fest 2020", service.hello()); } } ①JUnitとSpring Testを 連携させる ②ApplicationContextの 設定ファイルを指定 (classesでJava Configも 指定可能) ③テスト対象Beanを取得
  11. Spring Bootの場合 Copyright © Acroquest Technology Co., Ltd. All rights

    reserved. 12 @SpringBootTest public class FooServiceTest { @Autowired FooService service; @Test void testHello() { assertEquals("Hi, Spring Fest 2020", service.hello()); } } @SpringBootApplicationがついたクラスを探索し、 設定を自動的に読み込む。 ※@ExtendWith(SpringExtension.class)は @SpringBootTestの中で指定されている。 (Boot 2.1~)
  12. Spring TestContextフレームワーク Copyright © Acroquest Technology Co., Ltd. All rights

    reserved. 13 SpringExtension TestContextManager TestContext テストクラスと ApplicationContextを セットにして管理 ApplicationContext テストクラス @Autowired FooService service; TestExecutionListener use create Spring Bean テストコードの設定に対応 するApplicationContextを ロード 依存オブジェクトの注入と いった、テストの前処理・ 後処理を実行 ApplicationContextは キャッシュされて使い回される
  13. テスト実行フロー Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    14 テストクラスのロード ApplicationContextの初期化 テストクラスにSpring Beanを注入 各テストメソッドを実行 SpringExtension @Test JUnit
  14. スライステスト Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    15 • 全てのBeanを初期化するには時間がかかる • テスト対象に必要なBeanのみを初期化したい →スライステスト @WebMvcTest @WebFluxTest @JsonTest @RestClientTest @WebServiceClientTest @DataJpaTest @DataCassandraTest @JdbcTest @DataJdbcTest @DataLdapTest @DataMongoTest @DataNeo4jTest @DataR2dbcTest @DataRedisTest @JooqTest Boot
  15. Mockito連携 Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    17 1. spring-boot-starter-testでは、デフォルトでMockitoを使った Beanのモック化機能が提供 2. @MockBean / @SpyBean でBeanをモック化し、 振る舞いを変えることができる FooService BarService BarService @MockBean
  16. @MockBean / @SpyBean Copyright © Acroquest Technology Co., Ltd. All

    rights reserved. 18 @SpringBootTest public class FooServiceTest { @Autowired FooService service; @MockBean BarService barService; @Test void testHello() { Mockito.when(barService.name()).thenReturn("Mocked Response"); assertEquals("Hi, Mocked Response", service.hello()); } } フィールドにアノテーションを つけることで、Beanが自動的に モック化される(@SpyBeanも同様) Mock化された振る舞いを指定
  17. @MockBean / @SpyBean Copyright © Acroquest Technology Co., Ltd. All

    rights reserved. 19 @SpringBootTest @MockBean(BarService.class) public class FooServiceTest { @Autowired FooService service; @Autowired BarService barService; @Test void testHello() { Mockito.when(barService.name()).thenReturn("Mocked Response"); assertEquals("Hi, Mocked Response", service.hello()); } } クラスアノテーションでも指定可能 その場合は@Autowiredで注入する
  18. Q. ApplicationContextの起動回数は? Copyright © Acroquest Technology Co., Ltd. All rights

    reserved. 20 @SpringBootTest public class BarServiceTest { @Autowired BarService service; @MockBean MessageSource messageSource; @Test void testBar() { // 省略 } } @SpringBootTest public class FooServiceTest { @Autowired FooService service; @MockBean BarService barService; @Test void testHello() { // 省略 } } モック化する Beanが異なる
  19. @MockBeanと実行時間 Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    21 A. 2回 ⇒起動回数が増え、実行時間が遅くなりがち ApplicationContext ApplicationContext FooService BarService MessageSource FooService MessageSource BarService TestContext(FooServiceTest) TestContext(BarServiceTest) ApplicationContextが異なるため、 キャッシュが使われない モック化されたBeanは モックでないBeanとは 別のもの扱いとなる
  20. モックテストでも遅くならないようにする方法 Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    22 1. 各テストクラスでモック対象のクラスを完全に同一にする • 共通のJava Configを作って@ContextConfigurationで指定 • 一つでも違う部分があるとアウト 2. @MockBean/@SpyBeanを使わない • モックを使うテストではSpring Testを使わない • モッククラスを自作して、テスト中は@Primaryで差し替え @Service @Primary public class MockBarService { }
  21. DBテストのフロー Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    24 テストデータのセットアップ テストの実行 更新後のデータの検証 ロールバック / テストデータの破棄 テストデータの状態を一定に保つことで テストのべき等性を担保する
  22. テストデータのセットアップの2つの方法 Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    25 ① schema.sql / data.sql をクラスパスに配置する • DataSourceの初期化の段階で自動的に実行される • テストクラスごと、テストメソッドごとのような実行サイクルはできない ② テストクラス、テストメソッドに@Sqlを指定する • 任意のSQLファイルを読み込み実行する • テストメソッドの前後で実行させることができる
  23. @Sql Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    26 @DataJpaTest @Sql(scripts = "classpath:/employee.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) public class EmployeeRepositoryTest { @Autowired EmployeeRepository repository; @Test void testFind() { Employee employee = repository.findById(1L).get(); assertEquals("shindo", employee.getName()); } } TRUNCATE TABLE employee; INSERT INTO employee(employee_id, name) VALUES ('1', 'shindo'); AFTER_TEST_METHOD も指定可能
  24. トランザクション管理 Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    27 • ロールバックしないと・・・ DB テスト1回目 INSERT INTO employee (employee_id, name) VALUES(1, ‘shindo’); テスト2回目 INSERT INTO employee (employee_id, name) VALUES(1, ‘shindo’); employee (id=1, name=shindo) PK重複 employee (id=1, name=shindo)
  25. @Transactionalでロールバック Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    28 @DataJpaTest public class EmployeeRepositoryTest { @Autowired EmployeeRepository repository; @Autowired JdbcTemplate jdbcTemplate; @Autowired EntityManager entityManager; @Test @Transactional void testSave() { Employee employee = new Employee(); employee.setName("spring"); employee = repository.save(employee); entityManager.flush(); int count = JdbcTestUtils.countRowsInTable(jdbcTemplate, "employee"); assertEquals(1, count); } } テストメソッドがトランザクション境界となり、 メソッドが終わると自動でロールバックされる コミットさせたい場合は、@Transactionalの代わりに @Commitを使う
  26. JPA Repositoryの場合はflush()が必要 Copyright © Acroquest Technology Co., Ltd. All rights

    reserved. 29 @DataJpaTest public class EmployeeRepositoryTest { @Autowired EmployeeRepository repository; @Autowired JdbcTemplate jdbcTemplate; @Autowired EntityManager entityManager; @Test @Transactional void testSave() { Employee employee = new Employee(); employee.setName("spring"); employee = repository.save(employee); entityManager.flush(); int count = JdbcTestUtils.countRowsInTable(jdbcTemplate, "employee"); assertEquals(1, count); } } トランザクション境界の前にflush()して SQLを発行させる JPAについて詳しくは@suke_masaの 資料を参照
  27. 実行結果の検証 Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    30 1. JdbcTemplateでテーブルをクエリする 2. JdbcTestUtils • テーブル名を指定するだけで行数などを取得できる • 行、テーブルの削除も可能 @Test @Transactional void testSave() { Employee employee = new Employee(); employee.setName("spring"); employee = repository.save(employee); entityManager.flush(); // JdbcTemplateを使う場合 int count = jdbcTemplate.queryForObject("select count(*) from employee", Integer.class); assertEquals(1, count); // JdbcTestUtilsを使う場合 count = JdbcTestUtils.countRowsInTable(jdbcTemplate, "employee"); assertEquals(1, count); }
  28. DBテストフレームワーク Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    31 1. DBUnit ➢ xml, excelなどでテストデータのセットアップと結果の検証ができる ➢ 設定用のコードが多く必要だったり、JUnit 5に追従できていなかったりと不 便。 2. Database Rider ➢ DBUnitをベースに、アノテーションで楽に設定できるように ➢ Springとの連携も容易
  29. MockMvcとは Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    33 1. サーバーにデプロイせずに、Spring MVCのインテグレーションテ ストを支援するためのクラス 2. DispatcherServletに擬似的なリクエストを投げて、パスに対応す るHandlerを呼び出す
  30. MockMvcの仕組み Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    34 ① リクエストパスやパラメータなどを設定 ② 疑似的なリクエストを行う ③ リクエスト内容に一致するHandlerを呼び出す ④ レスポンス内容の検証を行う テストコード MockMvc DispatcherServlet Handler (Controller) Spring MVC ① ② ③ ④
  31. GETメソッドのテスト Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    35 @SpringBootTest @AutoConfigureMockMvc public class HelloControllerTest { @Autowired MockMvc mockMvc; @Test void testHello() throws Exception { mockMvc.perform( MockMvcRequestBuilders.get("/hello").accept(MediaType.TEXT_PLAIN) ) .andExpect( MockMvcResultMatchers.status().is2xxSuccessful()) .andExpect( MockMvcResultMatchers.content().string("Hi, Spring Fest 2020")); } } Spring Bootの場合は このアノテーションだけで設定可能 リクエスト内容、検証内容を メソッドチェーンで記述する
  32. GETメソッドのテスト Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    36 @SpringBootTest @AutoConfigureMockMvc public class HelloControllerTest { @Autowired MockMvc mockMvc; @Test void testHello() throws Exception { mockMvc.perform( get("/hello").accept(MediaType.TEXT_PLAIN) ) .andExpect(status().is2xxSuccessful()) .andExpect(content().string("Hi, Spring Fest 2020")); } }
  33. POSTメソッドのテスト Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    37 @Autowired ObjectMapper objectMapper; @Test void testCalc() throws Exception { CalcRequest request = new CalcRequest(); request.setLeftSide(2); request.setRightSide(3); request.setOperator("+"); mockMvc.perform( post("/calc") .content(objectMapper.writeValueAsBytes(request)) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) ) .andExpect(status().is2xxSuccessful()) .andExpect(jsonPath("$.result", CoreMatchers.is(5.0))); } JSONのリクエストは要シリアライズ JSONのフィールドを検証可能
  34. デバッグロギング Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    38 @Test void testHello() throws Exception { mockMvc.perform( get("/hello").accept(MediaType.TEXT_PLAIN) ) .andDo(MockMvcResultHandlers.print()) (後略) (一部) MockHttpServletResponse: Status = 200 Error message = null Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"20"] Content type = text/plain;charset=UTF-8 Body = Hi, Spring Fest 2020 Forwarded URL = null Redirected URL = null Cookies = [] print()の場合は標準出力 log()も利用可
  35. Web API アクセスのモック化 Copyright © Acroquest Technology Co., Ltd. All

    rights reserved. 39 Web API Springアプリケーション HTTP モック テストのときはHTTPコールを モック化したい
  36. MockRequestServiceServer Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    40 1. RestTemplateのふるまいをモック化するクラス 2. RestTemplateの内部のClientHttpRequestFactoryをモックに差 し替えることで、RestTemplateを使っているコードを変更するこ となく、HTTPコールをモック化できる RestTemplate <interface> ClientHttpRequest Factory HttpComponentsClient HttpRequestFactory MockClientHttp RequestFactory
  37. MockRequestServiceServer Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    41 @SpringBootTest class WeatherForecastApiRepositoryTest { @Autowired WeatherForecastApiRepository repository; @Autowired RestTemplate restTemplate; @Test void retrieveForecastAt() { MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build(); server.expect(MockRestRequestMatchers.method(HttpMethod.GET)) .andExpect(requestTo("http://foo.domain/forecast?location=Tokyo")) .andRespond(MockRestResponseCreators.withSuccess() .contentType(MediaType.APPLICATION_JSON) .body("{¥"location¥":¥"Tokyo¥",¥"weather¥":¥"sunny¥"}")); Forecast actual = repository.retrieveForecastAt("Tokyo"); assertEquals("Tokyo", actual.getLocation()); assertEquals("sunny", actual.getWeather()); } } RestTemplateはテスト対象のBeanを取得 RestTemplateの中身をモックに差し替え モックのふるまいを設定する
  38. @SpringBootTest Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    43 1. Spring Bootアプリケーションをテストするためのアノテーション 2. @SpringBootApplicationを検知して各種設定を自動的にロード 3. 組み込みサーブレットコンテナを実際に立ち上げて、 本物のHTTPリクエスト/レスポンスを投げてテストができる
  39. Copyright © Acroquest Technology Co., Ltd. All rights reserved. 44

    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class ApiTest { @LocalServerPort int port; @Test void testHello() { RestTemplate restTemplate = new RestTemplate(); String url = "http://localhost:" + port + "/hello"; ResponseEntity<String> entity = restTemplate.getForEntity(url, String.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); assertEquals("Hi, Spring Fest 2020", entity.getBody()); } } ランダムなポートでサーバーを起動 サーバーポートを取得 RestTemplateを使って 本物のHTTPリクエストを実行
  40. WebEnvironment Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    45 • @SpringBootTestで指定できるWebEnvironmentは下記の通り No WebEnvironment 1 MOCK (デフォルト) モックサーブレット環境を構成する。(実際のポートはリッスンしな い) @AutoConfigureMockMvcを組み合わせる。 2 RANDOM_PORT 組み込みサーブレットコンテナを起動し、ランダムなポートをリッス ンする。 3 DEFINED_PORT 組み込みサーブレットコンテナを起動し、プロパティで定義されたポ ートをリッスンする。 4 NONE ApplicationContextをロードするが、サーブレット環境を構成しな い。 バッチなどの非Webアプリケーションで利用する。
  41. MockMvcとの違い Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    46 RANDOM_PORT/ DEFINED_PORT MockMvc 実行環境 組み込みサーブレットコンテナ を実際に起動 モックサーブレット 起動時間 実環境と同一なため 低速 モック環境のため (比較的)高速 トランザクション ロールバック不可 @Transactionalで ロールバック可能 テストメソッドとは別のスレッドで処理が 実行されるため、テストコードから トランザクション管理できない
  42. TestRestTemplate Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    47 1. インテグレーションテスト用のRestTemplate 2. webEnvironment=RANDOM_PORT/DEFINED_PORT の場合に、サーバーポートが自動的に設定される 3. デフォルトでクッキーとリダイレクトが無効化されている
  43. TestRestTemplate Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    48 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class ApiTest { @Autowired TestRestTemplate restTemplate; @Test void testHello() { ResponseEntity<String> entity = restTemplate.getForEntity("/hello", String.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); assertEquals("Hi, Spring Fest 2020", entity.getBody()); } } @LocalServerPort不要
  44. WebTestClient Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    49 1. インテグレーションテスト用のWebClient 2. TestRestTemplateと同様、サーバーポートが自動的に 設定される 3. レスポンスの検証を流れるようなインターフェースで記述できる 4. 要spring-boot-starter-webflux
  45. WebTestClient Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    50 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class ApiTest { @Autowired WebTestClient client; @Test void testHello() { client.get().uri("/hello") .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hi, Spring Fest 2020"); } }
  46. MockMvcWebTestClient Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    52 @ExtendWith(SpringExtension.class) @WebMvcTest(controllers = HelloController.class) public class WebTestClientTest { @Autowired WebApplicationContext wac; WebTestClient client; @BeforeEach void setup() { client = MockMvcWebTestClient.bindToApplicationContext(wac).build(); } @Test void testWebTestClient() { client.get() .uri("/hello") .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hi, Spring Fest 2020"); } } 詳しくは@makingの 『What's New in Spring 5.3 and Spring Boot 2.4』 をチェック!
  47. アノテーションが多すぎる問題 Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    53 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SqlGroup({ @Sql(scripts = "classpath:/employee.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), @Sql(scripts = "classpath:/truncate.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) }) public class ApiTest { アノテーション多すぎ 同じレイヤーのテストに対しては、 大体同じアノテーションをつけることになる
  48. 合成アノテーションで解決 Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    54 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SqlGroup({ @Sql(scripts = "classpath:/employee.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), @Sql(scripts = "classpath:/truncate.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) }) public @interface ApiTestSupport { } @ApiTestSupport public class ApiTest { 合成アノテーションを作成して共通化する
  49. Furthermore… Copyright © Acroquest Technology Co., Ltd. All rights reserved.

    55 • WebFlux Test • spring-security-test • mybatis-spring-boot-starter-test • Testcontainers