Gradleを使ったWebアプリケーションのさくさく開発(war作成とJUnit編)

時間とモチベーションに余裕のあるうちに行けるところまで調べていきます。というわけで、今回はwarの作成と、Jettyでの起動確認。

コード

需要はないと思いますがGitHubに公開します(EclipseからGitを利用するのに妙にハマりました…。Gitはあまり使い慣れていないので余計なファイルが混ざってるかも…)

アプリケーションを作成する

作るwarはとりあえず何でもよいので、基本的なGETでメッセージを返すHelloServletを含むwarにします。

作ったファイルは以下の3つ。

  • sample.servlet.HelloServlet.java(src/main/java)
    • HttpServletを継承して、Get時にリソース内のファイル"hello.txt"を読み込んで、中身をそのまま返すServlet
  • hello.txt(src/main/resources)
  • web.xml(src/main/webapp/WEB-INF)
    • web.xml。src/mainの下にwebapp, WEB-INFディレクトリを作ってから、新規に作成。
    • Servlet3.0だとweb.xmlレスで開発できるみたい(使ったことはないです。。)ですが、今回は素直にweb.xmlを作ってます。


warファイルを作成する

warを作成するにはWarプラグインを使えばOK(参考 http://gradle.monochromeroad.com/docs/userguide/war_plugin.html

apply plugin: "war"

依存関係をいくつか追加しておきます。Jettyは先日9.0.0がリリースされましたが、まずは8系の最新で。他にもファイル読み込みを楽にしたり、HTTPクライアントとしてJerseyを使ってたりしますが、最低限必要なのは上の3つのみ。

dependencies {
    compile "org.eclipse.jetty.orbit:javax.servlet:3.0.0.v201112011016@jar"
    compile('org.eclipse.jetty:jetty-server:8.1.9.v20130131') { exclude module: 'javax.servlet' }
    compile('org.eclipse.jetty:jetty-webapp:8.1.9.v20130131') { exclude module: 'javax.servlet' }
    compile 'commons-io:commons-io:1.4'
    compile 'com.sun.jersey:jersey-client:1.17.1'
    compile 'com.sun.jersey:jersey-core:1.17.1'
    testCompile 'junit:junit:4.10'
}

ビルドスクリプト(build.gradle)の修正はこれだけです。

warの作成はEclipseから行う場合、「Gradle Tasks」ビューのwarをダブルクリックするだけでできます。ビルドが成功するとbuild/libsの下にwarファイルがgradle-sample-1.0.warとして作成されます。

というわけで、プロジェクトの構成を標準的なものにしていれば、warプラグインを使うことで、warを作成するためのタスクを一切記述せずに、warが作成できることが分かりました。Antで作りこむのと比べると、ビルドスクリプトの保守コストは0になりますし、俺流ではなく基本的な方法でビルドしているという心理的な安心感も生まれるので、いいことづくめ。

Tomcat上にデプロイして確認してみる(寄り道)

あまりに簡単に作成できてしまったので、寄り道をして、本当にできているか、Tomcatにデプロイして確認してみます。

Tomcatの導入手順

1. Tomcatをダウンロード(Tomcat の公式サイトから、今回は apache-tomcat-7.0.37.zip を選択)
2. 適当な場所に解凍して、環境変数CATALINA_HOMEを設定
3. conf/server.xmlの Connector port をデフォルトの 8080 から 28080 に変更(8080 は Jenkins と衝突しまったので)

後は、Gradleビルドで生成した build/libs/gradle-sample-1.0.war を webapps の下にコピーして、「bin/catalina.bat start」を実行。

うまく起動したら、http://localhost:28080/gradle-sample-1.0/hello にアクセスすると、"Hello, gradle!"が表示されます。作ったwarがちゃんとデプロイされているようです。


JUnitテストを動かしてみる

続いて、Jetty上にwarをデプロイして起動した状態で、GETリクエストで正しくレスポンスが取得できることを、JUnitテストコードとして検証できるようにします。

詳しいテストケースとしては、Jettyで起動する時のポートが 18080、コンテキストパスが "/gradle-sample" とすると、http://localhost:18080/gradle-sample/hello に GET アクセスすると、"Hello, gradle!" が表示出来ればOK。

そんなわけで、JerseyのHTTPクライアントを使うと、テストコードは下のようになります。余談ですが、JerseyのHTTPクライアントを使っているのはただの好み。

    @Test
    public void GETリクエストでHelloServletのレスポンスメッセージを取得できる() throws Exception {
        String expected = "Hello, gradle!";
        String actual = Client.create()
                .resource("http://localhost:18080/gradle-sample/hello")
                .get(String.class);

        assertThat(actual, is(expected));
    }

さて、JettyへのデプロイとJettyの起動・停止ですが、JUnitのRuleとして定義することにしました。今回はテスト対象が一つなので、@Beforeをつけたセットアップメソッドとして書いてもいいのですが、Ruleの方がテストコードがすっきりして見通しがよくなりますし、今後テストが増えていってもJettyの起動・停止みたいな横断的に共通な処理を、継承ではなく委譲の形でうまく集約できるので、便利だと思います。

というわけで、いきなりコード(sample.junit.rules.JettyApplicationServer)

/**
 * Jetty アプリケーションを起動・停止する Rule。
 */
public class JettyApplicationServer extends ExternalResource {

    private Server server;

    /**
     * テスト実行前に Jetty を起動する
     */
    @Override
    protected void before() throws Exception {
        server = new Server();

        SocketConnector connector = new SocketConnector();
        connector.setMaxIdleTime(1000 * 60 * 60);
        connector.setSoLingerTime(-1);
        connector.setPort(18080);
        server.setConnectors(new Connector[] { connector });

        WebAppContext context = new WebAppContext();
        context.setServer(server);
        context.setContextPath("/gradle-sample");
        context.setResourceBase("src/main/webapp");
        context.setClassLoader(getClass().getClassLoader());
        server.setHandler(context);

        // build した war を使いたければ↓でもOK!
        // context.setWar("build/libs/gradle-sample-1.0.war");

        server.start();
        System.out.println("server started.");
        // System.in.read();
    }

    /**
     * テストが終わったら Jetty を停止する
     */
    @Override
    protected void after() {
        try {
            server.stop();
            server.join();
            System.out.print("server stopped.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ExternalResourceを継承してRuleを作る場合、実行前に行う処理をbefore()、実行後に行う処理をafter()としてそれぞれオーバーライドすればよいので、before()に起動処理、after()に停止処理を書きます。

Jettyが用意しているAPIを使ってJettyの起動、停止を行う書き方については、下記の記事を参考にしました。起動と停止でbefore()とafter()に分けた点と、System.in.read()をコメントアウトして処理待ちにならないようにしたのが主な変更点です。

後は、このRuleを適用するようにテストコードに追記します。

    @Rule
    public JettyApplicationServer jetty = new JettyApplicationServer();

これだけで、直接テストクラスを実行しても、gradleのtestタスクを実行しても、テスト実行前にJettyが起動し、実行が終わるとJettyが停止した上で、きちんとGreenになります。

コードはシンプルですが、テストが実行されるたびにJettyへのデプロイとJettyの起動、停止が行っているので、ユニットテストよりは結合テストという位置づけに近いのかもしれないです。

ただし、テストの実行とJettyの起動が同一のJVMなので、結合テストで実施すべき「本物の」環境とはやはりずれがあるのは注意。Java EE結合テストフレームワークのArquillianでいうところのEmbeddedタイプ。本物っぽいけど本物とずれている結合テストを実施する際に留意すべき点については、nekop さんの記事が参考になります。

Embeddedタイプのコンテナの場合、テスティングフレームワークが管理するJava VM上でコンテナが起動されてテストが実行されることになります。本物のコンテナとは異なる起動のされかたをされ、本番で利用する設定は参照されず(もしくは個別に手動で設定する必要があり)、クラスローディングなども本物の環境とは異なる形で行われることになります。

http://d.hatena.ne.jp/nekop/20111219/1324345312

このように心に留めておくことはありますが、やはりさくっとデプロイしてアプリケーションの挙動を確認できるのは精神的にかなり楽です。ちなみに上記テストは、自分の環境だと1秒程度で完了します。

ろいうわけで、Groovy基礎勉強会行けばよかったかなぁと思いつつ、家でGradleと戯れての記事でした。