はじめに
このテキストのサンプルコードは次の環境で検証しています。
- Windows 10
- Eclipse IDE for Java Developers 2018-09(4.0.9)
- JRE 1.8.0_191
- groovy 2.5.4
- spock 1.3-groovy-2.5
Spockとは?
SpockはJava・Groovyアプリケーション向けの、テスト・仕様フレームワークです。他のツールと比べ、美しく表現力の高い仕様記述言語が特徴です。
https://spock-framework-reference-documentation-ja.readthedocs.io/ja/latest/introduction.html
JUnitと何が違うのか?
JUnitに比べてテストのコーディング量が圧倒的に少ない!
つまり、コーディング量が減るので単純に単体テストの効率を上げることができます。
では、どういった仕組みでテストコーディング量を抑えることができるでしょうか?
テストをgroovyでコーディングできる
Spockはテストをgroovyで記述します。
groovyの言語特性により、Javaよりもライトなコーディングが可能になります。
ビルドをgradleにしている場合にはどちらもgroovyになるので習得も早いかと思います。
Groovy(グルービー)は、Javaプラットフォーム上で動作する動的プログラミング言語である。
https://ja.wikipedia.org/wiki/Groovy
groovyを使ったことがない人には学習コストを負担に感じるかもしれません。
ただ、groovyはJavaのコードもほぼそのまま記述できるため、最悪groovyのコードが分からなければJavaで書くことができます。
同じテストをJUnitとSpock(groovy)でコーディングするとどれぐらい違うのか?
Spockのgithubで公開されているサンプルコードがあったので、JUnitで同じテストをコーディングした例がこちらです。
import static org.junit.Assert.assertEquals;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.h2.Driver;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
public class DatabaseDrivenTest {
private static Connection con;
@BeforeClass
public static void setup() throws Exception {
Driver.load();
con = DriverManager.getConnection("jdbc:h2:mem:");
Statement stmt = con.createStatement();
stmt.execute("create table maxdata (id int primary key, a int, b int, c int)");
stmt.execute("insert into maxdata values (1, 3, 7, 7), (2, 5, 4, 5), (3, 9, 9, 9)");
stmt.close();
}
@Test
public void testMaximumOfTwoNumbers() throws Exception {
// Math.max(a, b) == c;
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("select a, b, c from maxdata");
rs.next();
rs.getInt("a");
assertEquals(Math.max(rs.getInt("a"), rs.getInt("b")), rs.getInt("c"));
stmt.close();
}
@AfterClass
public static void after() throws SQLException {
con.close();
}
}
Spockのコードがこちら。
https://github.com/spockframework/spock-example/blob/master/src/test/groovy/DatabaseDrivenSpec.groovy
groovyは圧倒的にシンプルであることがわかると思います。
データ駆動テストで組み合わせテストを効率化
もう一つの強力な仕組みがデータ起動テストです。
パラメータと結果の組み合わせの繰り返しテストを容易に実現することができます。
import spock.lang.*
@Unroll
class DataDrivenSpec extends Specification {
def "minimum of #a and #b is #c"() {
expect:
Math.min(a, b) == c // テストの実行と結果の検証
where:
a | b || c // パラメータ1 | パラメータ2 || 結果
3 | 7 || 3
5 | 4 || 4
9 | 9 || 9
}
}
このコードは次の内容のテストとなります。
a=3、b=7でMath.min(a, b)を実行した結果が3
a=5、b=3でMath.min(a, b)を実行した結果が4
a=9、b=9でMath.min(a, b)を実行した結果が9
パラメータと結果のバリエーションを増やしたい場合、whereにパターンを追加すればいいと想像つくかと思います。
Excelで作成した組み合わせテストのパターン表からコンバートしやすい形です。
エラーレポート
テストに失敗した場合、エラーレポートがコンソールに出力されます。
Javaのエラースタックトレースより親切です。
Condition not satisfied:
validator.validate() == true
| | |
| false false
<Validator@5aa9e4eb>
at ValidatorSpec.validate test(ValidatorSpec.groovy:40)
モック化が容易
Spockにはモック化の機能も備わっています。
HttpServletRequestをモック化してgetRequestURIの戻り値を"/some/path.html"に書き換えてみます。
def request = Mock(HttpServletRequest)
request.getRequestURI() >> "/some/path.html"
ただし、モック化には色々と制約もあるのでその点は後述します。
導入方法
gradleまたはmavenでインストールできます。
groovyとのバージョン依存があるので注意してください。
testCompile group: 'org.spockframework', name: 'spock-core', version: '1.3-groovy-2.5'
testCompile group: 'org.objenesis', name: 'objenesis', version: '3.0.1'
testCompile group: 'cglib', name: 'cglib', version: '3.2.10'
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.3-groovy-2.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>3.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.10</version>
</dependency>
gradleのセットアップ例
apply plugin: 'java'
apply plugin: 'groovy'
sourceSets {
test {
java {
srcDir 'src/test/groovy'
}
}
}
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'org.spockframework', name: 'spock-core', version: '1.3-groovy-2.5'
testCompile group: 'org.objenesis', name: 'objenesis', version: '3.0.1'
testCompile group: 'cglib', name: 'cglib', version: '3.2.10'
}
ソースディレクトリは'src/test/groovy'に設定する必要があるので注意してください。
Eclipse Plugins
Eclipseで使うためには次のプラグインのインストールが必要です。
Market Placeからインストールしてください。
Spock Plugin
http://marketplace.eclipse.org/content/spock-plugin
Groovy Development Tools
http://marketplace.eclipse.org/content/groovy-development-tools
Spockの基本
テストの基本形はこのような形になります。
class ValidatorSpec extends Specification {
@Unroll
def "サンプルテスト param1=#param1 param2=#param2" () {
setup: // 1.前処理
def target = new テスト対象クラス()
def mockTarget = Mock(モック化したいクラス)
mockTarget.method() >> 書き換えたい値
target.mockTarget = mockTarget // テスト対象クラスのメンバ変数にモックを代入します
expect: // 2.テスト対象の実行
def returnObj = target.targetMethod(param1, param2)
then: // 3.結果の検証
returnObj == result
1 * mockTarget.method()
where: // 4
.パラメータと結果の組み合わせ
| param1 | param2 || result
| "a" | 1 || "ok"
| "b" | 20 || "ng"
}
}
- 前処理ではテスト対象クラスの生成やモックの生成を行います
- ここではテスト対象クラスのテスト対象メソッドを実行します。
- テスト対処メソッドの実行結果を検証します。検証検証はbooleanで評価されます。
- パイプ繋ぎでパラメータと結果の組み合わせを定義します。param1、param2、resultは変数宣言がないのがポイントです。
ブロックの種類
ブロックには次の種類があります。
ブロック | 説明 |
---|---|
expect | テストの実行と結果の検証(when + then) |
where | パラメータと結果 |
when | テスト対象を実行 |
then | 結果の検証 |
setup(given) | 事前処理 |
cleanup | 事後処理 |
JMockitと組み合わせてレガシーなコードのテストにも対応する
レガシーなコードで使われるstatic、final、privateコンストラクタ等、Spockだけではできないテストがあります。
これらのテストが必要になる場合はJMockitを使うのがおすすめです。
setup:
new MockUp<LegacyUtil>() {
@Mock
public boolean check() {
return true
}
}
Spockの効果が期待できないケース
テストカバレッジ100%が求められる
テストカバレッジ100%を目指すのは非常に大変で、100%を達成したからといって高品質になるとは言い難いです。ただ、品質指標値の一つとして求められる開発プロジェクトも実際に存在します。
spockだけでは実現が難しいので、jMockitを駆使するか、JUnitを併用して達成してください。
JUnitでもテストが難しいクラス
テストしやすいクラス設計をするのが原則ですが、実際の開発現場ではスパゲッティーコードに遭遇する機会も多いと思います。
テストがやりやすいようリファクタリングを施してください。
参考サイト
Spock
http://spockframework.org
Spock Framework リファレンスドキュメント
https://spock-framework-reference-documentation-ja.readthedocs.io/ja/latest/
Spockのサンプルコード
https://github.com/spockframework/spock-example