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も意識する必要があるわけです。

調べてみたら本家のWikiReservedWords in Ruby on Railsというページがありました。また、このページを元に完全な?予約語リストを作ったエントリもありました。

見てみると、つい使ってしまいそうな単語がちらほらありますね。例えば「Task」とかいうモデルはいかにもありそう。テーブルのカラムに使えないのは「connection」「format」「key」「session」「template」など。SQL予約語も考えると「catalog」とか「group」も使えなくなるのか…。

これら予約語との衝突を避ける方法として挙げられているのが、適当なprefixを付ける方法です。たとえばプロジェクトの頭文字をつけるとか。まあ、無難ですね。

*1:構文解析エンジンにひっかかるわけではないので、そういう意味での(狭義の)「予約語」ではないが、”フレームワークの作り上使えない”ということで広義の予約語と言って問題ないと思う

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 => @fooxActionView::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.rhtmlfoo/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のしくみを解説します。