Javaプロジェクトでテストをたのしく書くための試み | サイバーエージェント 公式エンジニアブログ
こんにちは、Ameba事業本部ゲームプラットフォーム室の山田(@stormcat24)です。
自分のミッションは主にゲーム部門の開発の改善で、最近はScalaでモナ・・・しながらツールを書いてたりClojureに手を出したりしています。

はじめに

ところでみなさんJava書いてますか?サイバーエージェントでは最近node熱が高いのですが、Javaプロジェクトもまだまだ根強く存在します。僕も隙あらばScalaをぶっこもうとしてますが、大人の事情でまだまだJavaを書くシーンも多いのです。
で、そんなテンションが上がりにくいJavaプロジェクトをやっていく上で、せめてテストくらいはなるべくたのしく書きたい!ということで、今のプロジェクトで取り入れた施策を簡単にですが紹介します。
めちゃくちゃ尖った技術を使ってるわけではないですが、これらをやっておけばそれなりに楽しく書けるかなと思ってますので、ゆる~い感じで呼んで頂ければ幸いです。

Javaであるが故の"テストの表現力"の限界

JavaでユニットテストといえばJUnitですよね。まだまだ健在なJUnitですが、なかなかレガシーなAPIですし、最近主流となっているBDDスタイルなテストを書くには貧弱さが否めません。

assertThat(JUnit4から登場)やhamcrestのMatcherが登場してタイプセーフに「それっぽい英文」みたいにテストを書けるようにはなりましたが、パッと見そのテストコードが何を意味しているかということは書いた人じゃないとわからないでしょう。RubyのRSpec、JavaScriptのJasmine、ScalaのSpecs2等でBDDを経験している方にとっては貧弱な仕組みに映るのではないでしょうか。

このような仕組みでテストをちゃんと書きましょう!なんて言っても、
メンバーにただ苦痛なテストコードを書くことを強いるだけだと考えていました。テストが無いプロジェクトは何かと叩かれる事も多くてそれは仕方ないとは思っていますけど、Javaプロジェクトに関しては「他の言語に比べて可読性と生産性に優れた仕組みが確立されてない」という面がテストの積み上げが促進されない要因の一つではないかと思います。

今回のプロジェクトではテストをとても大事にしたかったので、このような仕組みだとメンバーにただ苦痛なテストコードを書くことを強いるだけだと考え、色々と施策を考えることにしました。

Spock

そこで今回白羽の矢が立ったのがBDDテスティングフレームワークの「Spock」です。SpockはJavaで利用できますが、威力を発揮するのはGroovyでテストコードを書くときです。SpockはGroovyのDSL(ドメイン固有言語)の仕組みを利用していて、Javaと比べて簡潔に、高い表現力でテストを書くことができます。簡単に1つ例を紹介しましょう。

package example

import spock.lang.Specification
import spock.lang.Unroll

@Unroll
class SpockExample extends Specification {

    def "minimum of two numbers must be #expect" () {
        expect:
        Math.min(left, right) == expect
        where:
        left  | right  || expect
        1     | 3      || 1
        0     | -1     || -1
        -1    | -2     || -2
    }
}
java.lang.Math#minを検証する単純なコードです。まず目につくのはwhere部分じゃないでしょうか。これはData Tablesという仕組みで、パイプ区切りで入力値や期待値を設定しておいて、ヘッダ部分で定義してある変数にセットし、expect部分で記述している検証コードをレコード数分行ってくれるというものです。

また、この検証メソッドには文字列でSpec(仕様)を記述することができます。この中ではData Tablesで利用している変数を#つきで記述すると、Specに変数を当てて結果を出力してくれるので便利です。

他にもSpockの機能はありますが、Javaで書くテストに比べて生産性はもちろん表現力も優れていることがわかってもらえるかと思います。BDDはもちろん、TDD開発も促進されますね!

Groovyとのうまい付き合い方

テストをGroovyで書くということですが、Groovyを経験したことの無いメンバーの方が多いので、うまい付き合い方を決めておいた方が良いです。

Spockの基本的な書き方(DSL部分)さえ覚えてしまえば、DSL以外は普通にJavaスタイルで書いてもらってOKで、無理にGroovyスタイルで書く必要は無いと思います(言わずとも皆Javaスタイルで書いていて、Groovyスタイルで書いていたのは自分だけであった)。

アプリ本体のコードをGroovyでとなれば話は別ですが、テストに限定した利用だったのでそう抵抗無く受け入れられた感じです。

Spring Frameworkとの統合

今のプロジェクトでは
Spring Frameworkを利用しているので、Spockとうまく連携できる仕組みが必要でした。幸いにもSpockにはSpring連携ライブラリがあります。pom.xmlに以下の依存を追加してあげれば導入できます。
<dependencies>
    <dependency>
        <groupId>org.spockframework</groupId>
        <artifactId>spock-spring</artifactId>
        <version>0.7-groovy-2.0</version>
        <optional>true</optional>
    </dependency>
</dependencies>
spock-springにあるorg.spockframework.spring.SpringInterceptorを使えば、Spockのテスト起動時にDIコンテナを初期化したり、@Transactionalでのトランザクション制御ができるようになります。今回のプロジェクトではもっとカスタマイズしたかったので独自にInterceptorを実装しています。

コントローラー部分にはSpringMVCを利用していますが、SpringTestとの親和性も良いです。

DBUnit

これも古くからあるテスティングフレームワークですが、DBが関わるテストでテストデータを投入するために利用しています。

Spring+Spockと連携するために拡張を施していて、以下のようにDbUnitというアノテーションを付与するとデータを自動で投入してくれます。この処理は前述のSpockの独自Interceptor内で実装しています。
class TestServiceSpec {
    @Autowired
    TestService testService
    @DbUnit(path = ["master/m_hoge.xml", "test/user.xml"],
        datasource = "masterDataSource")
    @Transactional
    def "testspec" () {
        // test code
        expect:
            true
    }
}

H2 Database

主たるデータストアはMySQLなのですが、ユニットテストに関してはJava製のインメモリDBである
H2 DatabaseをMySQLモードで利用しています。

GHE(Github Enterprise)にpushされたコードはすぐさまJenkinsでテストが実行されるようにしてCIをワークさせてますが、テストコードも多いのでインメモリであるH2を使ったほうが圧倒的にテスト時間は短くなります。

H2利用時の注意点は厳密なSQLを用意しなくてはならないということでしょうか。mysqldumpを使ってダンプしたようなDDLはたいてい通らないです。あらかじめスキーマをSQLで用意するような運用が必要になります。
また、このプロジェクトではFlywayVagrantを使って各自のDBのマイグレーションを運用しているので、この仕組みとH2でのテストがシームレスに連携できるような仕組みを構築中です。

導入してみて

おかげさまで順調にテストは積み上がりました。これらの施策を実施してみて高い生産性と表現力を得ることができたと思います。表現力の高いテストコードが書けるということは、他の人が書いたテストコードを理解しやすいというメリットがありますね。

また、大胆にリファクタリングがしやすくなりました。これが一番大きいですね。恐る恐るリファクタリングしなくちゃいけないような開発は精神を摩耗するだけです。

今後はこの仕組みをもっとブラッシュアップしてフレームワーク化して、「たのしくテストを書く」という文化とともに浸透させていきたいと考えています。

終わりに
というわけでみなさん、Groovyに飽きたらScalaをやりましょうね!