Slide 1

Slide 1 text

1 Copyright © Acroquest Technology Co., Ltd. All rights reserved. 今こそ知りたいSpring Test Spring Fest 2020 Acroquest Technology株式会社 進藤 遼(@shindo_ryo)

Slide 2

Slide 2 text

自己紹介 • 進藤 遼 • Acroquest Technology株式会社 • 日本Springユーザ会 スタッフ • Twitter: @shindo_ryo • 好きなSpring Projectは Spring Cloud Sleuth Copyright © Acroquest Technology Co., Ltd. All rights reserved. 2

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Spring Testの基礎 Copyright © Acroquest Technology Co., Ltd. All rights reserved. 4

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

Spring Testのインストール Copyright © Acroquest Technology Co., Ltd. All rights reserved. 6 org.springframework.boot spring-boot-starter-test test デフォルトで設定済み

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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コンテナが依存オブジェクトを注入

Slide 9

Slide 9 text

通常のユニットテスト 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()); } }

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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を取得

Slide 12

Slide 12 text

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~)

Slide 13

Slide 13 text

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は キャッシュされて使い回される

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

スライステスト 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

Slide 16

Slide 16 text

モックテスト Copyright © Acroquest Technology Co., Ltd. All rights reserved. 16

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

@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化された振る舞いを指定

Slide 19

Slide 19 text

@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で注入する

Slide 20

Slide 20 text

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が異なる

Slide 21

Slide 21 text

@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とは 別のもの扱いとなる

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

DBテスト Copyright © Acroquest Technology Co., Ltd. All rights reserved. 23

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

@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 も指定可能

Slide 27

Slide 27 text

トランザクション管理 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)

Slide 28

Slide 28 text

@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を使う

Slide 29

Slide 29 text

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の 資料を参照

Slide 30

Slide 30 text

実行結果の検証 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); }

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Spring Webテスト Copyright © Acroquest Technology Co., Ltd. All rights reserved. 32

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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の場合は このアノテーションだけで設定可能 リクエスト内容、検証内容を メソッドチェーンで記述する

Slide 36

Slide 36 text

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")); } }

Slide 37

Slide 37 text

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のフィールドを検証可能

Slide 38

Slide 38 text

デバッグロギング 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()も利用可

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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の中身をモックに差し替え モックのふるまいを設定する

Slide 42

Slide 42 text

Spring Bootテスト Copyright © Acroquest Technology Co., Ltd. All rights reserved. 42

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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 entity = restTemplate.getForEntity(url, String.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); assertEquals("Hi, Spring Fest 2020", entity.getBody()); } } ランダムなポートでサーバーを起動 サーバーポートを取得 RestTemplateを使って 本物のHTTPリクエストを実行

Slide 45

Slide 45 text

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アプリケーションで利用する。

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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 entity = restTemplate.getForEntity("/hello", String.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); assertEquals("Hi, Spring Fest 2020", entity.getBody()); } } @LocalServerPort不要

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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"); } }

Slide 51

Slide 51 text

その他小ネタ Copyright © Acroquest Technology Co., Ltd. All rights reserved. 51

Slide 52

Slide 52 text

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』 をチェック!

Slide 53

Slide 53 text

アノテーションが多すぎる問題 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 { アノテーション多すぎ 同じレイヤーのテストに対しては、 大体同じアノテーションをつけることになる

Slide 54

Slide 54 text

合成アノテーションで解決 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 { 合成アノテーションを作成して共通化する

Slide 55

Slide 55 text

Furthermore… Copyright © Acroquest Technology Co., Ltd. All rights reserved. 55 • WebFlux Test • spring-security-test • mybatis-spring-boot-starter-test • Testcontainers