Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その17 )( Spock を 1.1-groovy-2.4 → 1.2-groovy-2.5 へバージョンアップする )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
参照したサイト・書籍
Allow usage of Groovy 2.5
https://github.com/spring-projects/spring-boot/issues/13444Release Notes - Peter Niederwieser, The Spock Framework Team Version 1.2
http://spockframework.org/spock/docs/1.2/release_notes.htmlChangelog for Groovy 2.5.0
http://www.groovy-lang.org/changelogs/changelog-2.5.0.htmlspock/docs/module_spring.adoc
https://github.com/spockframework/spock/blob/master/docs/module_spring.adocSpock 1.2 Annotations for Spring Integration Testing
https://objectpartners.com/2018/06/14/spock-1-2-annotations-for-spring-integration-testing/Spocklight: Indicate Specification As Pending Feature
http://mrhaki.blogspot.com/2017/06/spocklight-indicate-specification-as.htmlREST Client Testing With MockRestServiceServer
https://objectpartners.com/2013/01/09/rest-client-testing-with-mockrestserviceserver/SPR-14458 - Mock Exception happening pre/during http call
https://github.com/spring-projects/spring-framework/pull/1954/filesNo further requests expected while MockRestServiceServer was set to ExpectedCount.manyTimes()
https://stackoverflow.com/questions/50239757/no-further-requests-expected-while-mockrestserviceserver-was-set-to-expectedcoun/50241367#50241367
目次
手順
build.gradle を変更する
dependencyManagement { imports { // mavenBom は以下の URL のものを使用する // https://repo.spring.io/release/org/springframework/boot/spring-boot-starter-parent/2.0.6.RELEASE/ // bomProperty に指定可能な property は以下の URL の BOM に記述がある // https://repo.spring.io/release/org/springframework/boot/spring-boot-dependencies/2.0.4.RELEASE/spring-boot-dependencies-2.0.6.RELEASE.pom mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) { // Spring Boot の BOM に定義されているバージョンから変更する場合には、ここに以下のように記述する // bomProperty "thymeleaf.version", "3.0.9.RELEASE" bomProperty "groovy.version", "2.5.4" } } } dependencies { def jdbcDriver = "org.postgresql:postgresql:42.2.5" def spockVersion = "1.2-groovy-2.5" def domaVersion = "2.19.3" ..........
- https://repo.spring.io/release/org/springframework/boot/spring-boot-dependencies/2.0.6.RELEASE/spring-boot-dependencies-2.0.6.RELEASE.pom を見ると
<groovy.version>2.4.15</groovy.version>
と記述されているので、dependencyManagement block 内にbomProperty "groovy.version", "2.5.4"
を追加して Groovy のバージョンを 2.5系にバージョンアップします。 - dependencies block 内で
def spockVersion = "1.1-groovy-2.4"
→def spockVersion = "1.2-groovy-2.5"
に変更します。
Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると、"BUILD SUCCESSFUL" のメッセージが出力されました。
@SpringBean, @SpringSpy を試してみる
Spock 1.2 から @SpringBean, @SpringSpy というアノテーションが追加されました。
src/main/java/ksbysample/webapp/lending/helper/user/UserHelper.java のテストを作成して試してみます。
@Component public class UserHelper { private final UserInfoDao userInfoDao; public UserHelper(UserInfoDao userInfoDao) { this.userInfoDao = userInfoDao; } public String[] getApprovalMailAddrList() { List<String> approvalMailAddrList = userInfoDao.selectApproverMailAddrList(); return Iterables.toArray(approvalMailAddrList, String.class); } }
まずは @SpringBean から。@SpringBean アノテーションを付けると Spock の Mock を Bean として自動的に Spring の Test 用 Context に追加してくれます。
package ksbysample.webapp.lending.helper.user import ksbysample.webapp.lending.dao.UserInfoDao import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import spock.lang.Specification @SpringBootTest class UserHelper2Test extends Specification { // = Mock() を記述すること @SpringBean UserInfoDao userInfoDao = Mock() @Autowired UserHelper userHelper def "getApprovalMailAddrListメソッドのテスト"() { when: def approvalMailAddrList = userHelper.getApprovalMailAddrList() then: // 呼び出された回数のチェック("1 *")と、戻り値の定義が同時にできる 1 * userInfoDao.selectApproverMailAddrList() >> ["tanaka.taro@sample.com"] approvalMailAddrList == ["tanaka.taro@sample.com"] } }
次は @SpringSpy。@SpringSpy を付けると Mock ではなく実クラスのインスタンスを Bean として Spring の Test 用 Context に追加してくれます。またメソッド呼び出し回数を検査できるようにもなります。
package ksbysample.webapp.lending.helper.user import ksbysample.webapp.lending.dao.UserInfoDao import org.spockframework.spring.SpringSpy import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import spock.lang.Specification @SpringBootTest class UserHelper2Test extends Specification { @SpringSpy UserInfoDao userInfoDao @Autowired UserHelper userHelper def "getApprovalMailAddrListメソッドのテスト"() { when: def approvalMailAddrList = userHelper.getApprovalMailAddrList() then: // 呼び出された回数のチェック("1 *")と、戻り値の定義が同時にできる // 戻り値を定義すればそれが使われるし、定義しなければ本来の Bean のメソッドの処理が実行される // 1 * userInfoDao.selectApproverMailAddrList() >> ["tanaka.taro@sample.com"] 1 * userInfoDao.selectApproverMailAddrList() approvalMailAddrList == ["tanaka.taro@sample.com", "suzuki.hanako@test.co.jp"] } }
簡単なクラスなのにテストに 20数秒かかります。。。 これくらいならば、@SpringBean, @SpringSpy を使わずにシンプルに Mock() でテストした方が早いかもしれません。以下の書き方だと 2秒未満で終わります。
package ksbysample.webapp.lending.helper.user import ksbysample.webapp.lending.dao.UserInfoDao import spock.lang.Specification class UserHelper2Test extends Specification { UserInfoDao userInfoDao = Mock() UserHelper userHelper def setup() { userHelper = new UserHelper(userInfoDao) } def "getApprovalMailAddrListメソッドのテスト"() { setup: userInfoDao.selectApproverMailAddrList() >> ["tanaka.taro@sample.com"] when: def approvalMailAddrList = userHelper.getApprovalMailAddrList() then: approvalMailAddrList == ["tanaka.taro@sample.com"] } }
@PendingFeature を試してみる
@PendingFeature はテストが失敗する場合に failed ではなく ignored にするアノテーションです。1.2 から追加された機能ではありませんが、初めて見たので試してみます。
例えば、以下のように失敗するテストがあると通常 BUILD も失敗となりますが、
package ksbysample.webapp.lending.helper.user import ksbysample.webapp.lending.dao.UserInfoDao import spock.lang.Specification class UserHelper2Test extends Specification { UserInfoDao userInfoDao = Mock() UserHelper userHelper def setup() { userHelper = new UserHelper(userInfoDao) } def "getApprovalMailAddrListメソッドのテスト"() { when: def approvalMailAddrList = userHelper.getApprovalMailAddrList() then: approvalMailAddrList == ["tanaka.taro@sample.com"] } }
@PendingFeature が付与されたテストは失敗しても failed ではなく ignored となり、BUILD は成功します。
package ksbysample.webapp.lending.helper.user import ksbysample.webapp.lending.dao.UserInfoDao import spock.lang.PendingFeature import spock.lang.Specification class UserHelper2Test extends Specification { UserInfoDao userInfoDao = Mock() UserHelper userHelper def setup() { userHelper = new UserHelper(userInfoDao) } @PendingFeature def "getApprovalMailAddrListメソッドのテスト"() { when: def approvalMailAddrList = userHelper.getApprovalMailAddrList() then: approvalMailAddrList == ["tanaka.taro@sample.com"] } }
ちょっと面白い機能だと思いますが、開発中なら merge しなければよさそうな気がしますし、これ使う場面あるのかな。。。
@Retry を試してみる
1.2 から追加された機能です。マニュアルは こちら 。
src/main/java/ksbysample/webapp/lending/service/calilapi/CalilApiService.java に定義している restTemplateForCalilApi と、
@Bean public RestTemplate restTemplateForCalilApi() { return this.restTemplateBuilder .setConnectTimeout(CONNECT_TIMEOUT) .setReadTimeout(READ_TIMEOUT) .rootUri(CalilApiService.URL_CALILAPI_ROOT) .build(); }
MockRestServiceServer を利用して @Retry のテストを作成してみましたが、以下のことが分かりました。
- MockRestServiceServer は RestTemplate の requestFactory の定義を変更してしまうため、RestTemplate の readTimeout のテストは作成できない。
- Spock のテストに
@Slf4j
が記述できるが、これは groovy.util.logging.Slf4j。lombok.extern.slf4j.Slf4j では "log.~" と記述してもエラーになる。 - Groovy 2.5 から Java 8 の lambda は
.andRespond { request -> withTimeout().createResponse(request) }
のように記述できる。
作成してみたテストは以下のものです。最後の def "リトライして4回目で200OKのレスポンスが返る場合のテスト"()
に @Retry アノテーションを付加しており、3回は SocketTimeoutException が throw されますが 4回目で成功することでテスト自体は成功します。
※(2018/12/09追記)MockRestServiceServer を利用するテストクラスには @DirtiesContext アノテーションを付けること。RestTemplate のインスタンスを Bean として生成・利用している場合、MockRestServiceServer が RestTemplate Bean を変更するため、他のテストが失敗することがあります。
package ksbysample.webapp.lending.service.calilapi import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.http.HttpMethod import org.springframework.http.MediaType import org.springframework.http.client.ClientHttpRequest import org.springframework.http.client.ClientHttpResponse import org.springframework.test.annotation.DirtiesContext import org.springframework.test.web.client.MockRestServiceServer import org.springframework.test.web.client.ResponseCreator import org.springframework.web.client.ResourceAccessException import org.springframework.web.client.RestTemplate import spock.lang.Retry import spock.lang.Specification import static ksbysample.webapp.lending.service.calilapi.CalilApiService2Test.TimeoutResponseCreator.withTimeout import static org.springframework.test.web.client.match.MockRestRequestMatchers.method import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess @Slf4j @SpringBootTest @DirtiesContext class CalilApiServiceRetryTest extends Specification { /** * タイムアウト用 ResponseCreator * Spring Framework 5.1 になると * responseCreator = MockRestResponseCreators.withException(SocketTimeoutException).createResponse(null) * という書き方が出来るらしいので、おそらくこのようなクラスを定義する必要はない */ static class TimeoutResponseCreator implements ResponseCreator { @Override ClientHttpResponse createResponse(ClientHttpRequest request) throws IOException { if (true) { throw new SocketTimeoutException("Testing timeout exception") } return null } static TimeoutResponseCreator withTimeout() { return new TimeoutResponseCreator() } } @Autowired RestTemplate restTemplateForCalilApi def responseJson = """ { "key": "sample", "value": 1 } """.stripIndent() def retryCount def setup() { retryCount = 0 } def "200OKでレスポンスが返る場合のテスト"() { given: MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplateForCalilApi).build() mockServer.expect(requestTo(CalilApiService.URL_CALILAPI_ROOT + "/sample/")) .andExpect(method(HttpMethod.GET)) .andRespond(withSuccess(responseJson, MediaType.APPLICATION_JSON_UTF8)) when: def result = restTemplateForCalilApi.getForObject("/sample/", String) then: result == responseJson } def "タイムアウトする場合のテスト"() { given: MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplateForCalilApi).build() mockServer.expect(requestTo(CalilApiService.URL_CALILAPI_ROOT + "/sample/")) .andExpect(method(HttpMethod.GET)) .andRespond { request -> withTimeout().createResponse(request) } when: def result = restTemplateForCalilApi.getForObject("/sample/", String) then: ResourceAccessException e = thrown() e.cause instanceof SocketTimeoutException } @Retry def "リトライして4回目で200OKのレスポンスが返る場合のテスト"() { given: retryCount++ ResponseCreator responseCreator if (retryCount == 4) { responseCreator = withSuccess(responseJson, MediaType.APPLICATION_JSON_UTF8) } else { responseCreator = { request -> withTimeout().createResponse(request) } } // MockRestServiceServer.bindTo(...).build() が呼ばれると RestTemplate の requestFactory が変更されるらしい // つまり restTemplateForCalilApi のタイムアウトの設定も変更されるので、restTemplateForCalilApi のタイムアウト // のテストにはならないようだ MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplateForCalilApi).build() mockServer.expect(requestTo(CalilApiService.URL_CALILAPI_ROOT + "/sample/")) .andExpect(method(HttpMethod.GET)) .andRespond(responseCreator) when: log.warn("calling...") def result = restTemplateForCalilApi.getForObject("/sample/", String) log.error("finished!!") then: notThrown ResourceAccessException result == responseJson } }
履歴
2018/11/23
初版発行。
2018/12/09
* IntelliJ IDEA を 2018.2.6 → 2018.2.7 → 2018.3.1 へバージョンアップ の記事を反映して、CalilApiServiceRetryTest テストクラスに @DirtiesContext アノテーションを追加しました。