2014年5月13日火曜日

PadrinoでSinatra::Flashを使う(Padrino::Flashでなく)

Webアプリを開発してますと、エラーメッセージを画面に出力するときに、次のリクエストの時に出力させたい、なんてことがよくあります。例えば何かの処理をした後に別のURLにリダイレクトさせて、その画面でエラーメッセージを出力させる場合がそうですね。

セッション変数に値を保持すれば、次回以降のリクエストに値を引き継げますが、もしこれがエラーメッセージだとすると、表示したらもう使わないので消さなければいけません。いちいち自分で消すのは面倒です。そこで、次のリクエストまでに限り値を保持するしくみがフレームワークやプラグインで用意されていたりします。このような仕組みは "Flash" と呼ばれています。(綴りは同じですがAdobeのFlashとは別物です。)

Sinatraの場合、公式ページにてRack:Flashを使うように書いてあります。しかしPadrinoでも使おうとしたらエラーになって使えませんでした(Gemfileに追加するだけでエラーになります。詳しい原因は追求してません)。

ということで、Padrinoの設定ファイルに記述があるSinatra:Flashを使うことにします。Sinatra:Flashは、セッション変数 session[:flash] にデータを保持しますが、直接セッション変数をアクセスせずに、flash[:key]を通じてアクセスします。ちなみに動作を見ていくとなかなか面白いです。まずは以下のコードと実行結果をみてください。

flash[:error] = "hoge"
p flash[:error]                   #=> nil
p flash.now[:error]               #=> nil
p flash.next[:error]              #=> "hoge"

flash.now[:error] = "fuga"
p flash[:error]                   #=> "fuga"
p flash.now[:error]               #=> "fuga"
p flash.next[:error]              #=> "hoge"

flash.next[:error] = "foo"
p flash[:error]                   #=> "fuga"
p flash.now[:error]               #=> "fuga"
p flash.next[:error]              #=> "foo"

入れたはずの値が入っていないように見えますが、実は以下のような仕組みです。flashはSinatra::Flashオブジェクトのインスタンスであり、Sinatra::Flashの[]= メソッドは@nextインスタンス変数に値を代入するようになっていて、[] メソッドは@nowインスタンス変数の値を出力します。そしてFlashオブジェクトのインスタンス変数@next, @nowには同名のメソッドでアクセスできるようになっています。

つまり代入は@nextへ、出力は@nowからってわけですね。よって一見上記は不思議に見えるようですがこれで正しい動作なのです。

でもって、アプリケーションのafterフックで現在の@nowの内容を@nextでまるごと置き換えてしまいます。これで次回リクエスト時には@nextの内容にアクセスされることになり、@nextへ記録した内容は次回リクエストまで限定で有効、ということになります。

ちなみに、Padrino:Flashというライブラリもあります。こちらはセッション変数 session[:_flash] にデータを保持します。使い方はSinatra:Flashと同じで、実際、Gemfileの記述をPadrino:Flashに変えただけでエラーなく動作しました。

ではデータの保存先以外に何が違うかというと、値を保存する際に、その値がシンボルである場合、自動的にI18n.traslateを通してくれること。そして付属するヘルパーメソッドのHTML出力形式が違うこと。

でも、今回は値の保存形式は独自に決めたかったし(エラーの種類ごとに更に内部にハッシュを持たせたかった)、その出力のためのヘルパーメソッドも自分で書くことにしたので、正直どちらでも良かったんですが、結局余計なことをしないシンプルなSinatra:Flashを採用したってわけです。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月10日土曜日

Safariで"戻る・進む"時のキャッシュを無効にする

Webアプリの開発時に、ブラウザの戻る・進むボタンを押した時にキャッシュを使われるのは時として都合が悪いことがあります。そういう時は、HTTPレスポンスヘッダを設定するのが一つの手段です。

