Play framework 2.x Java and 1.x Advent Calendar 2013の16日目(通算2回目)担当の kara_d です。

今回は、Play 2.2 Javaのユニットテストに、Scala版で採用されているspecs2を使ってみるという話題です。

Play 2には、JavaとScalaそれぞれの対応版があり、いずれかの言語を使ってそれぞれの世界観に基づいた開発を行うことが出来ます。

実は、Play Javaによる開発においても、Hoge.scalaみたいなファイルを任意のディレクトリに配置すると、普通にコンパイルされてJava側から使うことが出来ます。

であれば、おそらくspecs2もきっと動くだろうということで試してみました。

EbeanModelを用意

まずは、普通にJava版でプロジェクトを作成し、Modelを用意します。 Java版なのでEbeanです。CompactDiscという名前のモデルを用意しました。

これには、titleというフィールドが1つだけあります。 また、このモデルには、getOldCDというメソッドがあり、古い登録済のCDを返すという機能があります。

今回は、このメソッドをテストしてみます。

package models;

import static play.data.validation.Constraints.*;
import com.avaje.ebean.annotation.*;
import play.db.ebean.Model;
import utils.OptionUtil;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
import java.util.List;
import static play.libs.F.*;

@Entity @Table(name="compact_disks")
public class CompactDisc extends Model {

    @Id                 Long    id;
    @Required           String  title;
    @CreatedTimestamp   Date    created;
    @UpdatedTimestamp   Date    modified;

    // このメソッドをテストしてみる
    public static Option<List<CompactDisc>> getOldCD() {
        Model.Finder<Long, CompactDisc> finder =
                new Model.Finder<Long, CompactDisc>(
                    Long.class,
                    CompactDisc.class);
        return OptionUtil.apply(finder
                .where()
                .orderBy("created ASC")
                .findPagingList(1)
                .getPage(0)
                .getList());
    }

    // getters and setters
}

上記のCompactDiscモデルのgetOldCDは、下記のように使えます。

Option<List<CompactDisc>> newCD = CompactDisc.getOldCD();

FakeAppの作成

このユニットテストをする場合、Ebeanモデルのテストになるため、FakeApplicationを作成する必要があります。

まずは、モデルのユニットテストと、FakeApplication周りを切り離すため、FakeAppクラスを作ります。 モデルのテストクラスは、これを継承して行うこととします。

package apps;

import com.avaje.ebean.Ebean;
import org.apache.commons.io.FileUtils;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import play.test.FakeApplication;
import java.io.IOException;
import static play.test.Helpers.*;

public class FakeApp {
    public static FakeApplication app;
    public static String createDdl = "";
    public static String dropDdl = "";

    @BeforeClass
    public static void startApp() throws IOException {
        app = fakeApplication(inMemoryDatabase());
        start(app);

        String evolutionContent = FileUtils.readFileToString(app
                .getWrappedApplication().getFile("conf/evolutions/default/1.sql"));
        String[] splitEvolutionContent = evolutionContent.split("# --- !Ups");
        String[] upsDowns = splitEvolutionContent[1].split("# --- !Downs");
        createDdl = upsDowns[0];
        dropDdl = upsDowns[1];
    }

    @Before
    public void createCleanDb() {
        initDb();
    }

    public static void initDb() {
        Ebean.execute(Ebean.createCallableSql(dropDdl));
        Ebean.execute(Ebean.createCallableSql(createDdl));
        executeEbeanSQL();
    }

    /**
     * Execute SQL
     */
    public static void executeEbeanSQL() {
        Ebean.execute(Ebean.createSqlUpdate("INSERT INTO  `compact_disks` (`id`, `title`, `created`, `modified`) VALUES ('1',  'Vulgar Display of Power', '2013-12-17 12:34:56', '2013-12-16 12:34:56');"));
        Ebean.execute(Ebean.createSqlUpdate("INSERT INTO  `compact_disks` (`id`, `title`, `created`, `modified`) VALUES ('2',  'Cowboys from Hell', '2013-12-16 12:34:56', '2013-12-16 12:34:56');"));
        Ebean.execute(Ebean.createSqlUpdate("INSERT INTO  `compact_disks` (`id`, `title`, `created`, `modified`) VALUES ('3',  'Far Beyond Driven', '2013-12-18 12:34:56', '2013-12-16 12:34:56');"));
    }

    @AfterClass
    public static void stopApp() {
        stop(app);
    }

}

ここでは、起動時にDDLの実行を行い、データベースにテーブルを構築する他、初期データの投入を行っています。

YAMLによる挿入ではなく、SQLによる挿入を行う理由としては、SQL自体のテストを別で行う場合に取り回しがしやすいためです。

JUnitのユニットテスト作成

さて、上記を継承したJUnitのテストを書いてみましょう。

下記みたいな感じでコンパクトに書けます。

package models;

import apps.FakeApp;
import org.junit.Test;
import static play.libs.F.*;
import java.util.List;
import static org.fest.assertions.Assertions.assertThat;

public class CompactDiscTest extends FakeApp {

