Integration testの中でファイルアップロードができない
現在のRails(1.2.4および2.0 Preview)にはIntegration testの中でファイルアップロードができないというバグがあるようです。実際にやってみると、fixture_file_uploadを使ってもActionController::TestUploadedFileのオブジェクトにならず、ActionController::TestUploadedFileをinspectした文字列がコントローラに渡されます。
この問題を回避するためのコードが#4635 (unable to test file uploads with integration testing framework) - Rails Tracにあります。このticketに添付されているファイルはパッチではなく、ActionController::Integration::Session#multipart_postというメソッドを定義する.rbファイルです。つまりこのファイルをrequireし、fixture_file_uploadを使うpostには、代わりにmultipart_postを使えば良いわけです。comment:26に書かれている通りです。
Integration testなんて書かないという人も多いと思いますが、Integration testでしか検出できないバグもあるわけで、そういうバグを見つけてしまうと書かずにはいられませんね。
テストの中でnamed routesを使う
テストコードにおいて、RESTfulなURLへのリンクやformのactionがあることをassertする場合、どう書きますか?例えば下のようなリンクが表示されることを確認したい場合です。
<a href="/products/1;edit">
下のようにパスをハードコードしてしまったら、
assert_select "a[href = /products/1;edit]"
Rails 2.0でパスのスキーマが変わった時(/products/1;edit
が/products/1/edit
になった時)テストコードを書き直さなければなりません。
コントローラで使える名前付きルート(named routes, resource routes)はprotectedなので、次のように書くと
assert_select "a[href = #{@controller.edit_product_path(1)}]"
エラーになります。
url_for
はpublicメソッドなので下のように書くことができますが((link_to
で生成されるのはパスのみなので、url_for
で同じパスを作るには:only_path
オプションが必要です。))
assert_select "a[href = #{@controller.url_for(:action => 'edit', :id => 1, :only_path => true)}]"
resource routeがあるものをurl_for
で書くのは何だかカッコ悪いです。
テストの間だけnamed routesをpublicに
テストコードの中で次のように書いてやると、上に出てきたedit_product_path
を使えるようになります。
class ProductsController public :edit_product_path end
例えばこの例だと、products_controller_test.rbのclass ProductsControllerTest < Test::Unit::TestCase
などと書いてある行の上にでも書けば十分でしょう。
protectedのようなアクセス制限は、本来プログラマーにオブジェクト指向に従ったコーディングを促し、ソフトウェアの品質を高めるために存在するので、むやみに解除すべきではありませんが、テストコードの中でのみ使う限り問題ないと思われます。
この方法の問題点は、数あるnamed routesを使う分だけ列挙する必要があることです。個人的な感覚では5,6個までなら我慢できますが、それ以上になったらまとめてpublicにできるヘルパーを書きたくなります。
皆さんはこういうテストをどう書いてますか???
Railsの予約語はこんなにたくさん
今日はカラム名に(つまりモデルのattributeに)使ってはいけない単語を使ってしまい、時間をロスしてしまったのでメモしておきます。
一般にプログラミング言語には識別子(つまり変数などの名前)として使ってはいけない単語、”予約語”がありますね。英語では単にkeywordと言いますが。Ruby自体の予約語ももちろんありますが、Railsを使う場合にはRailsの予約語*1も意識する必要があるわけです。
調べてみたら本家のWikiにReservedWords in Ruby on Railsというページがありました。また、このページを元に完全な?予約語リストを作ったエントリもありました。
見てみると、つい使ってしまいそうな単語がちらほらありますね。例えば「Task」とかいうモデルはいかにもありそう。テーブルのカラムに使えないのは「connection」「format」「key」「session」「template」など。SQLの予約語も考えると「catalog」とか「group」も使えなくなるのか…。
これら予約語との衝突を避ける方法として挙げられているのが、適当なprefixを付ける方法です。たとえばプロジェクトの頭文字をつけるとか。まあ、無難ですね。
render :partialのcollectionとしてHashのArrayを渡せるか?
タイトルを分かりやすく書くと、例えば
<%= render :partial => "foo/foo", :collection => @foox %>
のとき、@foox
としてHashのArrayを渡せるか?つまり
@foox = [ {:a => "moo", :b => "baw"}, {:a => "coo", :b => "bah"} ]
を渡したら、{:a => "moo", :b => "baw"}
と{:a => "coo", :b => "bah"}
を使ってfoo/_foo.rhtml
がrenderされるか?という問題です。
答えは”されません”。どうなるかと言うと、Hashオブジェクトの代わりにnil
が渡され、nil.[]
がNoMethodErrorになります。
なぜか。render :partial => "foo/foo", :collection => @foox
はActionView::Partials#render_partial_collectionでrenderされますが、このメソッドはcollectionのメンバ毎にActionView::Partials#render_partialを呼び出します。このときメンバがHashだと(問題のケース)、render_partialはそのHashをlocalsだと思ってしまい、代わりにpartial名のついたインスタンス変数(上の例だと@foo
)を探しに行ってしまうからです。@foo
は用意していないのでnil
になります。
どうしてもHashのArrayを使いたい場合は、次のようにpartial名のキーを持ったHashで包んでやります。
@foox = [ {:foo => {:a => "moo", :b => "baw"}}, {:foo => {:a => "coo", :b => "bah"}} ]
なぜかというと、render_partialは最終的に@foo
をlocalsにpushしてrenderを実行するので、その通りにpushされたようなHashを作っておいてやればよい、ということです。
しかし普通はそんな変なハックはせずに、Hashではないオブジェクトを渡すようにするべきです。当然ですが。
レヴュー: MasterView - Part 3 MasterViewの欠点
MasterViewは素晴らしいプラグインなのですが、イマイチ広まらない原因と思われることを挙げてみます。
テストへの対応が不十分
functionalテストの中には当然テンプレートをレンダリングするものもありますが、そういうテストを実行したとき、テンプレートがMasterViewのXHTMLだとActionController::MissingTemplateというエラーになります。このエラーを回避するには、MasterView付属のrakeタスクを使って、XHTMLファイルからERBのテンプレートを生成する必要があります。テンプレートが多くなってくるとこのタスクに時間がかかります(全てのXHTMLファイルをパースするため)。これにはちょっとイラつきます。
mv:rebuild_all
がテンプレートを破壊する
前回のMasterViewにおけるテンプレートの再利用のしくみの中で、mv:rebuild_all
でテンプレートをコピーすることを説明しました。しかし、そのコピー処理が未熟で、様々な破壊をもたらします。たとえば:
- DOCTYPE宣言が消える
- 属性値を囲んでいたシングルクォートがダブルクォートになる(値の中にダブルクォートがあったら一巻の終わり)
mv:import_render
で指定したコレクションの値が保持されない。mv:gen_partial
で指定したコレクションに置き換わってしまう
最後の項目がわかりにくいと思うので説明します。パーシャルを使ってコレクションをレンダリングする場合ですが、次のような定義になっていたとします。
<li mv:gen_partial=":partial => 'articles/article', :collection => @foo_articles"> <span mv:content="article.name">Article Name</span> </li>
これを別の場所で次のように使うとします。
<li mv:import_render=":partial => 'articles/article', :collection => @bar_articles"> </li>
上では@foo_articlesを使い、下では@bar_articlesを使っています。
これでmv:rebuild_all
すれば、<span>の部分がコピーされて下のようになる気がしますが
<li mv:import_render=":partial => 'articles/article', :collection => @bar_articles"> <span mv:content="article.name">Article Name</span> </li>
実際には@bar_articles
ではなく@foo_articles
になってしまいます。
<li mv:import_render=":partial => 'articles/article', :collection => @foo_articles"> <span mv:content="article.name">Article Name</span> </li>
このように完全にテンプレートが破壊されてしまうため、mv:rebuild_all
は使用できません。
gettextとの相性が悪い
ご存知の通り、gettextで置換したい文字列は_()
で囲みますが、この_()
はRubyで評価される必要がありますよね?ERBでは評価して欲しいものをどこでも<%= %>
で囲めば済みますが、XHTMLだと評価してもらえる場所は限られます。例えば
<p><%= _('Hello') %></p>
は
<p mv:content="_('Hello')">Hello</p>
と書かなければなりません。実は<%= %>
と互換の{{{= }}}
という記法があるのですが、
<p>{{{= _('Hello') }}}</p>
と書けばブラウザには
{{{= _('Hello') }}}
と表示されるので、WYSIWYGではなくなってしまいます。
MasterViewを使う意味はあるのか?
以上のように優れた機能を持ちながら完成度が”うーん”なMasterViewですが、使う価値はあるのでしょうか。私は使っていますし、これからも使うつもりです。理由はといえば、”ブラウザで見られるテンプレートが好きだから”としか言いようがありません。3エントリも使ってしょうもないオチですいません。気が向いたらパッチを書きます。
レヴュー: MasterView - Part 2 MasterViewの仕組み
Railsのテンプレートはレイアウト、コンテンツ、パーシャルに分類されます。ERBを使うとこれらは別のファイルになるのですが、なんとMasterViewでは一つのXHTMLファイルです。そしてそのファイルは、レンダリング結果のサンプルとしてブラウザで表示できます。さらに、そのファイルをWYSIWYGなHTMLエディタで編集しても、テンプレートとして壊れません。
MasterViewはどのようにしてそんな芸当を実現しているのでしょうか。MasterViewは独自のディレクティブをHTMLエレメントの属性として埋め込みます。例えば見出しを動的に生成するには次のように書きます。
<h1 mv:content="@title">Sample Title</h1>
上の見出しはそのままブラウザで表示すれば「Sample Title」ですが、レンダリングすると@title
の値が入るというわけです。
このようなディレクティブをXHTMLエレメントに加えることで、MasterViewはXHTMLファイルを複数のテンプレートに分割します。一般的には、最も外側の要素であるにレイアウトのテンプレート名を与えます。これは
<html mv:generate="layouts/foo.rhtml"> </html>
のように書きます(便宜上、他の属性は省いています)。このように書くと、<html></html>内の内容がlayouts/foo.rhtml
というテンプレートになります。
そして<html></html>の中に別のテンプレート名を指定された子孫要素があれば、その要素はレイアウトテンプレートから外され、新たなテンプレートとして認識されます(←ここがミソ)。
<html mv:generate="layouts/foo.rhtml"> <head><title>タイトル</title></head> <body> <div mv:replace="@content_for_layout"> <div mv:generate="foo/index.rhtml"> メインコンテンツ </div> </div> </body> </html>
(ちょっと端折りましたが、<div>mv:replace="@content_for_layout"</div>
は<%= @content_for_layout %>
になります。)上の内容をMasterViewでパースすると2つのテンプレートができるわけです。このパースは自動的に行われます。
パーシャルの切り出し方もほぼ同じです。ただしディレクティブにはmv:gen_partial
を使います。
<html mv:generate="layouts/foo.rhtml"> <head><title>タイトル</title></head> <body> <div mv:replace="@content_for_layout"> <div mv:generate="foo/index.rhtml"> メインコンテンツ <p mv:gen_partial=":partial => 'foo/message'"> パーシャル </p> </div> </div> </body> </html>
上のコードからはfoo/_message.rhtml
が生成されるというわけです。ここまでの内容はMasterViewのサイトで図で説明されています。
テンプレートの再利用
作ったテンプレートは再利用しなければ意味がありません。ERBではテンプレートの再利用はレンダリング時にのみ発生しますが、MasterViewではテンプレートの段階で再利用できる必要があります。例えば一つのXHTMLファイルでレイアウトを定義したら、そのレイアウトを利用する別のファイルをブラウザで表示した時、指定したレイアウトで表示される必要があります。
そのためにMasterViewには、指定したテンプレートを別のXHTMLファイルにコピーするrakeのタスクが用意されています。コピーするレイアウトはmv:import
で指定します。例えば上のレイアウトlayouts/foo.rhtml
をfoo/show.rhtml
で再利用するには次のように書きます。
<html mv:import="layouts/foo.rhtml"> <div mv:replace="@content_for_layout"> <div mv:generate="foo/show.rhtml"> showする </div> </div> </html>
ユーザは<head>や<title>や<body>を手作業でコピーする必要はありません。上のようにコピーしたいテンプレートの名前だけをmv:import
で指定して、rake mv:rebuild_allを実行すると、レイアウトがコピーされて下のようになります。
<html mv:import="layouts/foo.rhtml"> <head><title>タイトル</title></head> <body> <div mv:replace="@content_for_layout"> <div mv:generate="foo/show.rhtml"> showする </div> </div> </body> </html>
同様に、パーシャルを再利用するにはmv:import_render
を使います。
<html mv:import="layouts/foo.rhtml"> <head><title>タイトル</title></head> <body> <div mv:replace="@content_for_layout"> <div mv:generate="foo/show.rhtml"> showする <p mv:import_render=":partial => 'foo/message'"> </p> </div> </div> </body> </html>
以上がMasterViewのキモとなる、テンプレートに関するディレクティブです。
このようにレンダリング前からテンプレートを再利用可能にするMasterViewのrakeですが、実際には致命的な欠陥があります。次回はその点を含め、 MasterViewの欠点を遠慮なく挙げていきます。
レヴュー: MasterView - Part 1 MasterViewの長所
RailsのデフォルトのテンプレートエンジンはERBですが、レンダリング結果を見るには実際にレンダリングしてみる必要があります(つまりテンプレートのままではレイアウトなどのデザインを確認できません)。
MasterViewはテンプレート記述言語にXHTMLを拡張したXMLを用いることでブラウザで表示可能にした(つまりWYSIWYG)テンプレートプラグインです。
今回自分のプロジェクトにMasterViewを採用した経験から、3回に分けてレヴューを書いてみたいと思います。
MasterViewの最大の長所、それは表現力のある仕様書を書けること
前述の通り、MasterViewはWYSIWYGタイプのテンプレートエンジンです。MasterViewの詳しい仕組みは次回説明することにして、WYSIWYGの価値について感じたことを書いてみます。
”テンプレートをブラウザで表示できる”。単にそう聞くと、それほど魅力は感じないかもしれません。すぐに思いつくメリットは、”ERBの分からないデザイナーさんとも作業しやすい”などでしょうか。しかし現実的にデザイナーさんはテンプレートの簡単な修正くらいはできるべきだし、そもそも一人で開発する人には関係ないし。”別に、最初から最後までERBでいいよ”という人が大半ではないでしょうか。
ところが実際にXHTMLでテンプレートを書いてみると、想像していなかったメリットがあることに気づきます。それはテンプレートという仕様書の書きやすさ、使いやすさです。
テンプレートは仕様書
テンプレートを「仕様書」と書きましたが、読んで字のごとく、テンプレートは仕様書だと考えられます。プログラミングはコードの一行まで仕様書だという考え方がありますが、同様に、コードのコメント、テストコード、テンプレート、データベースのスキーマも全て仕様書です。
コード/仕様書を書くというのは、頭の中にあるイメージを文書に落とし込んでいく作業です(それ以外の要素もありますが)。イメージを最も良く表現する単語を紙に(あるいはファイルに)書き込んだ瞬間、頭の中に新たな空間がうまれ、それが瞬時に次のイメージで埋められる。そんな経験をしていませんか?この次々と浮かんでくるイメージが開発の効率やコードの質を決めることになります。
だとしたらより良質なイメージを作り出したい。イメージの質をコントロールしたいと思うでしょう。そういう要求があるなら、仕様書は表現豊かであるべきです。紙やファイルに落としたイメージが脳に刺激を与え、次のイメージを作り出す。ブラウザで表示できる視覚的な仕様書は脳に良質な刺激を与えるでしょう(多分)。
視覚的な仕様書は、書くときだけでなく使うときもメリットをもたらします。テンプレートを見ながらコントローラを書く習慣はありませんか?Getting Realによれば「Interface First」ですから、ControllerよりもViewが先にできていても不思議ではありません。視覚的なテンプレートは直感的に次の作業を教えてくれます。
Amrita2との比較
ここまで書いてきたことはMasterViewに限らず、WYSIWYGテンプレート全てに共通の長所です。Rails用のWYSIWYG型テンプレートとしては、MasterViewより以前からAmrita2がありました。ではAmrita2との違いは何でしょうか?
一番の違いは、コミュニティだと思います。MasterViewの開発者Jeff Barczewskiはメーリングリストでまめに利用者の質問に答えており、MasterViewを採用する際に安心感を与えてくれます。
他の違いとしては、Amrita2だとViewに属するコードがControllerに入ってしまうとか。まあこの辺は回避策もあるでしょうし、MasterViewのテンプレートに入ったRubyコードが読みやすいかと聞かれるとはっきり言って疑問なので、どっちもどっちだと思います。
次回はMasterViewのしくみを解説します。