ちなみにPHPですと、このキャッシュを無効化する機能はキャッシュリミッタとして言語に標準で実装されていたりします。さすがWebアプリに特化した言語ですよね。
http://www.php.net/manual/ja/function.session-cache-limiter.php

上記マニュアルによれば、該当するヘッダは

Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0

ですね。そこで、現在Rubyで開発しているシステムで同様のレスポンスヘッダを設定してみたところ、上手くいくと思ったらこれがSafariだと効きません・・・。

色々レスポンスヘッダをいじってみますが、結局ダメでした。ちなみに調べてみるとヘッダでもHTMLのメタタグでもダメで、Javascriptに頼るしかないようです。
http://stackoverflow.com/questions/11979156/mobile-safari-back-button

上記URLによると、次のようなスクリプトを使えばよいようです。

window.onpageshow = function(event) {
  if (event.persisted) {
    window.location.reload()
  }
};

URL中の説明にもありますが、jQuery経由では動作しないので生のJavascriptで書きます。これで Safari でも"戻る・進む"時にキャッシュではなくサーバから読むようになります。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月9日金曜日

Capybara で一時的に非同期処理の待ち時間を調整する

Ruby で開発している Web アプリのテストを強力に支援してくれる Capybara。デフォルトで非同期処理のテストにも対応してまして、特定の要素を見つけ出したり、条件に一致する要素の存在確認をしたりするテストにおいて、失敗した場合に一定時間(デフォルトでは2秒間)待ち、再度DOMを読み直してテストを再実行する機能が組み込まれております。

これは便利な機能ですが、しかし非同期処理を想定しないテストの時も、毎回レッドのたびに2秒待たされるのも無駄です。この非同期処理の待ち時間を変更する方法の1つは、using_wait_time メソッドを使うことです。

using_wait_time 1 do
  expect(page).to have_css '#hoge'
end

上記コードは非同期処理の待ち時間を1秒に設定してブロック内のテストを実行します。

あるいは、Capybara 2.1以降なら find メソッドにおいて :wait オプションが使えます。Capybara 2.2以降なら、matcher系メソッド(have_cssなど)でも :wait オプションが使えます。

expect(page).to have_css('#hoge', :wait => 1)

上記コードは先程載せた using_wait_time の使用例と同じく、非同期処理の待ち時間を1秒に設定してテストを実行できます。

ここで、Cucmuberとの組み合わせの例を示しておこうと思います。僕が実際使っているステップ定義の一部を紹介しましょう。

ならば(/^".*?\((.*?)\)" に "(.*?)" と表示(?:され|されていること)(\(非同期\))?( debug)?$/) do |t_css, t_text, t_async, debug|
  show_debug_info if debug
  target_node = page.find(t_css, :text => t_text, :visible => true, :wait => t_async ? async_time : 0)
  expect(target_node.text).to eq(t_text)
end

def async_time
  Capybara.default_wait_time
end

def show_debug_info
  puts page.response_headers
  puts page.status_code
  puts page.current_url
  puts page.html
end

上記のステップ定義において、末尾に (非同期) という文字列をつけると非同期処理の待ち時間をデフォルト(2秒)にし、付けない場合は0秒すなわち待たない設定としています。例えば、非同期処理を待たない場合はこんなふうになります。

ならば "ログイン名(#login-name)" に "admin" と表示されていること

ここで、もし非同期通信を待ちたければ、

ならば "ログイン名(#login-name)" に "admin" と表示されていること(非同期)

と書けばいいです。

ちなみに、このステップの実行直前にデバッグ情報を表示させたければ

ならば "ログイン名(#login-name)" に "admin" と表示されていること debug

と書けばできるようにしています。

正規表現によるステップ定義は高い柔軟性をもたせられるのが利点ですね。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月8日木曜日

Padrino で i18n (他言語化)対応

Padrinoは i18n ライブラリに対応しており、簡単な設定で使いはじめることができます。まずは、config/boot.rbにて以下の記述を追加します。

Padrino.before_load do
  I18n.enforce_available_locales = true
  I18n.default_locale = :ja
