読者です 読者をやめる 読者になる 読者になる

『「納品」をなくせばうまくいく』というパラダイム

久しぶりのブログです。
コードの匂いが全くしない内容です。

「納品のない受託開発」のSonicGardenの社長 倉貫さんが、日々実践されていることを本にまとめられたので、早速読みました。

「納品のない受託開発」は、SIerとよばれるIT企業による受託開発モデルの課題を解決するためのビジネスモデルです。 SI型のシステム開発は、企業内の開発部隊が作る「内製」とは異なり、他の企業にシステム開発を発注します。 多くの場合、企業内の情報システム部門や大企業のIT系子会社が発注しています。

私はIT子会社(ユーザ系)に所属していますが、書籍に書かれているような課題は常日頃感じています。 そこで、受注側ではなく発注側における目線で、システム開発の「常識」と「課題」についてこの機会に書いてみます。

情報システム部門におけるSI型システム開発

1.役割分担

情報システム部門は、企業内に利用者(ユーザ部署)や企画部署があるという立場から、社内の資金や意思決定に関わる調整と開発のスケジュール管理を行うことが多いです。

技術は日々生まれては進歩しています。多くの問題を解決出来るようになっている一方、システム開発に必要な技術の複雑性は増しています。日々の業務をこなすだけでは、技術の進歩に置いていかれるため、実際にシステムの開発を行う部分をSI専門の企業に受託開発をお願いしています。

システム開発に関わることになると、いわゆるSI的なシステム開発をするケースが多いです。 要望・要求をベースに、システムの設計・開発・運用を含めてSIerと言われるシステム開発会社に発注すると言うものです。 システム開発は非常に難しいもので、過去の失敗から改善を繰り返し、そこから学んできたことを愚直に実践していると思います。

2.ドキュメントによるナレッジの蓄積

組織でシステムの開発や保守を行うと、担当者は定期的にローテーションで部署異動が行われます。 特定の個人しか知らない事が多い中で、担当者が抜けてしまうと、たちまち業務が立ち行かなくなってしまいます。 そのため、システムを開発する際はドキュメントを沢山作ることで、知識を人ではなく組織にためていきます。 次に来た担当者は、ドキュメントを読めば業務を引き継ぐことが可能です。

3.ウォーターフォール型の開発プロセス

ウォーターフォール型と呼ばれる要件定義から開発、テストまでのプロセスを1つずつ完了させ、次のプロセスにうつる方法が多くの案件で採用しています。 設計がちゃんと終わっていないまま開発を行うと、設計の変更によって作り直しが発生し、無駄な開発体力の消費とスケジュールの遅延を発生させます。前の土台がしっかりしていないのに次の土台を作れないという考え方がベースにあります。

とはいえ、現実には次のステップに進む前に、開発途中で発生するトラブルや、計画時に想定されていなかった作業などから遅延するということが起きがちです。スケジュールや見積りでバッファを設けますが、バッファを食いつぶすような問題が発生することも、まれによくあることです。 開発やテストなどのそれぞれのプロセスに直列的にスケジュールが引かれるため、1つでもスケジュールが遅延すると後続のプロセスもその分遅れていきます。デスマーチと呼ばれる過酷な労働は、納品期限に間に合わせるため遅延を取り戻そうとすることで発生します。

この開発手法は、過去の失敗からの改善を積み上げてきた結果です。 大規模なシステム開発を中心にこれまでに一定の成功を納めてきたと思います。

SI的開発手法の課題

ただし、とりまく環境が変わって来ました。 詳細は書籍を読んでいただくとして、ここでは簡単に今の開発プロセスの課題を簡単に記載します。

1.計画が全て = 計画を見誤れば失敗のリスク

ウォーターフォールという開発手法は計画が全てです。 計画が見誤っていたり、とりまく環境が変わった場合に、抜本的な変更を行うことができません。 つまり、変化に非常に弱い方法です。数年規模の開発を行っていた結果、開発していたものがリリース以前に不要なものとなるリスクを抱えています。 また、計画外のトラブルや検討漏れはどうしても発生します。

2.膨らみ続けるシステム・コスト

最初に計画したスケジュールよりも大きく前倒しで納品するような事例は、ほとんど聞いたことがありません。 計画よりも大幅に前倒しに終わるということは、発注側からすると「余分」なコストが発生しており、過剰見積りと見なされてしまいます。 また、もし次のシステム開発があったとして、見積り金額よりも低い金額でしか予算が獲得できなくなる「恐れ」があります。 これは、トラブル等のためのバッファ分が削られることを意味し、次の開発で納期に間に合わなくなるリスクが高まることにつながります。 そのため、システム開発の予算は膨らみやすくても縮小しにくいです。

3.引き返せないタイミングでの確認

情報システム部門は、開発しているシステムの直接的な利用者や業務の担当者ではありません。 作ったシステムに対して、計画通りかどうかは判断できたとしても、業務的に正しいかどうか、利用者が使いやすいと感じるものかどうかは、判断が難しく一般論の範囲でしか意見を言うことができません。 そこでシステム開発の最終段階で、担当者による受け入れテストが行われます。 受け入れテストでは、担当者が実業務を想定した動作検証や、利用者の操作シミュレーションを行います。 なぜ最終段階で行われるかというと、ウォーターフォール型の開発が1つずつのステップを経ることで動作するシステムを作るため、最終段階にならないと設計した機能が使えると保証できないためです。

