技術ネタはQiitaに移りました。壁もどこぞに。

Spock x DBUnit でテストを書いてみる

Java プロジェクトで、Groovy を使えるようになって、Spock を試してみたところ、とても使いやすく、これはいいものだとすぐさま実感できた。

興味を刺激されたのでさらに調べてみると、Groovy を利用することで、DBUnit のデータ挿入もコード内で簡潔に済ませてしまう例などが見つかり、これは捗りそうだなーと感じた。

このへんとか。

d.hatena.ne.jp

ただ、Groovy の表現力をもってすれば、「もっといけるだろう」という期待もあった。ので、ちょっと不足していた Groovy の知識を補いつつ、いろいろ試してみた。ゴールは Spock ライクにデータ挿入をおこなえるようにすること。

Spring と、Mybatis を組み合わせた状態のテストコードになっているものの、データ挿入部分であれば、ほぼそのまま利用できるので載せておく。できたのが以下。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 中略 -->

    <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>
</project>

MybatisCustomerRepositorySpec.groovy

// 省略

@ContextConfiguration(loader = SpringApplicationContextLoader, classes = [ApplicationContext, TestContext])
@WebIntegrationTest(["server.port=55555"])
@Unroll
@Stepwise
@Transactional
@TransactionConfiguration
class MybatisCustomerRepositorySpec extends Specification {
    @Autowired
    DataSource dataSource;

    @Autowired
    MybatisCustomerRepository mybatisCustomerRepository;

    def "findSummaryByYearMonthテスト"() {
        setup:
        def tester = new DataSourceDatabaseTester(dataSource)

        def data = {
            _cols_ 'STR_ID' | 'STR_NAME' | 'STR_CREATED' | 'STR_MODIFIED'
            store  'ST1X'   | 'おみせ1X'  | '2015-04-01'  | '2015-04-01'
            store  'ST2X'   | 'おみせ2X'  | '2015-04-01'  | '2015-04-01'

            _cols_   'CST_ID' | 'CST_LASTNAME' | 'CST_FIRSTNAME' | 'CST_SEX' | 'CST_AGE' | 'CST_REGION' | 'CST_CREATED' | 'CST_MODIFIED'
            customer 'CS1X'   | 'みょうじ1X'    | 'なまえ1X'       | '1'       | 20        | '東京都'      | '2015-04-01'  | '2015-04-01'
            customer 'CS2X'   | 'みょうじ2X'    | 'なまえ2X'       | '2'       | 10        | '神奈川県'    | '2015-04-01'  | '2015-04-01'

            _cols_ 'SLS_ID' | 'SLS_SALES' | 'SLS_STR_ID' | 'SLS_CST_ID' | 'SLS_CREATED' | 'SLS_MODIFIED'
            sales  'SL1X'   | 1000        | 'ST1X'       | 'CS1X'       | '2015-04-01'  | '2015-04-01'
            sales  'SL2X'   | 2000        | 'ST1X'       | 'CS2X'       | '2015-04-01'  | '2015-04-01'
            sales  'SL3X'   | 3000        | 'ST2X'       | 'CS1X'       | '2015-05-01'  | '2015-05-01'
            sales  'SL4X'   | 4000        | 'ST2X'       | 'CS1X'       | '2015-05-01'  | '2015-05-01'

            build()
        }
        data.delegate = new SpockLikeFlatXmlBuilder()

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

        expect:
        def items = mybatisCustomerRepository.findSummaryByYearMonth(new SearchCondition(
                storeId,
                new SimpleDateFormat("yyyyMM").parse(fromDate),
                new SimpleDateFormat("yyyyMM").parse(toDate)
        ));

        assert expect == items.size()

        where:
        storeId | fromDate | toDate   || expect
        "ST1X"  | "201503" | "201506" || 2
        "ST2X"  | "201503" | "201506" || 1
    }
}

Spock での where 同様に、テストデータの作成部分でも | を使用して、Spock ライクなデータの表現ができている。これを実現するために勉強した Groovy の機能が以下の3つ。

まずは、Spock のように表形式でデータを表現するために、| 演算子オーバーロードを実装する必要があった。これは難なく実装できた。実装については後述。

次に、クロージャ内でテーブル名として登場する、存在しない名前のメソッド呼び出し。これに難儀した。調べてみると、Groovy では (GroovyObject を継承したクラスでは)、存在しないメソッドがコールされた場合、invokeMethod が、処理をハンドリングすることがわかった。以下の記事などが大変参考になりました。感謝です。

13スライド目

www.slideshare.net

opamp.hatenablog.jp

そして、これらを利用して実装したクラスの動きを適用するために delegate で、このクラスに処理を移譲させた。なんだろう、われながら説明が非常にわかりにくい。。。なお、delegate については以下の記事が参考になりました。ありがとうございます。

uehaj.hatenablog.com

npnl.hatenablog.jp

さて、これらを経て実装したのが以下です。

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++) {
            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()
    }
}

テストデータの定義を表現したクロージャの処理を、この作成したクラスに移譲することで、DBUnit が受け取れる FlatXml 形式の文字列に起こしてやる、ということをやっています。

こんな短いコードで Spock ライクにテストデータの投入ができるようになるのだから、Groovy はすごい。これでずいぶんとテストが捗るのではないかと期待している。

今回のコードは Github でも公開しつつ、また、Maven リポジトリとしても公開してみたので、データ投入をコード内で Spock ライクにおこなってみたいなんて場合には、ためしに使ってみてはどうか。

github.com

再掲 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 中略 -->

    <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>
</project>