Edited at

Spock ちっくにテストデータを挿入してみた話

More than 3 years have passed since last update.

Java プロジェクトでのテストに、Groovy を使用するとすごく捗りますよね。

Spock まで採用していると、さらに捗りますね。

ただ、これらを使用しても捗らないものもあります。

それがテストデータの管理です。

これについては、みなさんどのようにしているのか、わたし自身も興味のあるところではありますが、ここではヒントになるかもしれない、アプローチのひとつをご紹介したいと思います。


環境

以下のとおり。


  • Java 1.8.0_45

  • Groovy 2.4.3

  • Spock 1.0-groovy-2.4

  • DBUnit 2.5.1


Spock の良さ

Spock でのテストがなぜ捗り、快適なのか。

その助けのひとつとなっているのが where: の見通しの良さではないかと感じています。

多くの方が御存知のとおり、where: では、表形式でテストパターンを記述することができます。

以下のように。


FizzBuzzSpec.groovy

@Unroll

@Stepwise
class FizzBuzzSpec extends Specification {
def "FizzBuzz に #number をかけた場合 #result が返る"() {
expect:
def fizzBuzz = new FizzBuzz()
fizzBuzz.fizzBuzz(number) == result

where:
number || result
1 || 1
3 || 'fizz'
5 || 'buzz'
15 || 'fizzbuzz'
6 || 'fizz'
10 || 'buzz'
0 || 'fizzbuzz'
-15 || 'fizzbuzz'
}
}

class FizzBuzz {
def static void main(String[] args) {
def fizzBuzz = new FizzBuzz();

10.upto(100) {
println fizzBuzz.fizzBuzz(it)
}
}

def fizzBuzz(int number) {
return fizz(number) + buzz(number) ?: number
}

def fizz(int number) {
return number % 3 == 0 ? 'fizz' : ''
}

def buzz(int number) {
return number % 5 == 0 ? 'buzz' : ''
}
}


いい感じ。

スクリーンショット 2015-12-03 14.19.33.png

この見通しの良さに助けられて、コードレビューにかかるパワーも抑えられるところが、わたしはとても気に入っています。


テストデータの管理

いよいよ本題です。

DB アクセスを伴ったりするアプリケーションには、テストデータは不可欠です。

とくに複雑な業務プロセスを踏んだり、データパターンが必要になってくる場合には、できる限りテストデータをしっかりと用意したいところ。

ただ、これに対するソリューションは (少なくともわたしの周りでは) 選択肢がそう多くなく、テスト実装の方針を決める際には、いつも頭を悩ませるポイントでもあったりしました。


選択可能なソリューション

どんなソリューションがあったのかというと、具体的には以下のようなものたちでした。


  • DB 自体に大量のテストデータを事前に蓄積しておく

  • DBUnit + Excel

  • DBUnit + XML

  • DBUnit + CSV

  • テストコード内で POJO などに起こす

どれもあまりしっくりきません。

わたしの中には、テストデータに求める要件があったのです。


ソリューションに求める要件

テストデータを作成、管理するソリューションには以下のようなものが求められました。


  1. テストは何度回しても必ず同じ結果を返すこと

  2. テストデータのレビューが可能であること


    • できれば、Github などでやりたい

    • できれば、同じ画面の中だけで収まるようにしたい



  3. レビュー負荷が大きくなり過ぎないこと


    • テストコードと同等、もしくはそれよりやや大きい程度までが許容範囲

    • テストデータによって、テストコードが肥大化し過ぎることを避けたい




ソリューションの評価

これらを満たせるかどうかで、先に挙げたソリューションを見てみると。


  • DB 自体に大量のテストデータを事前に蓄積しておく


    1. DB に蓄積されたデータは変化してしまうため、NG

    2. コードレビューには通せず、インラインコメントなど残せないので、NG

    3. レビューがそもそもできないので、NG



  • DBUnit + Excel


    1. ファイルに定義されたものを流すことになり、テストデータは不変なため、OK

    2. コードレビューには通せず、インラインコメントなど残せないので、NG

    3. 表形式なのでデータは俯瞰しやすいが、別ファイルでケースと紐付かないので、あと一歩。



  • DBUnit + XML


    1. ファイルに定義されたものを流すことになり、テストデータは不変なため、OK

    2. テキストファイルのため、コードレビューには通せるが別ファイルなので、あと一歩

    3. 別ファイルになり、また一見してどのケースと紐付いているか判断できないので、NG



  • DBUnit + CSV


    1. ファイルに定義されたものを流すことになり、テストデータは不変なため、OK

    2. テキストファイルのため、コードレビューには通せるが別ファイルなので、あと一歩

    3. 表形式なのでデータは俯瞰しやすいが、別ファイルでケースと紐付かないので、あと一歩。



  • テストコード内で POJO に起こす


    1. ファイルに定義されたものを流すことになり、テストデータは不変なため、OK

    2. テキストファイルであり、テストコードと並んで表現されるため、OK

    3. テストデータを起こすためのコードにテストコードが煩雑になるため、NG



まとめるとこんな感じ。

ソリューション
1
2
3

DB 自体に大量のテストデータを事前に蓄積しておく
:umbrella:
:umbrella:
:umbrella:

DBUnit + Excel
:sunny:
:umbrella:
:cloud:

DBUnit + XML
:sunny:
:cloud:
:umbrella:

DBUnit + CSV
:sunny:
:cloud:
:cloud:

テストコード内でデータをPOJO に起こす
:sunny:
:sunny:
:umbrella:

候補となったソリューションたちは全滅しました。