end

ちなみに I18n.enforce_available_locales を true にすると、その言語が利用可能ではない(翻訳ファイルが見つからない)時に例外を投げてくれるようになるらしいです。この設定はPadrino::Helpersモジュールによりデフォルトでtrueになっているのでわざわざ設定は不要な気もしますが、念のため明示的にtrueを指定してます(下記ソース参照)。
https://github.com/padrino/padrino-framework/blob/master/padrino-helpers/lib/padrino-helpers.rb

なお、 I18n.default_locale = :ja を  I18n.locale = :ja と書いている解説ページもちらほら見かけましたが、cucumberによるテストを実行した際に翻訳が効きませんでした。cucumberによるテストはマルチスレッドでして、どうやら、I18n.locale はスレッドローカルな設定であることが原因で  I18n.default_locale しか設定が効かないっぽいです。そこんとこの詳しいメカニズムまでは調べてませんが。まあ、I18n.default_locale のほうを設定しておけばいいと思います。

翻訳ファイルは、app/locale/ja.yml に置けばいいです。形式としてはYAMLで、

ja:
  login: ログイン
  top: トップ

みたいな感じです。

翻訳のためのメソッドは I18n.translate ですが、padrino が短縮で呼び出せるペルパーを用意してくれてます(Padrino::Helpers::TranslationHelpers)。
そのおかげで、

<%=  I18n.translate(:login) %>

と書かなくても

<%= t(:login) %>

と書くか、あるいはRails風に

<%=t :login %>

と書けば「ログイン」と出力してくれます。

ちなみに、 I18n.localize というのもあって、こちらはDate, DateTime,Timeのいずれかのオブジェクトを受け付け、日付や時刻を言語に対応した形式で表示してくれます。こちらも同様短縮形が用意されているので、

<%=l Time.now %>

と書くと

2014/05/07 21:01:00

などと表示してくれます。その他にも翻訳時のパラメータ対応など機能がありますが、また別の機会に書きたいと思います。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月3日土曜日

テスト駆動開発の自動テスト"以外"の部分

それにしても、テスト駆動開発を実際にやっていると、テスト駆動開発っていうのは自動化テストありきの話では全くないということに気づきます。つまり、自動化テストが全くないとしても、テスト駆動開発的な進め方・考え方というのは元からあったわけで、それが自動化テストという仕組みにぴったりマッチした、と捉えることもできるのですよ。

例えば、テスト駆動開発の基本的な流れは、
  1. テスト記述
  2. テスト実行(レッド)
  3. コード記述
  4. テスト実行(グリーン)
  5. リファクタリング
  6. テスト実行(グリーン)
  7. (1. に戻る)
と繰り返していきますよね。

でもこれ、例えばWebアプリ開発で言えば、自動化テストの代わりに、ブラウザ上で実際に手動で動かして確認することに置き換えると、実は昔からやっていたやり方になるんですよね。要するに、
  1. 実現したい動作を決める
  2. ブラウザで現在の動作を確認する(失敗)
  3. コード記述
  4. ブラウザで現在の動作を確認する(成功)
  5. リファクタリング
  6. ブラウザで現在の動作を確認する(成功)
  7. (1. に戻る)
という感じですね。リズムとしては、ブラウザによる動作確認が自動化テストに置き換わればそのままテスト駆動開発になってしまいます。ですので、テスト駆動開発を始めた時に、全く違和感なく実に自然に始めることができたんですよね。
多分この自動テスト"以外"の部分が以前からの開発の流れとぴったり重なると、テスト駆動開発を容易に採り入れることができ、恩恵を享受できると思います。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月2日金曜日

PadrinoでRack::Session::Poolを使う方法

先日、Rack::Session::PoolとRack::Session::Cookieの違いについて書きましたけれども、Padrinoで実際にRack::Session::Poolを使う設定をしてみたので、メモしておきます。

Padrinoでは、デフォルトではapp/app.rbに

enable :sessions