ただし、もし受け入れテストで重大な問題があったとしても、最終段階のため取り返すことは絶望的です。
もちろん、そんなことが行われないために、沢山の認識合わせの打ち合わせと、設計書確認による言質のようなもので担保しようとします。それでも重大な問題が発生してしまった場合は、利用者に負担を強いる運用でカバーする方法がとられるか、また予算を確保して追加開発を行う方法か、それらの組み合わせで対応されます。 全部の機能が動かなくとも、優先度の高い部分だけでも早めに確認すれば、重大な問題となるリスクがおさえられるはずです。

なぜこれらの問題は変えられないか

企業によって事情は様々だと思いますが、これまで記載してきたような開発手法にビジネスモデルや業務スタイルをフィットさせてきたためといえます。
予算獲得のための提案から稟議承認までの各種ワークフローや、受発注手続きといった金銭周りの仕組みから、開発現場における成果物の定義からスケジュール管理、開発・運用の体制までが数十年をかけて、納品のある開発スタイルに最適化されてきました。
また、そうした環境で仕事を何年も何十年もやってきた方には、常識として考え方や取り組み方が染み付いていると思います。
また、少しずつ改善はしてきても根本的な問題として常に立ちはだかってきたため、「変えられないもの」と思い込んでしまっているのかもしれません。
そして本当のところは、「今の方法で成功はしていないけど、それほど失敗していないじゃない。目の前の仕事に追われてそれどころじゃないよ」と考える方の割合が多いのかもしれません。
でも翻って、目の前の忙しさって、実は今の開発プロセスの歪みから生まれている部分なのでは?とも思います。既存のパラダイムの限界で整合性が保てなくなってきていて、次のパラダイムへ移行する必要が出てきているフェーズなのかもしれません。
(この辺りの話は、『科学革命の構造』で語られている内容と同じなのかなと。)

「正しい」方法で、「一か八か」ではなく堅実な開発

SonicGardenさんの「納品のない受託開発」は、納品のある受託開発とは抜本的に異なる手法でありながら、システム開発を「一か八か」にしない方法だと思います。
全部を作らずに優先順位に基づく開発や、月額定額性、技術者の採用、どれをとっても堅実です。 既存の方法でうまく行っていない点を、「正しい」と思える方法で解決しています。

また、ともすれば「完成させること」が目的になりがちなシステム開発を、ビジネス成功のための手段とするアプローチです。
例え「銀の弾丸」でなかったとしても、パラダイムを変えて目線を変えれば、「変えられない」と思っていたことが実は変えられるものになり得ると教えてくれます。

どんな方法かは、ぜひ実際に書籍を手にとって読んでください。

さいごに

システム開発の新しいパラダイムとして、世の中に広がっていき、多くの人が喜んだり幸せになり価値を生むシステムが増える事を願うとともに、自身も何らかの形で実践していきたいと思います。

「納品」をなくせばうまくいく ソフトウェア業界の“常識

「納品」をなくせばうまくいく ソフトウェア業界の“常識"を変えるビジネスモデル

Chromeの履歴の検索結果を全て選択して消し去りたい

あるサイトを4週間以上見てるけど、このサイトの履歴をChromeに残したくないけど1つずつクリックして消すのが面倒だなー。
かといって、Chromeの履歴を全部消すのは嫌だし。
そんなことあると思います。

ということで手順を作りました。

1. 履歴を開く

 Windowsなら"Ctrl + H", Macなら"command + Y" で履歴を開きます。

2. 履歴を検索する

 画面右上の検索ボックスに検索したいものを入力します。
 特定のサイトとかだったらURLのホスト名とかで検索すると良いと思います。

3. JavaScriptコンソールを開く

 Windowsなら"Ctrl+Shift+J",
 Macなら"command + alt(option) + J"
 でJavaScriptコンソールを開きます。

4. history-frameを選択する

 履歴ページがiframeを多用した画面なので、
 <top frame>と書いてあるところをクリックして、
 history-frameを選択します。

5. consoleにコードをコピペしてEnter

 以下のJavaScriptコードをコンソールにコピペしてEnterします。

(function(){
    //履歴を全て選択する
    var selectAll = function(){
      for(var i = 0;i < 150;i++) {
        var id = 'checkbox-' + i;
        $(id).checked = 'checked';
      };
    };

    //履歴を削除する
    var rmSelected = function(){
      removeItems();
      $('alertOverlayCancel').click();
    };

    function deleteAll(){
       try{
         selectAll();
         rmSelected();
       } catch(e) {
            console.log("たぶん読み込み中");
       }
    };

    deleteAll();
    setInterval(function(){ deleteAll(); }, 5000); 
})();

これで5秒ごとに履歴を削除し続けてくれます。 5秒長いよという方はsetIntervalの5000のところを適当な数字に変更してください。ミリ秒です。

うまく動かない場合は、history-frameが選択出来ていない可能性が高いです。
もしくは、Chromeの履歴ページの仕様が変わってしまったら使えなくなるかもしれません。

履歴を削除すると、Chromeは勝手に再検索してくれることを使ってみました。 削除し終わっても動き続けるので、画面をリロードするか閉じてください。

Play Frameowrk2.x Javaのテスト入門 ~Controller, View編〜

Advent Calendarは終わってしまいましたが、今年中になんとかplay!2.x Javaのテスト入門の続きを書きます。

