Anker PowerCor
旅行には必須の大容量モバイルバッテリー!
[ノースフェイス] THE NORTH FACE メンズ アウター マウンテンライトジャケット
防水暴風で耐久性抜群なので旅行で大活躍です!
レッドブル エナジードリンク 250ml×24本
翼を授けよう!
モンスターエナジー 355ml×24本 [エナジードリンク]
脳を活性化させるにはこれ!
ドラゴンクエスト メタリックモンスターズギャラリー メタルキング
みんな大好き経験値の塊をデスクに常備しておこう!
BANDAI SPIRITS ULTIMAGEAR 遊戯王 千年パズル 1/1スケール
もう一人の僕を呼び覚ませ!!
MOFT X 【新型 ミニマム版】 iPhone対応 スマホスタンド
Amazon一番人気のスマホスタンド!カード類も収納出来てかさ張らないのでオススメです!
スポンサーリンク
目次
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と言う新しいフレームワークが登場した事でユニットテストの環境が変わってきています。
なんにせよ、ユニットテストの後回しは自分の首を絞める事になるので必ず初期開発から導入するようにしてみると幸せになれると思います♪