2010/02/07

appengine-jrubyとSinatraとRSpec

節操なくいろいろなものを試してみているこの頃。今日はSinatra。

AppEngineでいろいろなものを動かしてみてはいるんだけど、どのフレームワークもAppEngineへの対応するのがまだまだ試行錯誤的な感じでチュートリアルで書いてあることがすぐに陳腐化してしまっている気がする。

特にフルスタックな重量級フレームワークを動かそうとすると、大体がモデルからの自動生成がフレームワークの便利さの主たる部分になっているのでAppEngineを利用するとその旨味を活かしきれない。逆に、普段の開発では気にしないライブラリのロードやスレッド関連がAppEngineの制約にあわず、うまく動かない原因になったり調べるにも追いかけるのが大変だったりとめんどくさい。

AppEngineの勉強という意味でも最初は薄いフレームワークから始めるのがいいかなぁと思う今日この頃です。JDOとLowlevelAPIみたいなもんですね。

どのフレームワークもテストまわりがどうもうまく動かせたことがないのと、RSpec Bookを試してみたかったのでAppEngineでRSpecを動かして、TinyDSのサンプルをテストするのが目標。appengine-jrubyの時点で裏でゴニョゴニョしてて良くわからないのだけど。

環境はUbuntu 9.10。以下必要なもの。括弧内はうちの環境

  • ruby(MRI 1.8.7-p249)
  • rubygems(1.3.5)
  • jruby(1.4.0)
  • AppEngine SDK Java (1.3.0)

以下手順

  1. MRIでappengine-jrubyのgemをいれる。
  2. テンプレートを作る

    appcfg.rb generate_app shout

  3. Gemfileで必要なGemを設定

    # Critical default settings: disable_system_gems disable_rubygems bundle_path ".gems/bundler_gems" # List gems to bundle here: gem "appengine-rack" gem "sinatra" gem "tiny_ds"

  4. アプリを作る。shout.rb

    require 'sinatra' require 'tiny_ds' class Shout < TinyDS::Base property :message, :text property :created_at, :time property :updated_at, :time end helpers do include Rack::Utils alias_method :h, :escape_html end get '/' do @shouts = Shout.query.sort(:created_at, :desc).all erb :index end post '/' do shout = Shout.create(:message => params[:message]) redirect '/' end

    views/index.erb

    <html> <head> <title>Shoutout!</title> </head> <body style="font-family: sans-serif;"> <h1>Shoutout!</h1> <form method=post> <textarea name="message" rows="3"></textarea> <input type=submit value=Shout> </form> <% @shouts.each do |shout| %> <p>Someone wrote, <q><%=h shout.message %></q></p> <% end %> <div style="position: absolute; bottom: 20px; right: 20px;"> <img src="/images/appengine.gif"></div> </body> </html>

  5. config.ruを設定する。

    require 'appengine-rack' AppEngine::Rack.configure_app( :application => "shout", :precompilation_enabled => true, :version => "1") require 'shout' run Sinatra::Application

  6. 動作確認
    以下を実行してブラウザでアクセスしてみる。

    dev_appserver.rb .

  7. Spec用の前準備

    AppEnigneのローカル環境で動かすためには通常appcfg.rb run -S irbの様にappcfg.rbを通じて呼び出すのだけど、AppEngine上でRubyGemsが使えないので、使用するGemはBundlerという仕組みを通してひとつのJarに固められたものを使用するようになる。で、appcfg.rbを通すとRubyGemsが無効化されるようで(たぶんGemfileのdisable_rubygems)、appcfg.rb run -S specとかやってもundefined method `bin_path' for Gem:Moduleとかいわれて動かない。ここで、JRubyから直接動かせと書かれているのでその準備が必要。

    1. jruby側に必要なGemを入れる。
      Gemsファイルに追加したものとappengine-api、rspecが必要。

      jruby -S gem install appengine-api rspec sinatra tiny_ds rack-test

    2. specディレクトリを作る。
      mkdir spec
    3. ローカル環境用スクリプトを作る。
      基本はGAE/Jと同じくローカル環境が必要。ここを参照させていただき、最近のGAEに合わせてgetAttributesを追加。
      spec/local_gae.rb

      class LocalGAE require "java" import "com.google.apphosting.api.ApiProxy" import "com.google.appengine.tools.development.ApiProxyLocalImpl" import "com.google.appengine.api.datastore.dev.LocalDatastoreService" class GaeEnvironment include ApiProxy::Environment def getAppId; "JRubyLocal" end def getVersionId; "1.0" end def setDefaultNamespace(s); end def getRequestNamespace; nil end def getDefaultNamespace; nil end def getAuthDomain; nil end def isLoggedIn; false end def getEmail; nil end def isAdmin; false end def getAttributes; {} end end class EmptyApiProxyLocalImpl < ApiProxyLocalImpl end def self.setup(profile_dir=nil) profile_dir ||= "./_tmp_gae_profile" #puts "setup" ApiProxy.setEnvironmentForCurrentThread(GaeEnvironment.new) localImpl = EmptyApiProxyLocalImpl.new( java.io.File.new(profile_dir) ) ApiProxy.setDelegate(localImpl) proxy = ApiProxy.getDelegate proxy.setProperty(LocalDatastoreService::NO_STORAGE_PROPERTY, "true") end def self.teardown proxy = ApiProxy.getDelegate datastoreService = proxy.getService("datastore_v3") datastoreService.clearProfiles ApiProxy.setDelegate(nil) ApiProxy.setEnvironmentForCurrentThread(nil) #puts "teardown" end def self.run(profile_dir=nil) setup(profile_dir) yield(ApiProxy.getDelegate) teardown end end if __FILE__ == $0 LocalGAE.run do |proxy| p proxy end exit end

  8. specを作る。spec/shout_spec.rb

    #-*- coding: utf-8 -*- require 'local_gae' require 'shout' require 'spec' require 'rack/test' set :environment, :test describe 'The Shout App' do include Rack::Test::Methods def app Sinatra::Application end before do LocalGAE.setup end it "最初はShoutout!と表示されメッセージが登録されていないこと" do get '/' last_response.should be_ok last_response.body.should =~ /Shoutout!/ last_response.body.should_not =~ /Someone/ end it "Helloとポストするとそのメッセージが表示されること" do post '/', {:message => "Hello"} follow_redirect! last_response.should be_ok last_response.body.should =~ /Hello/ end after do LocalGAE.teardown end end

  9. 環境変数の設定。
    google-appengineのjarにパスが通っていないと動かないのでCLASSPATHを設定する。

    export CLASSPATH=$GAEJ/lib/impl/appengine-api-stubs.jar:$GAEJ/lib/impl/appengine-local-runtime.jar:$GAEJ/lib/impl/appengine-api.jar:$GAEJ/lib/impl/appengine-api-labs.jar

  10. 実行

    jruby -S spec spec/shout_spec.rb

うちの環境だとなぜかspec動かした後プロンプトに戻ってこないのでCtrl-Cで強制終了する必要があるのが残念。