Spock x DBUnit でテストを書いてみる
Java プロジェクトで、Groovy を使えるようになって、Spock を試してみたところ、とても使いやすく、これはいいものだとすぐさま実感できた。
興味を刺激されたのでさらに調べてみると、Groovy を利用することで、DBUnit のデータ挿入もコード内で簡潔に済ませてしまう例などが見つかり、これは捗りそうだなーと感じた。
このへんとか。
ただ、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
そして、これらを利用して実装したクラスの動きを適用するために delegate で、このクラスに処理を移譲させた。なんだろう、われながら説明が非常にわかりにくい。。。なお、delegate については以下の記事が参考になりました。ありがとうございます。
さて、これらを経て実装したのが以下です。
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 ライクにおこなってみたいなんて場合には、ためしに使ってみてはどうか。
再掲 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>