と記述があり、セッション管理が有効になっています。PadrinoのベースであるSinatraは上記の記述がある場合にRack::Session::Cookieをオプション無しで呼び出します。そのためここではCookieの有効期限などの細かい設定はできません。

細かい設定をしたい場合は、enable :sessions の代わりに Rack::Session::Cookie を直接使うように、Sinatraの公式ドキュメントにも書いてあります。だったら、同様にRack::Session::Cookie ではなくRack::Session::Poolを直接呼び出せば大丈夫のような気がします。

ところがPadrinoだと、プロジェクトジェネレータで生成されたアプリケーションのapp/app.rbにおいて、

disable :sessions
use Rack::Session::Pool

などとやろうとすると下記のようなエラーになってしまいます。ちなみにこれはRack::Session::Cookie を使おうとしても同様のエラーになります。

you need to set up a session middleware *before* Rack::Protection::AuthenticityToken (RuntimeError)

これは、Padrinoのデフォルトで Rack::Protection 関係のオプションが有効になっているためです。config/apps.rbの次のような記述が該当します。

Padrino.configure_apps do
  # enable :sessions
  set :session_secret, 'sessionseacretkeystring12345567890abcdef'
  set :protection, :except => :path_traversal
  set :protect_from_csrf, true
end

なので、一旦 Rack::Protection 関係のオプションを無効にし、Rack::Session::Poolを直接呼び出した後にRack::Protectionを直接呼び出します(Rack::Protection 関係のオプションを改めて有効にするとエラーになってしまいます)。app/app.rbのenable :sessionsをコメントアウトし、次の記述を追加します。disable :sessionsは不要かもしれませんが念のため入れてます。

set :protection, false
set :protect_from_csrf, false
disable :sessions
use Rack::Session::Pool, :key => 'rack.session', :path => '/', :expire_after => nil
use Rack::Protection
use Rack::Protection::AuthenticityToken, :authenticity_param => '_csrf_token'

これでエラーが消えました。セッションも、Cookieを確認すると、今までBase64でエンコードされた非常に長い文字列(おそらく暗号化したキー・バリューの組み合わせがセッションIDとともに入っている)だったのが、セッションIDのみらしき文字列に変化しています。上記コードでは有効期限を設定していませんが、指定すればCookieに反映されることは確認できました。なお、 Rack::Protection 関係がきちんど動作しているかどうかは未検証ですのでご注意ください。Rack::Protection::AuthenticityTokenも動作してるのを確認しました。

※2014/05/02 Rack::Protection::AuthenticityTokenの動作を確認したことを追記、コードの use Rack::Protection::AuthenticityToken にトークンのパラメータ名を指定する引数を追加

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月1日木曜日

さあ、IEを捨てよう

さて、IE(Internet Explorer)のVer6〜11という幅広いバージョンに影響のある脆弱性が見つかり、さらにXPはサポート終了につきマイクロソフトは対応する予定も当然ない、というニュースが流れてますね。

いい機会です。もういい加減にIEやめましょう。と言いますかまだIE使ってるなんて勘弁して下さい。ましてやIE6とか、もう化石かと突っ込みたくなります。

もちろん、脆弱性が修正されないまま放置されるPCが多く出現するという状況自体は憂慮すべきことなのかもしれませんが、これを切っ掛けに皆がIEに見切りをつけ、FirefoxやChromeなどの他のブラウザに乗り換えてくれるなら(特にXPユーザが)、Web系エンジニアにとっては大変喜ばしい状況になるという側面も出てくるる気がします。そうなってほしい。

それにしても、IE6の呪縛が本当の意味で終わるまでにこんなに時間がかかるとは・・・。僕の周囲ではかなり消えつつありますが、まだゴキブリ並みのしつこさで生き残っている場所はあるんでしょうねぇ。ほんと、早く消えて欲しいものです。

さあ、今こそIEを捨てる時。ChromeかFirefoxに乗り換えよう。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。