投稿日:
2019年5月31日
最終更新日:
【SpringBoot2】JUniteに取って代わりそうな「Spock」を使ってコントローラーのユニットテストをやってみよう【TDD】
YouTubeも見てね♪
【最新機種】GoPro hero11 Black
最新機種でVlogの思い出を撮影しよう!
ペヤング ソースやきそば 120g×18個
とりあえず保存食として買っておけば間違いなし!
モンスターエナジー 355ml×24本 [エナジードリンク]
脳を活性化させるにはこれ!
ドラゴンクエスト メタリックモンスターズギャラリー メタルキング
みんな大好き経験値の塊をデスクに常備しておこう!
Bauhutte ( バウヒュッテ ) 昇降式 L字デスク ブラック BHD-670H-BK
メインデスクの横に置くのにぴったりなおしゃれな可動式ラック!
MOFT X 【新型 ミニマム版】 iPhone対応 スマホスタンド
Amazon一番人気のスマホスタンド!カード類も収納出来てかさ張らないのでオススメです!
サンディスク microSD 128GB
スマホからSwitchまで使える大容量MicroSDカード!
目次
Spockを使おう
時間がないからユニットテストを書かないは悪
皆さんはシステム開発をする際にユニットテストは書いていますでしょうか?
スケジュールがパツパツだから、ユニットテストは後回し!といった感じの案件も正直少なくないと思います。
しかし、ユニットテストがかかれないまま機能追加や機能改修が発生すると、結局ユニットテストを追加する余裕が出ずに、ユニットテストでカバー出来る部分をテスターさんが自分たちでやらないといけなくなってしまったりと、長い目で見た際に工数が結果として上がってしまう可能性がとても高いです。
テスト駆動開発の流行
さらに近年は、テスト駆動開発(TDD)という言葉が有名になってきているので、なおさらユニットテストを妥協するのはオススメ出来ません。
テスト駆動開発 (てすとくどうかいはつ、test-driven development; TDD) とは、プログラム開発手法の一種で、プログラムに必要な各機能について、最初にテストを書き(これをテストファーストと言う)、そのテストが動作する必要最低限な実装をとりあえず行った後、コードを洗練させる、という短い工程を繰り返すスタイルである。多くのアジャイルソフトウェア開発手法、例えばエクストリーム・プログラミングにおいて強く推奨されている。近年はビヘイビア駆動開発へと発展を遂げている。テスト駆動開発 – Wikipedia
JUnitとSpock
では、ユニットテストを書く際に利用するのフレームワークは何を使えば良いのでしょうか。
Java開発のユニットテストといえば、JUnitが有名ですよね。
JUnitとはJavaで開発されたプログラムにおいてユニットテスト(単体テスト)の自動化を行うためのフレームワークである。JUnit – Wikipedia
最新バージョンはJUnit5であり、もちろんSpringBootもサポートしています。
そんなJUnit一強時代を崩すべく登場したのがSpockです。
Spock
the enterprise ready specification frameworkSpock
Spockとは、Groovy言語で動作するテスティングフレームワークです。
こちらは、当時JUnitが弱かったパラメータ化テスト(Parameterized Test)や、実施結果が直感的に分かりづらい、などといったデメリットを補うべくして生まれました。
もちろん、SpringBootもSpockの利用を推奨しており、手厚いサポートをしているため導入しやすいです。
さらには日本語ドキュメントも用意されています。
最高かよ!!!
今回はプロジェクトへの導入方法から簡単なコントローラークラスのテスト方法までをご紹介しようと思います。
本来はtest用のprofileを用意したり、サービス層配下はStub化する必要がありますが、今回はその辺は触れないので次回以降に余裕があったら対応してみようと思います。
手順
依存関係の追加
まずは依存関係をbuild.gradle
に追加していきましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
buildscript {
apply from: 'property.gradle'
...
(略)
...
dependencyManagement {
}
dependencies {
implementation "org.springframework.boot:spring-boot-starter:${accountService.springBootVersion}"
implementation "org.springframework.boot:spring-boot-starter-data-jpa:${accountService.springBootVersion}"
implementation "org.springframework.boot:spring-boot-starter-json:${accountService.springBootVersion}"
implementation "ch.qos.logback.contrib:logback-json-classic:${accountService.logbackJsonVersion}"
implementation "ch.qos.logback.contrib:logback-jackson:${accountService.logbackJsonVersion}"
testImplementation "org.springframework.boot:spring-boot-starter-test:${accountService.springBootVersion}" // 追加
testImplementation "org.spockframework:spock-core:${accountService.spockVersion}"// 追加
testImplementation "org.spockframework:spock-spring:${accountService.spockVersion}"// 追加
testCompileOnly "ch.qos.logback.contrib:logback-json-classic:${accountService.logbackJsonVersion}"
testCompileOnly "ch.qos.logback.contrib:logback-jackson:${accountService.logbackJsonVersion}"
}
test {
testLogging {
events 'started', 'skipped', 'passed', 'failed'
}
}
}
|
property.gradle
にもSpockのバージョンを追加します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
ext {
accountService = [:]
accountService.springBootVersion = '2.1.3.RELEASE'
accountService.sourceCompatibility = 1.8
accountService.group = 'tech.blogenist.service'
accountService.version = '0.0.1-SNAPSHOT'
accountService.lombokVersion = '1.18.6'
accountService.flywayVersion = '5.1.3'
accountService.postgresVersion = '42.2.5'
accountService.snakeyamlVersion = '1.19'
accountService.awsSdkVersion = '1.11.483'
accountService.cloudWatchLogbackAppenderVersion = '1.11'
accountService.logbackJsonVersion = '0.1.5'
accountService.applicationYmlPath = "../api/src/main/resources/"
accountService.spockVersion = "1.3-groovy-2.5" // 追加
}
|
テストクラスの作成
次にテストクラスを作成しましょう。
SpockはGroovy言語で作るので、 src/test/groovy
配下に作成する必要があります。
例の如く、テスト対象と同パッケージ階層にする必要があるので気をつけましょう。
今回はAccountDetailController
のテストクラスを作成してみようと思います。
テストクラスには、Specification
クラスを継承させる必要があります。
また、クラス名はSpec
で終わらせるのがルールとなっています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
package tech.blogenist.service.account.api.controller.account.detail
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.web.context.WebApplicationContext
import groovy.json.JsonSlurper
import spock.lang.Specification
import spock.lang.Unroll
import tech.blogenist.service.account.api.infrastructure.rest.path.RestPathConst
import tech.blogenist.service.account.api.model.account.detail.response.AccountDetailResponseModel
import tech.blogenist.service.account.value.account.AccountDisplayNameValue
import tech.blogenist.service.account.value.account.AccountIdValue
import tech.blogenist.service.account.value.account.AccountInfomationIdValue
import tech.blogenist.service.account.value.account.AccountNameValue
import tech.blogenist.service.account.value.account.AgeValue
import tech.blogenist.service.account.value.common.datetime.MasterNameValue
@AutoConfigureMockMvc
@SpringBootTest
class AccountDetailControllerSpec extends Specification {
@Autowired
protected MockMvc mvc
@Unroll
def "【正常系】#CASE : #ACCOUNT_ID + #HTTP_STATUS"() {
setup:
def TARGET_PATH =
RestPathConst.PATH_SEPARATOR + RestPathConst.VersionPath.V1 +
RestPathConst.CategoryPath.AccountPath.BASE_PATH +
ACCOUNT_ID + RestPathConst.PATH_SEPARATOR
def parser = new JsonSlurper()
when:
def actual = mvc.perform(MockMvcRequestBuilders.get(TARGET_PATH))
.andReturn()
.getResponse()
then:
assertThat(actual.getStatus(), is(HTTP_STATUS))
def json = parser.parseText(actual.getContentAsString())
assertThat(json.accountId.value, is(RESPONSE_BODY.accountId.value))
where:
CASE | ACCOUNT_ID || HTTP_STATUS | RESPONSE_BODY
"対象のアカウントが存在する事" |10001|| 200 | AccountDetailResponseModel.builder()
.accountId( AccountIdValue.of(10001) )
.accountInfomationId( AccountInfomationIdValue.of(20001 ))
.name( AccountNameValue.of("Test太郎" ))
.age( AgeValue.of(20 ))
.displayName( AccountDisplayNameValue.of("表示名" ))
.sex(MasterNameValue.of( "男性" ))
.build();
"対象のアカウントが存在する事" |10002 || 200| AccountDetailResponseModel.builder()
.accountId( AccountIdValue.of(10002) )
.accountInfomationId( AccountInfomationIdValue.of(20001 ))
.name( AccountNameValue.of("Test太郎" ))
.age( AgeValue.of(20 ))
.displayName( AccountDisplayNameValue.of("表示名" ))
.sex(MasterNameValue.of( "男性" ))
.build();
}
@Unroll
def "【異常系】#CASE : #ACCOUNT_ID + #HTTP_STATUS"() {
setup:
def TARGET_PATH =
RestPathConst.PATH_SEPARATOR + RestPathConst.VersionPath.V1 +
RestPathConst.CategoryPath.AccountPath.BASE_PATH +
ACCOUNT_ID + RestPathConst.PATH_SEPARATOR
def parser = new JsonSlurper()
when:
def actual = mvc.perform(MockMvcRequestBuilders.get(TARGET_PATH))
.andReturn()
.getResponse()
then:
assertThat(actual.getStatus(), is(HTTP_STATUS))
def json = parser.parseText(actual.getContentAsString())
assertThat(json.code, is(CODE))
assertThat(json.message, is(MESSAGE))
assertThat(json.timestamp, is(not(null)))
where:
CASE | ACCOUNT_ID || HTTP_STATUS | CODE | MESSAGE
"対象のアカウントが存在しない事" |1 || 404| "404_00001" | "共通エラーメッセージ";
}
}
|
要点の解説
@AutoConfigureMockMvc
サーバー起動せずに、MVCに関するテストを行えるようにするとても便利なアノテーション。
@SpringBootTest
SpringBootでユニットテストを行う際に、application.yml
を読み込んだりなどの準備をよしなにやってくれる魔法のアノテーション。
MockMvc
上記のアノテーションを追加することで、利用可能になるモッククラスです。
こちらを使うことで擬似的にリクエストを投げていると同等の動きを再現する事が可能になります。
@Unroll
後述するパラメタライズテスト時に、そのパラメーター単位でテスト結果をログやレポートに出力してくれます。
また、ケースとなる関数名にパラメタライズで指定した変数をセットする事が可能なので、レポートもとても見やすくなります。
1
2
3
4
5
6
7
8
9
|
// @Unrollがある場合
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】対象のアカウントが存在する事 : 10001 + 200 STARTED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】対象のアカウントが存在する事 : 10001 + 200 PASSED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】対象のアカウントが存在する事 : 10002 + 200 STARTED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】対象のアカウントが存在する事 : 10002 + 200 PASSED
// @Unrollがない場合
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】#CASE : #ACCOUNT_ID + #HTTP_STATUS STARTED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】#CASE : #ACCOUNT_ID + #HTTP_STATUS PASSED
|
setup句
setup句ではテストを行うための事前処理を行う際に記述します。
when句
実際に行う処理を記述します。
then句
処理結果の比較処理を行います。
when句
パラメタライズテストを行う際にデータを定義する事が出来ます。
Spockでは、マークダウンのテーブルのようにパイプ(|
)で仕切る事で項目を定義する、データパイプと言う記法を使う事が出来ます。
1行目には項目(ヘッダー)を指定し、2行目以降に実際のパラメーターを定義する事で、その行単位で関数がsetup句から実行されていきます。
実行
では実際にユニットテストを実行してみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
> Task :api:test
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】対象のアカウントが存在する事 : 10001 + 200 STARTED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】対象のアカウントが存在する事 : 10001 + 200 PASSED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】対象のアカウントが存在する事 : 10002 + 200 STARTED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】対象のアカウントが存在する事 : 10002 + 200 PASSED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【異常系】対象のアカウントが存在しない事 : 1 + 404 STARTED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【異常系】対象のアカウントが存在しない事 : 1 + 404 PASSED
|
すべて正常に完了すれば上記のようにすべてPASSED
になると思います。
エラーがある場合はこんな感じになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
> Task :api:test
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】対象のアカウントが存在する事 : 10001 + 404 STARTED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】対象のアカウントが存在する事 : 10001 + 404 FAILED
org.spockframework.runtime.ConditionFailedWithExceptionError at AccountDetailControllerSpec.groovy:54
Caused by: java.lang.AssertionError at AccountDetailControllerSpec.groovy:54
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】対象のアカウントが存在する事 : 10002 + 200 STARTED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【正常系】対象のアカウントが存在する事 : 10002 + 200 PASSED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【異常系】対象のアカウントが存在しない事 : 1 + 404 STARTED
tech.blogenist.service.account.api.controller.account.detail.AccountDetailControllerSpec > 【異常系】対象のアカウントが存在しない事 : 1 + 404 PASSED
3 tests completed, 1 failed
> Task :api:test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':api:test'.
> There were failing tests. See the report at: file:///Users/blogenist/develop/git/github/service-account/api/build/reports/tests/test/index.html
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
* Get more help at https://help.gradle.org
BUILD FAILED in 20s
11 actionable tasks: 2 executed, 9 up-to-date
|
エラーレポート
そして、Spockの一番の強みがユニットテストレポート機能が標準搭載されている事です。
ユニットテスト失敗時のログに出ているファイルにアクセスしてみましょう。
おいおい神かよ。
シンプルで見やすいレポートが生成されています。
比較結果の詳細が記載されているので、調査がしやすいですね。
もちろん、すべてPASSED
になると以下のようなレポートが生成されます。
また、HTML形式なのでCIツールと合わせて結果をサーバーなどにおけば、ユニットテストの結果が管理もしやすいです。
終わりに
以上のように、今まではJUnit一択だったユニットテストですが、Spockと言う新しいフレームワークが登場した事でユニットテストの環境が変わってきています。
なんにせよ、ユニットテストの後回しは自分の首を絞める事になるので必ず初期開発から導入するようにしてみると幸せになれると思います♪