Controllerのテストの難しさ

初っ端からですが、play!のControllerのテストはどうやって書けばいいかとても困りました。Controllerは、出力としてResult型のオブジェクトを返します。このResult型の中身は、httpステータスコードだけでなくViewに渡された処理結果のコンテンツ(html)が入っています。
ControllerをテストするときはViewに渡す引数オブジェクトをインターセプトしてテストできると良いのですが、残念ながら出来ません。そのためassertThat(contentAsString(result)).contains("<td>" + user.name + "</td>");のようなViewの中身を意識した書き方をします。
非常に面倒ですし、厄介です。

ControllerとViewの役割を見直す

悩みながら改めて自分のコードを見直して気付いたのですが、Viewで使う変数は全てControllerから引数として渡そうとしていました。Viewのロジックをできるだけ減らして、ControllerとModelにロジックを書くような方針です。でもそうするとテストを書くのがつらい。
ということで、ロジックをControllerからViewに出来るだけ移すことにしました。Controllerをなるべくシンプルに、最低限のCRUD操作とエラーハンドリング、ページハンドリングに絞ることにしました。その結果テストが書きやすくなったと感じます。

Controllerのテスト

ようやくControllerのテストを書いてみます。よくある一覧を返すindexの処理です。 シンプルにするためにページングは行いません。

indexのメソッドを呼び出すには二つの方法があります。

  • callAction()
  • route()

callAction()

callAction()は、直接Controllerのメソッドを呼び出す方法です。
こんな感じで書きます。

Result result = callAction(
        controllers.routes.ref.UserController.index()
);

冗長なので、staticのimportをした方が良いと思います。

import static controllers.routes.ref.UserController.*;
     ・
     ・
     ・
Result result = callAction(UserController.index());

route()

route()は、擬似的にhttpリクエストを投げるので、conf/routesの定義も合わせてテストすることになります。ルーティング設計をしてからControllerを作ると思うので、ここはテストの分割にこだわる必要はないかなと思います。こんな感じで書きます。

Result result = route(fakeRequest(GET, "/users"));

シンプルでいいですね。

よくある200 OKで返すパターン

では、実際に一覧表示をするだけのindexメソッドのテストを書いてみます。 Inputとして、httpリクエストで”GET”で”/users”を投げたとき、Outputとして

を期待します。

import org.junit.*;

import static org.fest.assertions.api.Assertions.*;
import static play.test.Helpers.*;

import play.mvc.*;
import models.User;

public class UserControllerTest {
    @Before public void setUp() {
        start(fakeApplication(inMemoryDatabase()));
    }

    @Test public void callIndex() {
        Result result = route(fakeRequest(GET, "/users"));

        assertThat(status(result)).isEqualTo(OK);
        assertThat(contentType(result)).isEqualTo("text/html");
        assertThat(charset(result)).isEqualTo("utf-8");
    }
}

あとは、indexを呼んだ結果としてUserのリストが画面に表示されればよいです。自動テストとしては、どのViewTemplateが呼び出されたかを確認できるといいのですが、今のところ直接Result型に入っているhtmlの内容をでテストする方法しか知りません。assertTemplate("index");みたいな書き方ができればいいのですが。。。

さて、htmlの内容をテストするには、contentAsString(result)を使います。この結果の中にUserのリスト情報が入っているかをチェックします。ただし、このテストをController側で書こうとすると、テストパターンが非常に多くなり、SessionとかCacheとか使い始めるとなおさら辛くなっていきます。そのため、私はControllerからViewに引数を渡す場合を除いて、contentAsString()によるテストは書かない方針にしています。

ダイレクトするパターン

play!でリダイレクトをすると、httpステータスコードは303 See Otherとなります。
例えば、POSTメソッドで処理された結果、リダイレクトでindex画面(/users)に返るとします。その場合は、

を期待します。

