前回は「CIツール導入ガイド 第1回 導入と概要」の解説をした。今回はCIによるユニットテストの導入と解説を行う。
1. ツール紹介
1.1 Spock
Spock は、自動テストを行うためのテストフレームワークである。 同様のツールには、古くからの有名なテストツールである JUnit 等が挙げられる。
自動テスト中の一つのテストで行われる一般的な処理は、
- 前処理(必要なら)
- テストデータ準備(必要なら)
- テスト対象の処理(メソッド)を実行
- 結果の検証
- 後処理(必要なら)
と進んでいく。この処理を順に愚直に記述してしまうと、何をテストしているのかわかりにくいソースができてしまう。 テストコードの可読性を上げるには、テストそのものに重きが置かれた記述であることが重要である。比較的新しいテストフレームワークでは、DSL 等を用いた宣言的な記述で何をどうやってテストしようとしているのかをコードで表現できるように工夫されている。 テストコードを読むことによって仕様がわかるのであれば、テストコードそのものを仕様とみなすことも可能である。
Spock は、そのようなことを目的としたフレームワークであり、’仕様(specification)’フレームワークと言われている。 Groovy 言語(JVM 上で動くスクリプト言語) による、 RSpec のようなわかりやすい DSL で記述を行う。 そのため Groovy をある程度学ぶ必要はあるが、それを補って余りある使いやすさで高く評価されている。
また、Spock の特徴として、データ駆動テストが挙げられる。 データとテストのロジックを分けることができれば、同じテストロジックを繰り返し記述する必要もなく、テストを見通しやすくなる。 Spock はデータ駆動なテストを簡潔に記述できる。もちろんデータを外部に定義することもできるが、データテーブル記法により簡単に、わかりやすくデータをソースに埋め込むこともできる。
下記にサンプルからテストケースを一つ抜粋する。
@Unroll
def "map() は投稿メッセージ本文 \"#messageSource\" の先頭が有効なコマンドの時,コマンド文字列を command カラムに格納して返す"() {
given:
def messageForm = new MessageForm()
messageForm.setMessage(messageSource)
when:
def result = service.map(messageForm, ZonedDateTime.now())
then:
result.command == expectedCommand
where:
messageSource || expectedCommand
"#red Lorem ipsum" || "#red"
"#reddolor sit amet" || "#red"
"#green Lorem ipsum" || "#green"
"#greendolor sit amet" || "#green"
"#blue Lorem ipsum" || "#blue"
"#bluedolor sit amet" || "#blue"
}
まず、テストケースの名称に任意の文字列を指定することができる。これによって、テスト結果の出力がとても見やすいものになる。
given:
ブロックでは、テスト対象のオブジェクトの初期化を行っている。
when:
ブロックでは、テスト対象となるメソッドの実行を行っている。
then:
ブロックでは、処理結果の検証を行う。基本的には各行が true
を返せばテストは通過する。
where:
ブロックはテーブルでのデータの定義である。 groovy は messageSource
に右側の列の値を、expectedCommand
に左側の列の値を当てはめて、(ヘッダを除いた)行数分このテストケースを反復実行する。 テストデータの記述方法はテストを書くときの悩みのタネの一つだが、多くの場合はそれほど多くのケースのデータを揃える必要はなく、データの件数が多くない場合はこのテーブル記法は非常に有効であることが見て取れるだろう。
具体的な例は下記のサンプルコードを参照していただきたい。
1.2 Jacoco
Jacoco は、テストが対象ソースコードをどの程度網羅しているかを計測する、Java 言語のカバレッジツールである。
Jacoco はステートメントカバレッジとブランチカバレッジを取得できる。 つまりすべての命令文が実行されるか、またすべての条件分岐が網羅されているかを計測し、網羅率と網羅されていない箇所を報告してくれる。 カバレッジが100%であることがテストの完全性を保証するものではないし、どうしても100%にしなければならないというものでもないが、テストの重要な指標として用いることができる。
下記サンプルでは実際にカバレッジを計測し、結果を見て対処を行っているので参考にしていただきたい。
2. パッケージ導入
build.gradle
に必要な依存性を追加する。 更に、あとで使うので gradle に渡すプロパティでテスト用データベース設定を上書きできるように処理を追加しておく。
build.gradle
diff --git a/build.gradle b/build.gradle
index 4fd75f6..954649b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -9,6 +9,7 @@ plugins {
id "java"
id "eclipse"
id "idea"
+ id "groovy"
id "org.springframework.boot" version "2.0.4.RELEASE"
id "io.spring.dependency-management" version "1.0.6.RELEASE"
}
@@ -39,4 +40,29 @@ dependencies {
compile "org.springframework.boot:spring-boot-starter-data-jpa"
compile "org.springframework.boot:spring-boot-starter-thymeleaf"
runtime "org.postgresql:postgresql"
+ testCompile "org.codehaus.groovy:groovy-all:2.4.15"
+ testCompile "org.springframework.boot:spring-boot-starter-test"
+ testCompile "org.spockframework:spock-core:1.1-groovy-2.4"
+ testCompile "org.spockframework:spock-spring:1.1-groovy-2.4"
+ testRuntime "cglib:cglib-nodep:3.2.4"
+}
+
+test {
+ // テストタスク実行前処理
+ // ./src/test/resources/application.properties を上書きする
+
+ // -Pdburl=*** ユニットテスト実行時JDBC接続文字列指定
+ if (project.hasProperty('dburl')) {
+ systemProperty("spring.datasource.url", project.getProperty("dburl"))
+ }
+
+ // -Pdbuser=*** データベース接続ユーザ
+ if (project.hasProperty('dbuser')) {
+ systemProperty("spring.datasource.username", project.getProperty("dbuser"))
+ }
+
+ // -Pdbpass=*** データベース接続パスワード
+ if (project.hasProperty('dbpass')) {
+ systemProperty("spring.datasource.password", project.getProperty("dbpass"))
+ }
}
3. テストコード追加
まず MessageMappingService
クラスのテストを用意する。 MessageMappingService
は、特別なライブラリに特に依存していないクラスなので、フレームワークに依存しないテストになる。
src/test/groovy/com/example/messageboard/MessageMappingServiceSpec.groovy
このように Spock は Groovy 言語によって完結かつ直感的にテストを記述することができる。 given
ブロックで初期条件を、when
ブロックでテスト対象のコードを、then
ブロックでテストの評価コードを記述する。 また、@Unroll
と where
ブロックによって、テストデータをわかりやすくソースコードに列挙できる。
ここで ./gradlew test
でユニットテストを実行しよう。以下のようにテストは失敗する。
> Task :test FAILED
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.codehaus.groovy.reflection.CachedClass (file:/Users/yuki/.gradle/caches/modules-2/files-2.1/org.codehaus.groovy/groovy-all/2.4.15/423a17aeb2f64bc6f76e8e44265a548bec80fd42/groovy-all-2.4.15.jar) to method java.lang.Object.finalize()
WARNING: Please consider reporting this to the maintainers of org.codehaus.groovy.reflection.CachedClass
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
com.example.messageboard.MessageMappingServiceSpec > map() は投稿メッセージ本文 "#blue Lorem ipsum" の先頭が有効なコマンドの時,コマンド文字列を command カラムに格納して返す FAILED
org.spockframework.runtime.SpockComparisonFailure at MessageMappingServiceSpec.groovy:21
com.example.messageboard.MessageMappingServiceSpec > map() は投稿メッセージ本文 "#bluedolor sit amet" の先頭が有効なコマンドの時,コマンド文字列を command カラムに格納して返す FAILED
org.spockframework.runtime.SpockComparisonFailure at MessageMappingServiceSpec.groovy:21
com.example.messageboard.MessageMappingServiceSpec > map() は投稿メッセージ本文 "#blue Lorem ipsum" の先頭が有効なコマンドの時,コマンド文字列と続く空白を除いた値を message カラムに格納して返す FAILED
org.spockframework.runtime.SpockComparisonFailure at MessageMappingServiceSpec.groovy:43
com.example.messageboard.MessageMappingServiceSpec > map() は投稿メッセージ本文 "#bluedolor sit amet" の先頭が有効なコマンドの時,コマンド文字列と続く空白を除いた値を message カラムに格納して返す FAILED
org.spockframework.runtime.SpockComparisonFailure at MessageMappingServiceSpec.groovy:43
20 tests completed, 4 failed
FAILURE: Build failed with an exception.
ソースを見ると typo していることがわかる。修正しよう。
src/main/java/com/example/messageboard/MessageMappingService.java
diff --git a/src/main/java/com/example/messageboard/MessageMappingService.java b/src/main/java/com/example/messageboard/MessageMappingService.java
index 8381619..08cd0a9 100644
--- a/src/main/java/com/example/messageboard/MessageMappingService.java
+++ b/src/main/java/com/example/messageboard/MessageMappingService.java
@@ -33,7 +33,7 @@ public class MessageMappingService {
// コマンド解析用正規表現ソース.
private static final String commandPatternSource =
- "^(?:(#(?:red|green|brue))\\s*)?(.*)$";
+ "^(?:(#(?:red|green|blue))\\s*)?(.*)$";
// コンパイル済み正規表現パターン.
private final Pattern commandPattern;
これでテストは成功する。
続けて、MessagesRepository
クラスをテストしよう。
src/test/groovy/com/example/messageboard/MessagesRepositorySpec.groovy
ここからはフレームワークに依存するテストコードになる。 基本的には、Bean でデータベースに対して照会・変更操作を行い、groovy の Sql
クラスを使って検証している。 こちらも直感的に記述できていることがわかるだろう。
このテストを実行するためにはテスト用のデータベース設定を容易したほうが良いろう。 以下のファイルを追加しよう。
src/test/resources/application.properties
# テストデータベース接続設定
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/messageboard_test
spring.datasource.username=messageboard
spring.datasource.password=password
前章を参考に messageboard_test
テーブルを用意すればテストを実行できる。
次に MessageBoardController
をテストする。
src/test/groovy/com/example/messageboard/MessageBoardControllerSpec.groovy
ここでは、注入されているサービスをモックに置き換えてリクエストに対する動作をテストしている。 Spock の Mock クラスは非常にわかりやすい記述ができる。 ビューが生成したレスポンスを確認することもできるが、今回はその部分は e2e テストに委ねることにする。
もう一度 ./gradlew test
を実行してテストが通過することを確認する。
4. カバレッジ
Jacoco を使ってカバレッジの計測を行う。
まず、ビルドの設定を追加する。
build.gradle
diff --git a/build.gradle b/build.gradle
index 954649b..c3a6c32 100644
--- a/build.gradle
+++ b/build.gradle
@@ -12,6 +12,7 @@ plugins {
id "groovy"
id "org.springframework.boot" version "2.0.4.RELEASE"
id "io.spring.dependency-management" version "1.0.6.RELEASE"
+ id "jacoco"
}
// 共通設定の読み込み
@@ -66,3 +67,12 @@ test {
systemProperty("spring.datasource.password", project.getProperty("dbpass"))
}
}
+
+jacocoTestReport {
+ // HTML のレポートを出力する
+ reports {
+ xml.enabled false
+ csv.enabled false
+ html.destination file("${buildDir}/jacocoHtml")
+ }
+}
ユニットテストの実行後に、./gradlew jacocoTestReport
を実行して、計測を行う。
./build/jacocoHtml/index.html
から結果を確認することができる。 MessageBoardController.index
, MessageMappingService#parseMessage
, Application.main
, Message.getId
に未達のコードが有ることがわかる。
Jacoco レポート1
Jacoco レポート2
Jacoco レポート3
Jacoco レポート4
Jacoco レポート5
Jacoco レポート6
まず Application.main
, Message.getId
は、main メソッドと getter なので気にしなくても良い。Application
クラスは計測対象から除外してしまおう。
build.gradle
diff --git a/build.gradle b/build.gradle
index c3a6c32..740723d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -75,4 +75,11 @@ jacocoTestReport {
csv.enabled false
html.destination file("${buildDir}/jacocoHtml")
}
+
+ // Application.java をカバレッジ計測対象から除外する
+ afterEvaluate {
+ classDirectories = files(classDirectories.files.collect {
+ fileTree(dir: it, exclude: '**/Application.class')
+ })
+ }
}
Jacoco レポート7
Message
, MessageForm
クラスも計測対象から除外してもよいが、Lombok などを使って getter, setter, コンストラクタのコード自体をなくしてしまうという方法もある。(Jacoco は Lombok 生成コードを計測しない機能を持っている) 今回は単純に無視してしまうことにする。
MessageMappingService#parseMessage
は到達してはいけないコードである。これも無視してしまおう。(ただし、実装方法の検討余地はある)
残った MessageBoardController.index
はテスト漏れである。テストを追加しよう。
src/test/groovy/com/example/messageboard/MessageBoardControllerSpec.groovy
diff --git a/src/test/groovy/com/example/messageboard/MessageBoardControllerSpec.groovy b/src/test/groovy/com/example/messageboard/MessageBoardControllerSpec.groovy
index 1a3abfa..03afe24 100644
--- a/src/test/groovy/com/example/messageboard/MessageBoardControllerSpec.groovy
+++ b/src/test/groovy/com/example/messageboard/MessageBoardControllerSpec.groovy
@@ -25,6 +25,15 @@ class MessageBoardControllerSpec extends Specification {
def mockMvc = MockMvcBuilders.standaloneSetup(messageBoardController).build()
+ def "GET / は全件取得を行い 200 を返す"() {
+ when:
+ def responce = mockMvc.perform(get("/"))
+
+ then:
+ 1 * messagesRepositoryMock.findAllOrderByCreatedAtDesc()
+ responce.andExpect(status().isOk())
+ }
+
def "POST / は正しいデータを受け取ったときに、データを登録して / にリダイレクトする"() {
given:
def message = new Message()
Jacoco レポート8
これで、無視することにしたもの以外の部分についてテストが走っていることが確認できた。
次回
社内サーバにリモートリポジトリを作るのも一つですが、「開発にまつわる面倒事」をこの際全部、tracpath(トラックパス)に任せてみませんか?
バージョン管理サービス・プロジェクト管理サービスの「tracpath(トラックパス)」では、
ユーザー5名、リポジトリ数3つまで、無料で利用可能です。
さっそく実務でも使って見ましょう。
自らも開発を行う会社が作ったからこそ、開発チームの「作る情熱」を支える、やるべきことに集中出来るサービスになっています。
エンタープライズ利用が前提のASPサービスなので、セキュリティも強固です。