NHN Cloud Meetup 編集部
Spring Boot Test
2017.08.28
14,264
Spring Bootのテスト機能を簡単にまとめました。
Spring Boot公式文書を整理したレベルですが、今後Spring Bootアプリケーションを開発、テストする方の参考になれば幸いです。
Spring Bootでテストを
Spring Bootは、アプリケーションをテストできるたくさんの機能を提供しています。Spring Bootのテストモジュールは、spring-boot-testとspring-boot-test-autoconfigureがあります。ほとんどの場合は、spring-boot-starter-testだけでも十分です。spring-boot-starter-testは、Spring Bootのテストに使用されるStarterパッケージで、JUnitはもちろん、AssertJ、Hamcrestなど、さまざまなライブラリが含まれています。
主なライブラリ
既存のSpring frameworkで使用していたspring-testの他にも、さまざまな有用なライブラリが含まれています。
ライブラリにはMockitoもあり、基本的にMockito1.xバージョンを使用していますが、必要に応じて2.xバージョンを使用することもできます。
- JUnit
- Spring Test&Spring Boot Test
- AssertJ
- Hamcrest
- Mockito
- JSONassert
- JsonPath
@SpringBootTest
spring-boot-testは、@SpringBootTestというアノテーションを提供しています。このアノテーションを使用すると、テストに使うApplicationContextを簡単に作成して操作できます。既存のspring-testで使用していた@ContextConfigurationの発展した機能と言えます。
@SpringBootTestは非常に多くの機能を提供しています。すべてのBeanの中から特定のBeanを選択して作成したり、特定のBeanをMockに代替したり、テストに使用するプロパティファイルを選択したり、特定のプロパティのみを追加したり、特定のConfigurationを選択して設定することもできます。また主な機能として、テストWeb環境を自動で設定する機能があります。
前述したさまざまな機能を使用する際、最も重要なことは、@SpringBootTest機能は必ず@RunWith(SpringRunner.class)と一緒に使用する必要があるという点です。
Bean
@SpringBootTestアノテーションを使うと、テストに使用できるBeanを非常に簡単に作成することができます。@SpringBootTest
アノテーションは、classesというプロパティを提供しており、当該プロパティを用いてBeanを生成するクラスを指定することができます。classes属性に@Configuration
アノテーションを使用するクラスがある場合、内部で@Beanアノテーションを用いて生成される頻度が登録されます。classes属性を用いてクラスを指定しない場合は、アプリケーション上に定義されたすべてのBeanを作成します。
@RunWith(SpringRunner.class) @SpringBootTest(classes = {ArticleServiceImpl.class, CommonConfig.class}) public class SomeClassTest { // Serviceで登録するBean @Autowired private ArticleServiceImpl articleServiceImpl; // CommonConfigで作成するBean @Autowired private RestTemplate restTemplate; }
TestConfiguration
従来定義したConfigurationをカスタマイズしたい場合は、TestConfiguration機能が使用できます。TestConfigurationはComponentScan過程で生成され、自分が属するテストが実行されるとき、定義されたBeanを作成して登録します。
@RunWith(SpringRunner.class) @SpringBootTest public class TestConfigArticleServiceImplTest { @MockBean private ArticleDao articleDao; @Autowired private RestTemplate restTemplate; @Autowired private ArticleServiceImpl articleServiceImpl; @Test public void test() { String good = restTemplate.getForObject("test", String.class); assertThat(good).isEqualTo("Good"); } @TestConfiguration public static class TestConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate() { @Override public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException { System.out.println("Good"); if (responseType == String.class) { return (T) "Good"; } else { throw new IllegalArgumentException(); } } }; } } }
ComponentScanを通じて検出されるため、万が一@SpringBootTestのclasses属性を利用して特定のクラスだけを指定した場合には、TestConfiguationは検出されません。そのような場合はclasses属性に直接TestConfigurationを追加する必要があります。しかしより良い方法は、@Importアノテーションを使用することです。@Importアノテーションを介して使用するTestConfigurationが明示でき、特定のテストクラスの内部クラスではない別途クラスに分離して、さまざまなテストで共有することもできます。
@TestConfiguration public class TestConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate() { @Override public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException { System.out.println("Good"); if (responseType == String.class) { return (T) "Good"; } else { throw new IllegalArgumentException(); } } }; } }
@RunWith(SpringRunner.class) @SpringBootTest(classes = ArticleServiceImpl.class) @Import(TestConfig.class) public class TestConfigArticleServiceImplTest { @MockBean private ArticleDao articleDao; @Autowired private RestTemplate restTemplate; @Autowired private ArticleServiceImpl articleServiceImpl; @Test public void test() { String good = restTemplate.getForObject("test", String.class); assertThat(good).isEqualTo("Good"); } }
MockBean
spring-boot-testパッケージはMockitoが含まれているため、従来のようにMockオブジェクトを生成してテストする方法もありますが、spring-boot-testでは新しい方法も提供しています。@MockBeanアノテーションを使って、名前の通りMockオブジェクトをBeanに登録できます。そのため、もし@MockBeanで宣言されたBeanを注入するなら(@Autowired同じアノテーションなどを通じて)SpringのApplicationContextはMockオブジェクトを注入します。
新たに@MockBeanを宣言するとMockオブジェクトをBeanで登録しますが、@MockBeanで宣言したオブジェクトと同じ名前のタイプがすでに登録されている場合は、当該Beanは宣言したMock Beanに置き換えられます。
@RunWith(SpringRunner.class) @SpringBootTest(classes = ArticleServiceImpl.class) public class ArticleServiceImplTest { @MockBean private RestTemplate restTemplate; @MockBean private ArticleDao articleDao; @Autowired private ArticleServiceImpl articleServiceImpl; @Test public void testFindFromDB() { List<Article> expected = Arrays.asList( new Article(0, "author1", "title1", "content1", Timestamp.valueOf(LocalDateTime.now())), new Article(1, "author2", "title2", "content2", Timestamp.valueOf(LocalDateTime.now()))); given(articleDao.findAll()).willReturn(expected); List<Article> articles = articleServiceImpl.findFromDB(); assertThat(articles).isEqualTo(expected); } }
Properties
Spring Bootは、基本的にクラスパス上、application.properties(またはapplication.yml)を通じてアプリケーションの設定を行います。しかし、テスト中は設定が既存と異なる場合が多いので、その機能をSpringBootTestで提供しています。SpringBootTest
は、propertiesという属性が存在します。この属性を使って、別のテストのapplication.properties(またはapplication.yml
)を指定できます。
@RunWith(SpringBoot.class) @SpringBootTest(properties = "classpath:application-test.yml") public class SomeTest { ... }
Web Environment test
前述のとおり@SpringBootTestアノテーションを使うと、簡単にWebテスト環境を構成できます。@SpringBootTestのwebEnvironmentパラメータを利用するとWebテスト環境を簡単に選択することができる。提供する設定値は以下のとおりです。
- MOCK
- WebApplicationContextをロードし、内蔵されたサーブレットコンテナではなく、Mockサーブレットを提供する。@AutoConfigureMockMvcアノテーションを使うと、特別な設定を行う必要がなく、簡単にMockMvcを使用したテストができる。
- RANDOM_PORT
- EmbeddedWebApplicationContextをロードし実際のサーブレット環境を構成する。生成されたサーブレットコンテナは任意のポートをlistenする。
- DEFINED_PORT
- RAMDOM_PORTと同様に、実際のサーブレット環境を構成するが、ポートはアプリケーションのプロパティで指定されたポートをlistenする。(application.propertiesまたはapplication.ymlで指定したポート)
- NONE
- 一般的なApplicationContextをロードし、サーブレット環境は構成しない。
TestRestTemplate
@SpringBootTestとTestRestTemplate
を使うと簡単にWeb統合テストを行うことができます。TestRestTemplateは名前が示すようにRestTemplate
のテストためのバージョンです。@SpringBootTest
でWeb Environmentを設定をしたら、TestRestTemplate
はそれに合わせて自動的に設定されてBeanが生成されます。
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class RestApiTest { @Autowired private TestRestTemplate restTemplate; @Test public void test() { ResponseEntity<Article> response = restTemplate.getForEntity("/api/articles/1", Article.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNotNull(); ... } }
既存のコントローラをテストするため頻繁に使用されていたMockMvcとどのような違いがあるのか気になります。最大の違いは、Servlet Containerの使用可否です。MockMvc
はServlet Containerを生成しませんが、一方で@SpringBootTest
とTestRestTemplate
はServlet Containerを使用します。そのため実際のサーバーが動作しているかのように(もちろんいくつかのBeanをMockオブジェクトに置き換えることがありますが)テストを実行できます。また、テストする観点もお互いに異なります。MockMvc
はサーバーの立場から、実装されたAPIを介してビジネスロジックが正常に実行されるか検証し、TestRestTemplate
はクライアントの立場から、RestTemplateを使用してテストを実行できます。
トランザクション
このとき注意すべき点は、@Transactionalアノテーションです。spring-boot-testは、ただspring-testを拡張したものであるため、@Testアノテーションと一緒に@Transactionalアノテーションを使うと、テストが終了するとロールバックされます。しかし、RANDOM_PORTやDEFINED_PORTでテストを設定すると、実際のテストサーバーとは別のスレッドで実行されるため、ロールバックは行われません。
ApplicationContextキャッシュ
ちなみに@SpringBootTest機能により生成されたApplicationContextはキャッシュされます。もし@SpringBootTestの設定が同じなら、同じApplicationContextを使用することになります。
@JsonTest
@JsonTestアノテーションを使用すると、より簡単にJSON serializationとdeserializationをテストできます。@JsonTestアノテーションは、ObjectMapperと@JsonComponentのBeanを含むJacksonのテストのモジュールを自動で設定します。
テストのためのBeanとしてJacksonTester、GsonTester、BasicJsonTesterなどがあります。これを注入して使用すると、より簡単にJSONをテストできます。またAssertjはJSONの機能を提供しています。(JSONassert、JsonPath基盤)下記はJSON serializeとDeserializeをテストサンプルです。
@RunWith(SpringRunner.class) @JsonTest public class ArticleJsonTest { @Autowired private JacksonTester<Article> json; @Test public void testSerialize() throws IOException { Article article = new Article( 1, "kwseo", "good", "good article", Timestamp.valueOf((LocalDateTime.now()))); // assertThat(json.write(article)).isEqualToJson("expected.json"); 直接ファイルと比較 assertThat(json.write(article)).hasJsonPathStringValue("@.author"); assertThat(json.write(article)) .extractingJsonPathStringValue("@.title") .isEqualTo("good"); } @Test public void testDeserialize() throws IOException { Article article = new Article( 1, "kwseo", "good", "good article", new Timestamp(1499655600000L)); String jsonString = "{\"id\": 1, \"author\": \"kwseo\", \"title\": \"good\", \"content\": \"good article\", \"createdDate\": 1499655600000}"; assertThat(json.parse(jsonString)).isEqualTo(article); assertThat(json.parseObject(jsonString).getAuthor()).isEqualTo("kwseo"); } }
@WebMvcTest
server-sideでAPIをテストする@WebMvcTestアノテーションについて調べます。このアノテーションは従来のspring-testでコントローラをテストするときによく使用していたMockMvc
の設定を自動で実行するアノテーションです。@WebMvcTest
アノテーションを使うと、テストに使用する@Controllerクラスと@ControllerAdvice、@JsonComponent、@Filter、WebMvcConfigurer、HandlerMethodArgumentResolver
などをスキャンします。そしてMockMvc
を自動設定してBeanに登録します。
@RunWith(SpringRunner.class) @WebMvcTest(ArticleApiController.class) public class ArticleApiControllerTest { @Autowired private MockMvc mvc; @MockBean private ArticleService articleService; @Test public void testGetArticles() throws Exception { List<Article> articles = asList( new Article(1, "kwseo", "good", "good content", now()), new Article(2, "kwseo", "haha", "good haha", now())); given(articleService.findFromDB(eq("kwseo"))).willReturn(articles); mvc.perform(get("/api/articles?author=kwseo")) .andExpect(status().isOk()) .andExpect(jsonPath("@[*].author", containsInAnyOrder("kwseo", "kwseo"))); } private Timestamp now() { return Timestamp.valueOf(LocalDateTime.now()); } }
Async Web Test
コントローラからFuture、またはDeferredResult
のオブジェクトを返すと、HTTPリクエストとレスポンスは非同期で動作します。既存と異なる方法で動作するにはMockMvcで若干、テスト方法の変更が必要です。
... @Test public void testGetArticle() throws Exception { Article expected = new Article(1, "kwseo", "good", "good content", now()); given(articleService.findOneFromRemote(eq(1))).willReturn(expected); MvcResult result = mvc.perform(get("/api/articles/1")).andReturn(); mvc.perform(asyncDispatch(result)) // asyncDispatch必要 .andExpect(status().isOk()) .andExpect(jsonPath("@.id").value(1)); } ...
上記のコードのようにMockMvcで要請した後、MvcResult
で受信してasyncDispatchでラップする必要があります。
@DataJpaTest
Spring Data JPAをテストしたい場合、@DataJpaTest機能を使用できます。このアノテーションと一緒にテストを実行すると、基本的にin-memory embedded databaseを作成し、@Entityクラスをスキャンします。一般的な他のコンポーネントはスキャンしません。
ちなみに@DataJpaTestは@Transactional
アノテーションを含んでおり、テストが完了すると自動でロールバックされるため@Transactional
アノテーションを行う必要がありません。もし@Transactional機能が不要であれば、以下のように設定できます。
@RunWith(SpringRunner.class) @DataJpaTest @Transactional(propagation = Propagation.NOT_SUPPORTED) public class SomejpaTest { ... }
@DataJpaTest機能を使うと、@Entityをスキャンしてリポジトリを設定する他にもテスト用にTestEntityManagerというBeanが生成されます。このBeanを使って、テストに用いたデータを定義できます。以下は@DataJpaTest
を使ってテストを実行するサンプルです。
@RunWith(SpringRunner.class) @DataJpaTest public class ArticleDaoTest { @Autowired private TestEntityManager entityManager; @Autowired private ArticleDao articleDao; @Test public void test() { Article articleByKwseo = new Article(1, "kwseo", "good", "hello", Timestamp.valueOf(LocalDateTime.now())); Article articleByKim = new Article(2, "kim", "good", "hello", Timestamp.valueOf(LocalDateTime.now())); entityManager.persist(articleByKwseo); entityManager.persist(articleByKim); List<Article> articles = articleDao.findByAuthor("kwseo"); assertThat(articles) .isNotEmpty() .hasSize(1) .contains(articleByKwseo) .doesNotContain(articleByKim); } }
テストにin-memory embedded databaseではなく、real databaseを使う場合は、@AutoConfigureTestDatabaseアノテーションを使うと簡単に設定できます。
@RunWith(SpringRunner.class) @DataJpaTest @AutoConfigureTestDatabase(replace = Replace.NONE) public class SomeJpaTest { ... }
@JdbcTest
Spring Data JPAを使わなくてもデータベースのテストは実施できます。@JdbcTestは@DataJpaTestと同じような設定を行いますが、純粋なJDBCのテストを準備します。@JdbcTestアノテーションを使用すると、同様にin-memory embedded databaseが設定され、テスト用のJdbcTemplateが生成されます。
@DataMongoTest
最近ますます人気を博しているNoSQL DBのMongoDBにも便利なテスト機能を提供しています。@DataMongoTestアノテーションが当該機能を提供しており、設定内容は@DataJpaTestと類似しています。上記の他データのテストモジュールと同様に、in-memory embedded MongoDBを使用しますが、@DataMongoTest
は@Entityではなく、@Document
をスキャンしてMongoTemplate
を生成します。
@RunWith(SpringRunner.class) @DataMongoTest public class SomeMongoTest { @Autowired private MongoTemplate mongoTemplate; ... }
in-memory embedded MongoDBではなく、外部に構築したMongoDBを使用する場合は、以下のように属性を追加します。
@DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class)
@RestClientTest
@RestClientTest機能は、自分がサーバーではなく、クライアントの立場となるコードをテストするときに便利です。例えば、Apache HttpClientやSpringのRestTemplateを使って、外部サーバーにWeb要求を送信する場合があります。@RestClientTest
は、要求に応答する仮想のMockサーバーを作成する、と想像してみましょう。内部コードでWeb要求が発生した場合、@RestClientTest
によって作成された仮想サーバーが応答します。もちろん、その仮想サーバーがどのように応答するかを定義することもできます。これを使用すると、RestTemplateのようなオブジェクトをMockオブジェクトに変えてテストするよりも、リアル環境に近い単体テストを実行できます。この機能を使うと、自動でMockRestServiceServer
と呼ばれるBeanが生成され、これを利用すると簡単に要求と応答の設定を行うことができます。
@RunWith(SpringRunner.class) @RestClientTest(ArticleServiceImpl.class) public class ArticleServiceImplWithRestClientTest { @MockBean private ArticleDao dao; @Autowired private ArticleServiceImpl service; @Autowired private MockRestServiceServer server; @Test public void testGetFindOneFromRemote() throws Exception { String articleJson = "{ \"id\": 1, \"author\": \"kwseo\", \"title\": \"gogogo\", \"content\": \"good\", \"date\": 1502322765 }"; server.expect(requestTo("http://sample.com/some/articles/1"%29%29 .andRespond(withSuccess(articleJson, MediaType.APPLICATION_JSON)); Article article = service.findOneFromRemote(1); assertThat(article.getId()).isEqualTo(1); } }