@Test public void callPost() {
    Result result = route(fakeRequest(POST, "/users/post”));

    assertThat(status(result)).isEqualTo(SEE_OTHER);
    assertThat(redirectLocation(result)).isEqualTo("/users");
}

ダイレクト先でcontentTypeとかcharsetをテストをしているので、リダイレクト元では必要ありません。(というか、そもそも書きようがない)

formデータ付きのPOSTリクエスト

faleRequest()にメソッドチェインで.withFormUrlEncodedBody(Map<String,String> params)を渡すとformのデータを送ったテストができます。Mapで渡すので、事前準備が少々冗長になります。

@Test public void callCreate() {
    Map<String, String> params = new HashMap<String,String>();
    params.put("name", "Alice");
    params.put("email", "alice@email.com");
    params.put("password", "password");

    Result result = route(
            fakeRequest(POST, "/users")
            .withFormUrlEncodedBody(params)
            );

    assertThat(status(result)).isEqualTo(SEE_OTHER);
    assertThat(redirectLocation(result)).isEqualTo("/users");
}

今回はPOSTの結果が正常終了してリダイレクトするパターンでしたが、POSTの内容に問題があった場合は、badRequest()で返すと思います。その場合は、httpステータスコードが400の”BAD_REQUEST”になっているかどうかをテストします。

assertThat(status(result)).isEqualTo(BAD_REQUEST);

form項目数の多い時にパターンテストをするのは大変なので、全部のパラメータをセットしたMapを返すメソッドを準備しておき、必要に応じて”remove”するようにしています。

callAction()でも以下のようにかけます。この後で書くsessionも同様です。

Result result = callAction(
     UserController.create(),
     fakeRequest().withFormUrlEncodedBody(params)
);

Session付きのリクエスト

fakeRequest()にメソッド・チェインで.withSession(“key”, “value”)をつなぐだけです。

Result result = route(
    fakeRequest(POST, "/users")
        .withFormUrlEncodedBody(params)
        .withSession("email", "alice@email.com")
);

これでControllerのおおよそのテストは書けるのではと思います。

Viewのテスト

Controllerと比べるとViewのテストは比較的シンプルです。Viewにロジックを書くため、DB内のデータ状態を整えるなどの事前準備が必要になる程度です。

package views;

import org.junit.Test;
import org.junit.Before;

import models.User;
import views.html.user.*;
import helper.UserHelper;

import static org.fest.assertions.Assertions.*;
import static play.test.Helpers.*;
import static play.data.Form.form;

public class UserViewTest {
    @Before public void setUp() {
        start(fakeApplication(inMemoryDatabase()));
    }

    @Test public void indexRender() {
        Ebean.save((List) Yaml.load("testData/users.yml"));

        String htmlString
          = contentAsString(index.render());

        List<User> users = User.find.findList();
        User user = users.get(0);
        assertThat(htmlString).contains("<td>" + user.name + "</td>");
        assertThat(htmlString).contains("<td>" + user.email+ "</td>");
        assertThat(htmlString).contains("<a href=\"/users/" + user.id + "\">link</a></td>");
    }
}

contentAsString(index.render())とやるとView処理の出力(html)をString型で受けることができます。引数ありの場合はcontentAsString(show.render(user))のようにします。
あとはassertThatでcontains()startsWith()などを使ってテストしていきます。 DBの検証時の”状態”準備は、yamlで準備しておくと再利用可能でよいです。

その他

テスト書くときに読んだフレームワークのソースをあげておきます。

  • libexec/framework/src/play/src/main/scala/play/api/http/StandardValues.scala
    • いろんな定数が定義されているので、見ておくと便利。
  • libexec/framework/src/play/src/main/scala/play/api/Results.scala
    • assertion対象の状態が集約されているので、何が検証可能か確認できる。
  • libexec/framework/src/play-test/src/main/java/play/test/Helpers.java
    • controllerテストで使うメソッドや定数が詰まっているので目を通しておくと、読みやすいコードが書けると思う

おわりに

テストの書き方について整理してきましたが、WebSocketを使った場合とかはまだ書いたことないので万全ではないと思います。
なので@kara_dさんの「Play framework 2 徹底入門」などを読んで補完してもらえたらと思います。特に4章以降の実践編は、名前の通り複数人開発を想定した場合の注意点とかまで考慮されていて大変参考になりました。

Play! JavaでTwitter BootStrap3を使う

この記事はPlay framework 2.x Java Advent Calendar 2013の12/19分です。
昨日は@s_kozakeさんのPlay framework1系をWindowsサービスで動かす方法でした。

Play! Javaでは、Bootstrap2を使うためのライブラリは準備されていますが、Bootstrap3はありません。 例えばフォームでデフォルトのhelperを使おうとしても、ラベルと入力エリアのscaffoldの幅指定を分けられなかったりして、form-holizotalがうまく使えず、困ってしまいます。

とはいえ、全部HTMLタグのみでゴリゴリ書くのもしんどいので、bootstrap3用のhelperを準備してあげると幸せになります。

viewsディレクトリにbootstrap3ディレクトリを作成して、そこに標準テンプレートを作ります。inputタグの場合は以下のような感じです。

@(field: Field, inputType: String = "", label: String = "", placeholder: String = "", help: String = "")

<div class="form-group @if(field.hasErrors) {has-error}">
  <label class="col-sm-2 control-label">@label</label>
  <div class="col-sm-10">
    <input type="@inputType" 
    class="form-control" 
    id="@field.id" 
    name="@field.name" 
    value="@field.value.getOrElse("")"
    placeholder="@placeholder" />
    @if(field.hasErrors) {
    <span class="help-block">@help</span>
    }
  </div>
</div>

実際のView側は、bootstrap3をimportすることでテンプレートが使えるようになります。以下のような感じです。

@(requestForm: Form[Request])

@import helper.form
@import bootstrap3._

@main {
  <div class="row">
    <div class="col-xs-6">
      @if(requestForm.hasErrors) {
      <p class="error alert alert-danger">
        @Messages("error.form")
      </p>
      }
      @helper.form(action = routes.Requests.create, 'class -> "form-horizontal") {

      @input(requestForm("title"),
      inputType = "text",
      label = "タイトル",
      placeholder = "タイトルを入力してください",
      help = Messages("error.title")
      )
      
        <input type="submit" class="btn btn-primary active" value="保存">
      }
    </div>
  </div>
}

このやり方はBootstrap3に限った話ではなく、よく使う構成とかがあれば、同じようにテンプレートを作っておくと再利用が可能なコンポーネントのような扱いができます。

この方法はplay-example-formを参考にしました。 いろいろ応用を考えてみると面白そうです。

明日のAdventCalendarは@yubaさんです。

追記

作りかけのまま放置されてますが、参考用のリポジトリのリンクを貼っておきます。

taise/workflow · GitHub

Play Frameowrk2.x Javaのテスト入門 ~準備からモデルまで〜

この記事はPlay framework 2.x Java Advent Calendar 2013の12/4分です。

はじめに

最近Play Frameworkの1系と2系を触る機会があり、どうやらこのWAFにはテストサポート機能があるらしいということで、せっかくなのでテスト・ファーストでアプリを書いてみようと思い立ちました。

公式サイトを訪れたところ、テストについて書いてあるページは2ページしかなく、チュートリアルのzentasksは説明の中でテストを一部書いているものの、Githubのリポジトリをみてみたらテストがありません。

これではplay初心者には、テストを書くのが大変辛い。
Play Java2系な人にテストを書いてもらって、ブログとかにどんどんノウハウをためていって欲しいということで、JUnitでのテスト入門です。

偉そうに書いていますが、PlayもJUnitも未熟なので、こうした方が良いとか、クソコードだとか突っ込みもらえるとうれしいです。

1. テストの実行

playのテストはコマンドライン、もしくはIDEで実行することができます。
IDEでJUnitを動かすには、Run As => JUnit testで実行します。

コマンドラインは以下の2通りで実行できます。

$ play test

もしくは

$ play
[projectName] $ test

テストが増えてきて、開発している特定のテストだけを実行したい場合は、playのコンソールモードでtestOnlyで指定して実行できます。一度全テストを実行した後であれば、testOnlyの後でtabを押すと引数で指定できるクラスの一覧が表示されます。
地味に便利。

$ testOnly targetTestClassName

2.AssertThatを使う

いまどきのテストコードはassertEqualsとかではなく、自然言語の表現に近いassertThatを使います。

playではHamcrestをわざわざ追加する必要はありません。
Festがもともと入っているので、以下のように宣言するだけでassertThatが使えます。

import static org.fest.assertions.Assertions.assertThat;

ところで、日付のテストをしようとした場合、オレオレ拡張をしたライブラリを作ること、あると思います。実はFestの2系を入れるとこんな感じでisToday()とかisBefore()とか書けてしまいますので、車輪の再発明しなくてすみます。

折角なのでsbtを使って入れてしまいましょう。

3.build.sbtを使う

ということで、早速Festの2系をインストールします。

name := "playTest"

version := "1.0-SNAPSHOT"

libraryDependencies ++= Seq(
  javaJdbc,
  javaEbean,
  cache,
  "org.easytesting" % "fest-assert-core" % "2.0M10" % "test"
)

play.Project.playJavaSettings

これでplay runとかすれば、2系のライブラリを使えるようになります。どうしてもHamcrestが使いたい人は同様にbuild.sbtに追加してください。

  "org.hamcrest" % "hamcrest-core" % "1.3" % "test",
  "org.hamcrest" % "hamcrest-library" % "1.3" % "test"

Eclipseとかを使っている方は、sbtで追加したライブラリは勝手には読み込んでくれませんでしたので、playがインストールされているディレクトリのlibexec/repository/cacheあたりにあるjarを追加してください。

4.モデルのテスト

さて、準備はととのったということで、早速モデルのテスト書いてみます。 以下のようなUserモデルがあるとして、findByEmailメソッドのテストを書いてみます。

package models;

import java.util.Date;

import javax.persistence.*;
import play.db.ebean.*;

@Entity
public class User extends Model {

    @Id
    public Long id;
    public String name;

    @Column(unique=true)
    public String email;
    public String password;

    public Date createdAt;
    public Date updatedAt;

    public User(String name, String email, String password) {
        this.name = name;
        this.email = email;
        this.password = password;

        this.createdAt = new Date();
        this.updatedAt = new Date();
    }

    public static Finder<Long, User> find
        = new Finder<Long, User>(Long.class, User.class);

    public static User findByEmail(String email) {
        return User.find.where().eq("email", email).findUnique();
    }
}

テストコードは、プロジェクト直下のtestディレクトリ以下に作っていきます。
デフォルトでは、 ApplicationTest.javaとIntegrationTest.javaがありますが、ここに直接テストファイルをどんどん置いていくと管理が煩雑で、意味/機能分けがやりにくいのでpackegeを切っておくと良いと思います。
私はプロダクション・コードとの対応関係がわかりやすいようにappsディレクトリと同じ構造でパッケージを切っています。

test
└── models
    └── UserTest.java

モデルのテストをするにはDBが必要です。テスト用のRDBのテーブルを準備するのもありですが、単体テストを書くには管理を考えると少々過剰です。 簡単便利なインメモリDBを使いましょう。

start(fakeApplication(inMemoryDatabase()));

正常系のテストを書くとするとこんな感じでしょうか。

package models;

import models.User;
import org.junit.*;

import static org.fest.assertions.Assertions.assertThat;
import static play.test.Helpers.*;

public class UserTest {
    @Test
    public void findByEmail() {
        String email = "alice@email.com";
        start(fakeApplication(inMemoryDatabase()));
        User user = new User("alice", email, "password");
        user.save();

        User actual = User.findByEmail(email);

        assertThat(actual.email).isEqualTo(email);
    }
}

このテストをコンソールで実行すると無事にテストが通ります。 ただし、多くの人がIDEでJUnitを実行すると思いますが、残念ながらそのままだとEbeanでエラーが出てしまいます。
この問題はSampoさんが解決方法を書いていらっしゃいます。 が、自分は普段Eclipseを使わないため、やり方がわからず苦労しました。。。

動くように変更するには、以下の手順で設定します。

  1. メニューから"Run"を選択
  2. "Run Configurations..."を選択
  3. 対象のUnitTestのArgumentsのVM argumentsに以下を設定します。
    /path/toのところは、各自のpathを指定してください。
-javaagent:/path/to/avaje-ebeanorm-agent.jar

無事動いたところで、異常系のテストも追加します。
事前処理で共通するものは@Beforeでまとめてしまいます。

public class UserTest {
    static final String EMAIL = "alice@email.com";

    @Before
    public void setUp() {
        start(fakeApplication(inMemoryDatabase()));
        User user = new User("alice", EMAIL, "password");
        user.save();
    }

    @Test
    public void findByEmail() {
        User actual = User.findByEmail(EMAIL);

        assertThat(actual.email).isEqualTo(EMAIL);
    }

    @Test
    public void dontFindByInvalidEmail() {
        String typoEmail = EMAIL + ".";
        User actual = User.findByEmail(typoEmail);

        assertThat(actual).isNull();
    }
}

モデルのテストが増えていったら@RunWith(Enclosed.class)で、階層化して分けていくと良いでしょう。

今回のテストは1つのユーザーデータだけですんだので良かったですが、データのバリエーションテストをしたい場合に、毎回new User()とかやっていられないです。 そんなときはFixtureです。YAMLで定義してあげると可読性も高くて管理しやすいです。

5.Fixtureでデータ管理

play2系のfixture用yamlファイルはconfディレクトリに作ります。 play1系は、yamlファイルをtestディレクトリに配置していたため、動かなくてはまりました・・・。

yamlをconf以下に作るのはいいですが、そのうちテストごとにyamlファイルを作りたくなると思います。そうするとconfディレクトリがカオスになってしまうので、私はconfディレクトリ内にtestDataディレクトリを作って管理しています。

conf
└── testData
    └── users.yml

Userモデルのyamlサンプルを以下に記載します。 なお、1つのyamlファイルに複数のデータを書くこともできます。

- &alice !!models.User
    name:      Alice
    email:     alice@email.com
    password:  password
    createdAt: 2013-01-01 00:00:00
    updatedAt: 2013-01-01 00:00:00

- &bob !!models.User
    name:      Bob
    email:     bob@email.com
    password:  password
    createdAt: 2013-01-01 00:00:00
    updatedAt: 2013-01-01 00:00:00

このYAMLファイルをテストで使うには以下のように書きます。

Ebean.save((List) Yaml.load("testData/users.yml"));

今回の例では独立したモデルですが、当たり前のようにOneToManyとかのリレーションを持たせたくなると思います。この場合もYAMLでデータを表現することができます。

例えば、このUserモデルがCompanyモデルと結合している場合は、以下のように書くことができます。

- &bobCorp !!models.Company
    corpId: 000001
    name:   bobCorporation
    phone:  00-0000-0000

- &alice !!models.User
    name:      Alice
    email:     alice@email.com
    password:  password
    company:   *bobCorp
    createdAt: 2013-01-01 00:00:00
    updatedAt: 2013-01-01 00:00:00

もしくは、一意になる情報を使って引っ張ってくることも可能です。その場合は、Userモデルのcompanyの部分を以下のように書いてあげます。

    company:  !!models.Company
                     corpId: 000001

ただし、読みやすさを考えると、前者の書き方をすることをお勧めします。

Fixtureの準備ができたので、テストコードをリファクタします。

package models;

import models.User;
import org.junit.*;
import java.util.List;
import play.libs.Yaml;
import com.avaje.ebean.Ebean;

import static org.fest.assertions.Assertions.*;
import static play.test.Helpers.*;

public class UserTest {
    static final String EMAIL = "alice@email.com";

    @Before
    public void setUp() {
        start(fakeApplication(inMemoryDatabase()));
        Ebean.save((List) Yaml.load("testData/users.yml"));
    }

    @Test
    public void findByEmail() {
        User actual = User.findByEmail(EMAIL);

        assertThat(actual.email).isEqualTo(EMAIL);
    }

    @Test
    public void dontFindByInvalidEmail() {
        String typoEmail = EMAIL + ".";
        User actual = User.findByEmail(typoEmail);

        assertThat(actual).isNull();
    }
}

これで、テストコードとしてノイズとなるデータ準備について外部化して、可読性をあげることができました。また、YAMLは他のテストコードでも再利用可能です。
ただし、外部化したYAMLのデータはテストコードとの依存関係が切れている訳ではないので、管理する対象が2つに増えたり、テストコードを読んだだけだとテスト実行前の状態がわからないといった課題はありますので、トレードオフを考えて活用すれば良いと思います。

コントローラのテスト入門は次回

今回の記事はここまでです。本当はコントローラーまで書きたかったですが、間に合わなかったので次に持ち越したいと思います。

なぜJUnit

今回の記事の最後に1つだけ。 BDDっぽく書けるspecs2ではなく、なぜ今更JUnitの記事なのかについて私の考えを書いておきたいと思います。

つまるところ、speces2を書くにはScalaも書けるようにならないといけないからです。私はもともとRubyを書いているのでSpecで書けるほうがうれしいです。が、テストでScalaが書けるならプロダクション・コードもScalaで書けば良いです。

Scalaの知識が不十分な状態でテストを書いても、正しい振る舞いであることを自信を持って書けない訳で、それってテストとしてどうなの?と思います。

そういうわけで、テスト対象をJavaで書くならテストもJava派です。 もちろん、ScalaとかGroovyとか、よりよい手段を活用して書けるのが理想的ですけど、Scala書けて当たり前というようなスキルの高いチームはなかなかなさそうというのが現状ではないかなと。

明日のPlay framework 2.x Java Advent Calendar 2013

Play Framework 2徹底入門を執筆されたkazuhiro haraさんです。
本が発売されるのが楽しみですね。

MySQLでスレーブ作る

作ることになったので、試してみてつまづいた点とか整理しておく。
なお、レプリケーションそのものの設定手順については、とみぞーノートさんが素晴らしいのでそちらを参照。

今回はお試しなので同じサーバ内にMaster-Slave構成を作る。 同じサーバ内なので以下のものがかぶらないように注意する。

  • datadir
  • log
  • pid
  • socket
  • port  

既にMasterが稼働している状態からSlaveを作る所からはじめる。なお、ソースをコンパイルしてインストールしたので初期状態はだいたい以下のような感じ。

  • datadir => /usr/local/mysql/data
  • log => datadir内
  • pid => datadir内
  • socket => /tmp/mysql.sock
  • port => 3306

これらを、Masterは/var/mysql以下に、Slaveは/var/mysql_slave以下に変更してかぶらないようにする。また、せっかくなのでオペミスを防ぐためにそれぞれ出力先フォルダも変える。(portをのぞく)

Masterの作業

まずはもろもろを準備する。
スレーブの同期を開始するときに、保存しているバイナリログのどこまで行っているかを教えてあげるので、先に確認しておく。

mysql> FLUSH TABLES WITH READ LOCK;
mysql> SHOW MASTER STATUS;
+-----------------+----------+--------------+------------------+
| File            | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+-----------------+----------+--------------+------------------+
| mysql-bin.000002 |   528208 |              |                  |
+-----------------+----------+--------------+------------------+
1 row in set (0.01 sec)
mysql> UNLOCK TABLES;

本番稼働しているものの場合は、ここでバックアップをとってロックを解除したらよい。
今回はdatadirもかえたいので、サーバを落として作業する。

$ df -h /tmp
   #=> バックアップ用の空き容量を見ておく
$ ps ax | grep mysql
   #=> 生きてるプロセスを確認
$ /usr/local/mysql/support-files/mysql.server stop
$ ps ax | grep mysql
  #=> プロセスが停止していることを確認
$ mysqldump -u root -p  --all-databases > /tmp/mysql_`date +%Y%m%d%H%M%S`.dump

ディレクトリ、cnfファイルを準備する。
Masterは以下のようにする。

  • datadir => /usr/local/mysql/data
  • log => /var/mysql/logs
  • pid => /var/mysql/run/mysql.master.pid
  • socket => /var/mysql/socket/mysql.master.sock
  • port => 3306 (変更なし)
$ mkdir /var/mysql /var/mysql/data /var/mysql/logs /var/mysql/run /var/mysql/socket 
$ cp -R /usr/local/mysql/data /var/mysql/data
$ mv /var/mysql/datadir/error.log /var/mysql/logs
   #=> エラーログ名は環境ごとで名前違うかも
$ cp /etc/my.cnf /etc/my_master.cnf

my_master.cnf

datadirを変更すると”mysql.plugin doesn't exist”とおこられるので、plugin-dirも併せて変更しておく。エラーログ名やpid名は、起動しているホスト名が入っていたりするので、その辺りは運用にあわせる。

レプリケーションをする場合は、起動インスタンスを識別するためのserver-idと、同期に使用するバイナリログの設定をしておく必要がある。

[mysqld]
port    = 3306
plugin_dir= /usr/local/mysql/lib/plugin
socket    = /var/mysql/socket/mysql.sock
pid-file  = /var/mysql/run/mysql.master.pid
datadir   = /var/mysql/data
log-error = /var/mysql/logs/error.log

# for replication
server-id  = 1
log-bin=mysql-bin

# innodb
innodb_data_home_dir = /var/mysql/data
innodb_log_group_home_dir = /var/mysql/data

ここまで来たら、Masterを起動する。 起動するときは、mysqld_safeに--defaults-fileオプションをつけて、cnfファイルを指定した状態で起動する。 mysql.serverで起動するとデフォルトはmysqlユーザになっているが、mysqld_safeで起動するとログインユーザかオプションで指定したユーザになるので、うまく立ちあがらない場合は、dataディレクトリの権限とかを確認する。

$ mysqld_safe --defaults-file=/etc/my_master.cnf
$ ps ax | grep mysql

Masterに接続してみる。

$ mysql  -u root -p -S /var/mysql/socket/mysql.sock
mysql> show databases;
   #=>  ちゃんとDBがコピーできてるか見る

動いているのを確認したら、Master側でSlave用のDB参照ユーザを作る。 GRANTの権限も専用のREPLICATION SLAVEになっている。

  mysql> GRANT  REPLICATION SLAVE ON *.* TO repl@localhost IDENTIFIED BY 'password';
  Query OK, 0 rows affected (0.00 sec)
  mysql> select user, host  from user;
  +-----------+---------------+
  | user      | host          |
  +-----------+---------------+
  | root      | 127.0.0.1     |
  | root      | ::1           |
  | repl      | localhost     |
  | root      | localhost     |
  +-----------+---------------+
  4 rows in set (0.00 sec)

Master側の作業はこれで一通り。

Slaveの作業

ここまで来たらスレーブを作る。
事前にできる作業も多いので、データ同期までの作業は先にやっておいてもよいかもしれない。

まずはSlave用のディレクトリを作る。

$ mkdir /var/mysql_slave /var/mysql_slave/data
$ /usr/local/mysql/scripts/mysql_install_db --datadir=/var/mysql_slave/data --basedir=/usr/local/mysql
$ mkdir /var/mysql_slave/run /var/mysql_slave/socket /var/mysql_slave/logs
$ cp /etc/my.cnf /etc/my_slave.cnf

できたのでcnfファイルを準備する

my_slave.cnf

ポートとserver-idを変えて、各ディレクトリをslave用に指定する。

[mysqld]
port    = 3307
plugin_dir= /usr/local/mysql/lib/plugin
socket    = /var/mysql_slave/socketl/mysql.sock
pid-file  = /var/mysql_slave/run/mysql.slave.pid
datadir   = /var/mysql_slave/data
log-error = /var/mysql_slave/logs/error.log

# for replication
server-id  = 2
log-bin=mysql-bin

# innodb
innodb_data_home_dir = /var/mysql_slave/data
innodb_log_group_home_dir = /var/mysql_slave/data

あとは、Slaveを起動してデータを入れる。

$ mysqld_safe --defaults-file=/etc/my_slave.cnf
$ ps ax | grep mysql
$ mysql -u root  -p -S /var/mysql_slave/socket/mysql.sock < /tmp/mysql_datetime.dump
$ mysql  -u root -p -S /var/mysql/socket/mysql.sock
mysql> show databases;

ちゃんとデータ入っているところまで行ったら、Masterがどれかを教えて、バイナリの位置を指定して同期を開始する。

  mysql> CHANGE MASTER TO
          MASTER_HOST='localhost',
          MASTER_USER='repl',
          MASTER_PASSWORD='password',
          MASTER_LOG_FILE='mysql-bin.000002',
          MASTER_LOG_POS=528208;
  mysql> START SLAVE;

ちゃんと同期しているかどうかは、Slave_IO_Running,Slave_SQL_Runningが'yes'になっていることを確認する。

  mysql> SHOW SLAVE STATUS\G

苦労した設定で、Masterの更新がSlaveに同期されているのをみると興奮する。

エキスパートのためのMySQL[運用+管理]トラブルシューティングガイド

エキスパートのためのMySQL[運用+管理]トラブルシューティングガイド

Webエンジニアのための データベース技術[実践]入門 (Software Design plus)

Webエンジニアのための データベース技術[実践]入門 (Software Design plus)

RailsでPostgreSQLを使うまでの設定

久しぶりにRailsアプリをPostgreSQL使って作ろうとしたときに詰まったのでまとめておく。 普段はMBA使ってるけど、家のiMacでやったのでインストールから。

$ brew install postgresql
$ brew link postgresql
$ pg_ctl -D /usr/local/var/postgres start

起動までできたので、DB一覧を表示してみるとエラー

$ psql -l
psql: could not connect to server: Permission denied
    Is the server running locally and accepting
    connections on Unix domain socket "/var/pgsql_socket/.s.PGSQL.5432"?

エラーメッセージをググってみると、みんなぶつかるものらしく解決策がすぐ見つかった。

ありの日記 : MacBook AirにPostgreSQLをbrewでインストールした

Macにデフォルトでインストールされているものが邪魔しているらしい。which psqlをみてみると確かにbinの位置がそうだった。この問題、brewではあるあるだなぁ・・・。

ということで、修正バッチをもってきて実行する。

$ cd /usr/local/Cellar/postgresql
$ curl -o fixBrewLionPostgresql.sh http://nextmarvel.net/blog/downloads/fixBrewLionPostgres.sh
$ chmod 777 fixBrewLionPostgresql.sh
$ ./fixBrewLionPostgresql.sh

もう一度DB一覧を取得してみると無事うまく行く。

$ psql -l

ようやくRailsに。DBをPostgreSQLに指定して新規作成。

$ rails new posgreApp -D postgreSQL

できたのでDBを作ろうとするとまたエラーが出る。

$ cd postgreApp
$ rake db:create
   ・
   ・
   ・
could not connect to server: No such file or directory
        Is the server running locally and accepting
        connections on Unix domain socket "/var/pgsql_socket/.s.PGSQL.5432"?
   ・
   ・
   ・

こっちは、rakeが探しにいく/var/pgsql_socket以下にソケットが作られていない問題だった。
ググるStackOverFlowに解決方法のってた。
lsでみてみると、そもそも/var/pgsql_socketディレクトリが存在していない。
今作られているソケットを探して、rakeが見に行く所にリンクを貼ってあげる。

$ sudo find / -name .s.PGSQL.5432
$ mkdir -p /var/pgsql_socket
$ ln -s /private/tmp/.s.PGSQL.5432 /var/pgsql_socket

これでrakeを実行すると正常に実行できる。
今回はソケットファイルをリンクしたけど、postgresql.conf内でunix_socket_directoryで/var/pgsql_socketを指定して再起動しても動くはず。