惜しいものもありましたが、すべての要件を満たせるものはありません。


顧客が本当に必要だったもの

さて、わたし自身がどのようなものを求めていたのか。

これをヒトコトで表現するなら、「Spock where: のようなテストデータの記述方法」です。

一度、Spock の良さを体感してしまうとやはり、どうにかこうできないのかと思いを馳せてしまうのです。

そこで考えてみました。

端的に、どのような条件を満たせれば、満足するのか。

以下のようなものに集約されるのではないでしょうか。


  • データが不変であること

  • データがテキストで表現されていること

  • データが表形式で俯瞰できること

  • データがテストコードと同一ファイル内にあり、テストコード自体を煩雑化しないこと

表形式で俯瞰できること、というのがすこし難しそうです。

ですが、Spock の利用を前提とするならば、そこには Groovy もついてきます。

Groovy がついてくるならば、演算子のオーバーロードにより、データを表のように見せかけることはできるかもしれません。


Groovy x DBUnit で実現

そこで作成してみたのが、こちらになります。

https://github.com/yo1000/dbspock


SpockLikeFlatXmlBuilder.groovy

package com.yo1000.dbspock

/**
* Created by yoichi.kikuchi on 2015/07/13.
*/

class SpockLikeFlatXmlBuilder extends GroovyObjectSupport {
def cols = []
def items = []

SpockLikeFlatXmlBuilder() {
Object.metaClass.or = { x ->
if (!(delegate instanceof List)) {
return [delegate, x]
}
delegate << x
}
}

@Override
Object invokeMethod(String name, Object args) {
if (!(args instanceof Object[]) || args.size() <= 0) {
return super.invokeMethod(name, args)
}

def arg = args[0]

if (name.toLowerCase() == "_cols_") {
cols = arg
return
}

def builder = new StringBuilder(name)

for (def i = 0; i < cols.size(); i++) {
if (arg[i] == null) continue
builder.append(/ ${cols[i]}="${arg[i]}"/)
}

this.items << "<${builder.toString()}/>"
}

String build() {
def builder = new StringBuilder("<dataset>")
for (def item : this.items) {
builder.append(item)
}
builder.append("</dataset>")

return builder.toString()
}
}



dbspock を試してみる

Maven からも参照可能なように、jar を公開してあるので、これを使用して実際の使用感を確認してみます。


pom.xml

<dependencies>

<dependency>
<groupId>com.yo1000</groupId>
<artifactId>dbspock</artifactId>
<version>0.1.2.RELEASE</version>
</dependency>
</dependencies>

<repositories>
<repository>
<id>com.yo1000</id>
<name>yo1000 maven repository</name>
<url>http://yo1000.github.io/maven/</url>
</repository>
</repositories>


今回は以下のようなテーブルが用意されているものとします。

TABLE: SHOP

COLUMN
TYPE
CONSTRAINT

SHOP_ID
VARCHAR(8)
PRIMARY KEY

SHOP_NAME
VARCHAR(80)
NOT NULL

SHOP_CREATED
DATE
NOT NULL

SHOP_MODIFIED
DATE
NOT NULL

TABLE: CUSTOMER

COLUMN
TYPE
CONSTRAINT

CSTM_ID
VARCHAR(8)
PRIMARY KEY

CSTM_NAME
VARCHAR(80)
NOT NULL

CSTM_SEX
CHAR(1)
NOT NULL

CSTM_CREATED
DATE
NOT NULL

CSTM_MODIFIED
DATE
NOT NULL

これに対するテストデータの挿入コードは以下です。


RepositorySpec.groovy

class RepositorySpec extends Specification {

def "DBSpockTest"() {
setup:
def tester = new DataSourceDatabaseTester(dataSource)

def data = {
_cols_ 'SHOP_ID' | 'SHOP_NAME' | 'SHOP_CREATED' | 'SHOP_MODIFIED'
shop 'SP-1' | 'BURGER KING' | '2015-04-01' | '2015-04-01'
shop 'SP-2' | 'RANDYS DONUTS' | '2015-04-01' | '2015-04-01'

_cols_ 'CSTM_ID' | 'CSTM_NAME' | 'CSTM_SEX' | 'CSTM_CREATED' | 'CSTM_MODIFIED'
customer 'CS1X' | 'Tony Stark' | '1' | '2015-04-01' | '2015-04-01'
customer 'CS2X' | 'PEPPER Potts' | '2' | '2015-04-01' | '2015-04-01'
}

def flatxml = {
data.delegate = new SpockLikeFlatXmlBuilder()
data.call()
data.build()
}

tester.dataSet = new FlatXmlDataSet(new StringReader(flatxml.call()))
tester.onSetup()

expect:
// Something with DB access.

where:
// Setting parameters by Spock.
}
}


いかがでしょうか。

テストデータが表形式で表現されるので、DB にどのようなテストデータが挿入されるのか一目瞭然です。

また、テストコード内に一緒に記述できる一方、テストデータの作成部分が、テストコードの実施部分を汚すこともないので、レビュー負荷も大きくなりません。

テストコード内に記述されているので、指摘があれば、Github などでインラインコメントをつけることもでき、指摘されたデータは、どのテストのものなのかがすぐに把握できます。

Excel などとは異なりテキストなので、差分管理もバッチリです。

実際最近まで関わっていたプロジェクトに、試験導入してみましたが、なかなかの好評でした。

テストデータ管理にお困りの際は、ぜひ一度試してみてはいかがでしょうか。

また、もっとイケてるテストデータ管理の方法を知っているゼ!ってご意見もあれば、ぜひ聞いてみたいです。