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>
Groovy でテストを書いてみる (STS: Spring Tool Suite)
IntelliJ IDEA に引き続き、Spring Tool Suite でも Groovy でテストを書いてみようと思い、試してみた。
使用した STS のバージョンは、STS 3.7.0.RELEASE (Based on Eclipse 4.5) の、Mac OS X 用のもの。基本的に Windows 版でも手順はさほど変わらない。
以下手順。
Groovy Eclipse Maven plugin のインストール
The Groovy Eclipse Maven plugin をインストールする。執筆時点 (2015-07-13) で、まだ Eclipse 4.5 用のプラグインが正式にリリースされていなかったため、今回はスナップショットから取得する。
http://docs.groovy-lang.org/latest/html/documentation/tools-groovyeclipse.html
[Help] > [Install New Software...] メニューを選択。
[Work with:] へ http://dist.springsource.org/snapshot/GRECLIPSE/e4.5 を入力して、出てきた一覧全てをチェック。
[Next]、[Next]、... とダイアログを進めていき、ライセンス確認まできたら、"I accept the terms of the license agreement" をチェックして、[Finish] ボタンをクリック。
再起動を促されるので、[Yes] で IDE を再起動。
ここまでで、プラグインのインストールは終わり。
プロジェクトへ、Groovy の適用
ひきつづき、プロジェクトのインポートをおこない、プロジェクトに Groovy サポートを追加すしていく。すでに pom.xml の存在しているプロジェクトが手元にある前提で進めます。
[File] > [Import...] メニューを選択。インポートダイアログで、[Maven] > [Existing Maven Projects] を選択し、プロジェクトをインポートする。
プロジェクトがインポートできたら、プロジェクト ルートを右クリックして、[Configure] > [Convert to Groovy Project] を選択。
これで Groovy サポートが有効になるが、クラスパスに Maven で取得した Groovy と、プラグインで追加される Groovy でライブラリ競合を起こすので、プラグイン側の Groovy を除去してしまう。プロジェクト ルートを右クリックして、[Groovy] > [Remove Groovy libraries from classpath] で、プラグイン側の Groovy を除去できる。
以上で、Groovy で記述されたテストも、実行可能になる。
ライブラリの競合に気づかず、しばらくハマった。。。
Groovy でテストを書いてみる (IntelliJ IDEA)
いつもは JUnit で済ませてしまうテストを、Groovy で書いてみようと思い、試してみたメモ。
今回使用したのは、IntelliJ IDEA 15 EAP (Early Access Program)。非常に簡単に Groovy サポートが有効になって驚いたが、IntelliJ IDEA は、そもそも Groovy をサポートしているので、むずかしいわけはなかった。
以下手順。
pom.xml に、依存関係を追加
<dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.4.3</version> <scope>test</scope> </dependency>
以上。
試しにほぼ JUnit まんまのコードで動作を確認してみる。今回は比較のため、もとにした Java のコードも併記しておく。
Java でのテストコード。
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = {ApplicationContext.class, TestContext.class}) @WebIntegrationTest({"server.port=55555"}) public class SalesResourceTest { @Autowired private RestTemplate restTemplate; @Test public void getSalesSummary() throws Exception { SalesSummary[] items = restTemplate.getForObject( "http://localhost:55555/api/v1/store/{storeId}/sales/summary/{dateFrom}/{dateTo}", SalesSummary[].class, new HashMap<String, String>() { { this.put("storeId", "ST2"); this.put("dateFrom", "201412"); this.put("dateTo", "201506"); } } ); Assert.assertThat(items, CoreMatchers.any(SalesSummary[].class)); } }
こちらが同様の内容を Groovy に直したもの。
@RunWith(SpringJUnit4ClassRunner) @SpringApplicationConfiguration(classes = [ApplicationContext, TestContext]) @WebIntegrationTest(["server.port=55555"]) class SalesResourceTest { @Autowired RestTemplate restTemplate; @Test void getSalesSummary() { def items = restTemplate.getForObject( "http://localhost:55555/api/v1/store/{storeId}/sales/summary/{dateFrom}/{dateTo}", SalesSummary[], [ storeId: "ST2", dateFrom: "201412", dateTo: "201506" ] ); Assert.assertThat(items, Matchers.any(SalesSummary[])); } }
これだけだと煩雑だった部分がちょっとスッキリした程度で、そこまで強い魅力は感じづらいかもしれない。ただ、Groovy を動かせることで、できることもいろいろと増えていくので、今回はここまでで良しとする。
Spring Boot と MyBatis を組み合わせて使用する
クエリレベルでのチューニングの必要があるようなプロジェクトで、Hibernate を適用できないといった場合にたびたび採用されるであろう MyBatis。今回はこれを Spring Boot と組み合わせてみる。
Spring Boot を使用すると、コンフィグレーションのほとんどがアノテーションベースになるため、MyBatis もそうかと思えば、そうではない。MyBatis は MyBatis で、これまでどおり XML ベースのコンフィグレーションファイルが必要になるので、ここは事前に認識しておく。
Spring と MyBatis の連携には、mybatis-spring
という、インテグレーションモジュールがでているので、まずは POM にこれらを追加する。
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.2.8</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.1</version> </dependency>
Spring から MyBatis を使用する場合には、SqlSessionTemplate
クラスを使用するので、これを構成する。
@SpringBootApplication public class ApplicationContext { @Autowired @Bean public DataSourceInitializer dataSourceInitializer( @Qualifier("dataSource") DataSource dataSource) { DataSourceInitializer dataSourceInitializer = new DataSourceInitializer(); dataSourceInitializer.setDataSource(dataSource); ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(); databasePopulator.addScript(new ClassPathResource("sql")); dataSourceInitializer.setDatabasePopulator(databasePopulator); dataSourceInitializer.setEnabled(false); return dataSourceInitializer; } @Autowired @Bean public DataSourceTransactionManager transactionManager( @Qualifier("dataSource") DataSource dataSource) { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; } @Autowired @Bean public SqlSessionTemplate sqlSessionTemplate( @Qualifier("dataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver( new DefaultResourceLoader()); // MyBatis のコンフィグレーションファイル bean.setConfigLocation(resolver.getResource("classpath:config.xml")); // MyBatis で使用する SQL ファイル群 bean.setMapperLocations(resolver.getResources("classpath:sql/*.xml")); return new SqlSessionTemplate(bean.getObject()); } @Primary @Autowired @Bean public DriverManagerDataSource dataSource() { // 今回の例は ORACLE、適宜変更する DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("oracle.jdbc.driver.OracleDriver"); dataSource.setUrl("jdbc:oracle:thin:@//localhost:1521/xe"); dataSource.setUsername("system"); dataSource.setPassword("manager"); return dataSource; } }
続いて、MyBatis 側でのコンフィグレーションファイルの内容。この設定ファイルがない場合、アンダースコア区切り (スネークケース) のカラム名が、キャメルケースのフィールドにマッピングされないため、ほとんど必須。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <!-- アンダースコア区切り (スネークケース) のカラム名をキャメルケースにマップする設定 --> <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- SQL 内で AS によって設定された列名をマップする設定 --> <setting name="useColumnLabel" value="true"/> </settings> <!-- カラムタイプによりデフォルトとは異なる O/R 変換処理が必要になる場合に設定 --> <!-- <typeHandlers> <typeHandler handler="package.name.LocalDateTypeHandler"/> </typeHandlers> --> </configuration>
最後に Repository (永続化) レイヤでの SqlSessionTemplate
の使い方と SQL ファイルのマッピングを例にあげておく。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- 任意の名前空間を定義する --> <mapper namespace="SalesRepository"> <!-- 名前空間 + ID でプロジェクト全体で一意になるように名前を設定 --> <!-- ParameterType は SqlSessionTemplate で渡すパラメタの型 --> <!-- ResultType は SqlSessionTemplate で返ってくるデータの型 --> <select id="findSummaryByYearMonthRange" parameterType="jp.hotpepper.beauty.frozen.model.entity.SearchCondition" resultType="jp.hotpepper.beauty.frozen.model.entity.SalesSummary"> SELECT STORE.STORE_ID AS STORE_ID, STORE.NAME AS STORE_NAME, COUNT(SALES.SALES_ID) AS SALES_COUNT, SUM(SALES.SALES) AS SALES_TOTAL, AVG(CUSTOMER.AGE) AS AGE_AVERAGE FROM SALES LEFT OUTER JOIN STORE ON SALES.STORE_ID = STORE.STORE_ID LEFT OUTER JOIN CUSTOMER ON SALES.CUSTOMER_ID = CUSTOMER.CUSTOMER_ID WHERE <!-- パラメタ型のフィールド名を #{ } で囲んで設定 --> <!-- パラメタ型が String など構造を持たない場合は任意の名前でよい --> SALES.CREATED BETWEEN #{from} AND #{to} GROUP BY STORE.STORE_ID, STORE.NAME ORDER BY STORE_ID </select> </mapper>
@Repository public class MybatisSalesRepository implements SalesRepository { @Autowired private SqlSessionTemplate template; @Override public List<SalesSummary> findSummaryByYearMonth(SearchCondition condition) { List<SalesSummary> items = this.<SalesSummary>getTemplate().selectList( // SQL を定義した XML での、名前空間 + ID を指定 // ID だけでもプロジェクト全体で一意になる場合は、ID のみの指定も可能 "SalesRepository.findSummaryByYearMonthRange", condition); return items; } protected SqlSessionTemplate getTemplate() { return template; } }
全体としてはこのような形で、Spring Boot で MyBatis を使用できるようになる。MyBatis のコンフィグレーションファイルを、アプリケーションコンテキストで別途読み込まないといけないところに気付くまでにずいぶん時間がかかってしまった。
Spring Boot で applicationContext.xml の ref 属性を表現する
Spring Framework 4、および Boot あたりから、本格的にアノテーションベースのコンフィグレーションに移行が進んでいる。これに際して、Spring Framework が直接使用するコンフィグレーションに関しては、すべてアノテーションベースに移行しようと思い試したところ、表題にあるような、applicationContext.xml
でごく普通に使用していた、ref 属性に相当する書き方はどのようになるのか、しばらく書き方がわからず、けっこうハマったので残しておく。
書き方はというと、参照される側 (@Bean
属性を適用した Bean メソッド) の名前をキーに使用して、参照する側 (ref 属性を使用したい Bean メソッド) の引数で、オートワイヤリングして、DI をかけてやる。名前の特定には、@Qualifier
アノテーションを使用する。以下のようになる。
@Autowired @Bean public SqlSessionTemplate sqlSessionTemplate( @Qualifier("dataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver( new DefaultResourceLoader()); bean.setConfigLocation(resolver.getResource("classpath:mybatis-config.xml")); bean.setMapperLocations(resolver.getResources("classpath:sql/*.xml")); return new SqlSessionTemplate(bean.getObject()); } @Primary @Autowired @Bean public DriverManagerDataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); // dataSource.set ... return dataSource; }
このように ref 属性相当の書き方をしないとならない場面が少なからずあるので、必ずおさえておきたい。
Spring Boot で設定可能なプロパティを追加する 2
過去記事に関連して、より柔軟なプロパティの割り当て方に至ったため、改めてまとめておく。 yo1000.hateblo.jp
@ConfigurationProperties
アノテーションを付けたクラスが、application.properties
に設定の外部化をできるようになる、というのは以前紹介したとおり。だが、このアノテーション、クラスのみに適用可能というわけではないようだったので、いろいろ調べてみたところ、@Bean
アノテーションと併用することで、Bean メソッドにも適用可能であることがわかった。
即ち以下のようになる。
@Bean @ConfigurationProperties(prefix = "userconf.jdbc") public JdbcConfiguration jdbcConfiguration() { return new JdbcConfiguration(); } @SpringBootApplication public static class JdbcConfiguration { private String driverClassName; private String connectionString; private String username; private String password; private String schema; // getter/setter }
クラス側に直接 @ConfigurationProperties
アノテーションを適用しない場合、設定可能となるプロパティとアノテーションの間で、少しばかり行が空いてしまうことになり、このあたりの見通しは若干犠牲になるかもしれない。ただ、Bean メソッドにこのアノテーションを適用することにはそれ以上の意味がある。
どのような場面で使用するかといえば、上記のような JDBC 接続設定クラスを作成したはいいが、接続先のデータソースが複数ある、といった場合にこれを活かすことができる。クラス側に @ConfigurationProperties
を適用してしまうと、同様のフィールド一式を持ったクラスを複数作成しなければならないことになり、コードの重複が発生してしまうため、Bean メソッド側にアノテーションを適用して設定クラスの重複を回避する。
Spring Boot のビルド方法あれこれ
Spring Boot の起動には基本的に、Maven (または、Gradle) を使用するが、2つのゴールが用意されているので、その使い方などを紹介。
spring-boot:run
Spring Boot アプリケーションを起動するためのゴール。Spring Boot の設定は、Maven の --define
オプションで渡す。
mvn spring-boot:run --define server.port=8011 --define logging.level.org.springframework.web=DEBUG
spring-boot:repackage
Spring Boot アプリケーション パッケージを作成するためのゴール。pom.xml
の packaging
要素での指定により、jar
、または war
のいずれかの成果物を選択できる。packaging
要素を省略した場合は、jar になる。
<?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> <groupId>com.yo1000</groupId> <artifactId>bluefairy</artifactId> <version>0.1.0</version> <packaging>jar</packaging> ... </project>
また、Maven から spring-boot:repackage
を実行する場合には、package
ゴールといっしょに使用する。
mvn package spring-boot:repackage
成果物は {プロジェクト ディレクトリ}/target
に配置される。これを起動する場合は、java
コマンドを使用する。
java -jar bluefairy-0.1.0.jar --server.port=8011 --logging.level.org.springframework.web=DEBUG
成果物まで用意できると、java のみで起動できるようになるので、サーバーのセットアップなども省力化できて、とても良いですね。