    @Test
    public void testGetOldCD() throws Exception {
        Option<List<CompactDisc>> newCD = CompactDisc.getOldCD();
        assertThat(newCD.getClass()).isEqualTo(Some.class);
        assertThat(newCD.get().get(0).getTitle()).isEqualTo("Cowboys from Hell");
    }

}

specs2で書く

さて、次はspecs2編です。

specs2のテストを書くといっても、何かをインストールしなければいけないとかの準備は不要です。testディレクトリ内でばりばり書いていけます。

まずは、Ebeanモデルテスト用の下準備としてFakeApp的なものを作ります。 specs2では、traitを使って、テストの開始時と終了時の処理を記述します。

下記のような感じになります。(ちなみにJavaのFakeAppのコードをIntelliJ IDEAでScalaのファイルにペーストしようとすると、Scalaコードに変換されます。すばらしいです。)

package apps

import org.specs2.specification.Scope
import org.specs2.mutable.After
import play.test.Helpers._
import org.apache.commons.io.FileUtils
import com.avaje.ebean.Ebean

trait FakeAppTrait extends Scope with After{

  val app = fakeApplication(inMemoryDatabase)
  val evolutionContent = FileUtils.readFileToString(app.getWrappedApplication.getFile("conf/evolutions/default/1.sql"))
  val splitEvolutionContent = evolutionContent.split("# --- !Ups")
  val upsDowns = splitEvolutionContent(1).split("# --- !Downs")
  val createDdl = upsDowns(0)
  val dropDdl = upsDowns(1)
  start(app)
  Ebean.execute(Ebean.createCallableSql(dropDdl));
  Ebean.execute(Ebean.createCallableSql(createDdl));

  def after = {
    stop(app);
  }

  /**
   * Execute SQL
   */
  def executeEbeanSQL = {
    Ebean.execute(Ebean.createSqlUpdate("INSERT INTO  `compact_disks` (`id`, `title`, `created`, `modified`) VALUES ('1',  'Vulgar Display of Power', '2013-12-17 12:34:56', '2013-12-16 12:34:56');"))
    Ebean.execute(Ebean.createSqlUpdate("INSERT INTO  `compact_disks` (`id`, `title`, `created`, `modified`) VALUES ('2',  'Cowboys from Hell', '2013-12-16 12:34:56', '2013-12-16 12:34:56');"))
    Ebean.execute(Ebean.createSqlUpdate("INSERT INTO  `compact_disks` (`id`, `title`, `created`, `modified`) VALUES ('3',  'Far Beyond Driven', '2013-12-18 12:34:56', '2013-12-16 12:34:56');"))
  }

}

続いて、specs2のテストを書きます。

JUnitに比べると、DSLっぽい感じで書けて、読みやすいです。

package models

import org.specs2.mutable._
import apps.FakeAppTrait

class CompactDiscSpec extends Specification {

  "Compact Disc" should {
    "new CD title is 'Cowboys from Hell'" in new FakeAppTrait {
      executeEbeanSQL
      val newCD = CompactDisc.getOldCD();
      newCD.get().get(0).title must equalTo("Cowboys from Hell")
    }
  }

}

さて、JUnitとspecs2が混在した状態で、testコマンドを実行するとどうなるでしょうか?

[advent] $ test
[info] Compiling 1 Scala source and 1 Java source to /Users/harakazuhiro/study_play22/advent/target/scala-2.10/test-classes...
Picked up _JAVA_OPTIONS: -Xms1024m -Xmx1024m -Xss1024m -Dfile.encoding=UTF-8
[info] models.CompactDiscTest
[info] + testGetOldCD
[info] 
[info] 
[info] Total for test models.CompactDiscTest
[info] Finished in 0.0020 seconds
[info] 1 tests, 0 failures, 0 errors

[info] CompactDiscSpec
[info] Compact Disc should
[info] + new CD title is 'Cowboys from Hell'
[info] Total for specification CompactDiscSpec
[info] Finished in 123 ms
[info] 1 example, 0 failure, 0 error
[info] Passed: Total 2, Failed 0, Errors 0, Passed 2
[success] Total time: 4 s, completed 2013/12/16 23:39:49

無事、両方が実行され、トータル件数も表示されました。

Javaの現場で、Scalaに慣れるための1つの手段として、ユニットテストをScalaで書くというのはどうでしょうか。

ぜひ試してみてください!

上記FakeAppを使ったJavaのユニットテスト手法は、Play framework 2 徹底入門でも丁寧に解説していますので、興味があればお買い求めいただけるとますます頑張れます。


Play Framework 2徹底入門
http://www.amazon.co.jp/dp/4798133922/

  • 発売日:2013年12月16日
  • ISBN:9784798133928
  • 判型:B5変
  • ページ数:540P
  • 定価:本体3,800円+税

アドベントカレンダー、まだ募集中です!

Play frameworkのアドベントカレンダーはScalaとJava両方があります。 まだまだ執筆者募集中なので、皆さんお気軽にぜひ!

Play framework 2.x Java and 1.x Advent Calendar 2013
http://www.adventar.org/calendars/104

Play framework 2.x Scala Advent Calendar 2013
http://www.adventar.org/calendars/114