yo_waka's blog

418 I'm a teapot

ActiveRecordのマイグレーションでMySQLのunsignedな数値タイプを指定できるGemライブラリ作った

今の会社でRailsを使うようになって、いわゆるマイグレーションの仕組み超便利。

なんですが、MySQLを使っているのにidや数値にunsignedを指定できないのどうなんだろう。
他のDBはサポートしてないからいらないよねっていうのも分かるんですが、せっかくアダプタが分けられてるならサポートしてもいいんじゃないかな。
ということで、ActiveRecordの勉強がてらマイグレーションでintegerなカラムに"unsigned"を指定できるGemライブラリを作ってみました。

github/activerecord-mysql-unsigned

ついでに初めてのRubyGemsで公開もしてみた。
rubygems/activerecord-mysql-unsigned

ActiveRecord3.2以降と4で動作確認しています。

使い方はGemfileに書いて、マイグレーションファイルで「unsigned: true」をオプションに指定するだけ。

class CreateUsersTable < ActiveRecord::Migration
  def self.change
    create_table :users, force: true do |t|
      t.string  :name, null: false
      t.integer :age,  null: false, unsigned: true
    end
  end
end

既存のテーブルの主キーや数値カラムを置き換えるのが主目的なので、change_columnでも使えます。
このためにv3.2でauto_incrementをオプションで指定できるようにもしてあったり。

class ChangeColumnToUsersTable < ActiveRecord::Migration
  def self.change
    change_column :users, :id,  :integer, null: false, unsigned: true, auto_increment: true # 主キー
    change_column :users, :age, :integer, null: false, unsigned: false
  end
end

v3.2とv4.0の両方対応させるためにActiveRecordのソース読みましたが、中のクラス構造や挙動が結構変わってるんですねー。
v3.2だとunsignedな数値カラムにマイナス値を入れると0が保存される。v4.0だとActiveRecord::StatementInvalidエラーにしてくれる。
v4.0の方が圧倒的に見通しもいいしソースも綺麗。カラムのマイグレーションにもcollationやextraオプションを指定できたりいろいろ便利そうな機能を発見しました。
ウチのサービスも早く4.0にしたいなーっ。。。

RailsアプリのJavaScriptをkonachaを使ってCLI上でテストする

前回のでアプリケーションのテストがMiniTest::Specで実行できるようになったので、今度はJavaScriptユニットテストをコマンドラインから実行できるようにする。

慣れてるMochaを使ってテストが書けるkonachaと、konachaが使用するCapybaraのPhantomJSドライバであるpoltergeistを使う。

Gemfile

group :test do
  gem 'konacha'
  gem 'poltergeist'
end

konachaの設定はinitializerで。
ドライバをpoltergeistにして、テスト対象ディレクトリを"test/javascripts"に変えた(デフォルトは"spec/javascripts")。

config/initializers/konacha.rb

if defined?(Konacha)
  Konacha.configure do |config|
    require 'capybara/poltergeist'
    config.driver = :poltergeist
    config.spec_dir = "test/javascripts"
  end
end

mochaの設定はテストディレクトリ直下にspec_helper.jsを置いてその中に書くらしい。

test/javascripts/spec_helper.js

// set the Mocha test interface
// see http://visionmedia.github.com/mocha/#interfaces
mocha.ui('bdd');

// ignore the following globals during leak detection
mocha.globals(['util']);

// or, ignore all leaks
mocha.ignoreLeaks();

// set slow test timeout in ms
mocha.timeout(5);

テストディレクトリ以下にある、"hoge_test.js"あるいは"hoge_spec.js"というファイルがテストファイルとなる。 もちろんSprokectsを使ってRailsのasset pipelineを使ってテストしたいJSファイルを読み込むことができる。
precompile済みならapplication.jsを指定するくらいか。

//= require spec_helper
//= require application

あとはchaiアサーションを使ってテストを書いてkonacha:runすればphantomjsでテストが実行される!

RAILS_ENV=test bundle exec rake konacha:run

Rails4で書いたアプリをMiniTest::Specでテストする

Rails4からはActiveSupport::TestCaseがTest::UnitからMiniTest::Unit::TestCaseのサブクラスに変わっている。
MiniTestはSpecなDSLをサポートしているので、RSpecを入れずともBDDスタイルでテストが書けるようになる。
ということで、いろいろtest_helper.rbをゴニョってたらminitest-rails-specというズバリなGemを見つけたので(><)これを使う。

minitest-spec-railsでやってくれることは、ざっくりいうと、RailsActiveSupport::TestCaseにMinitest::Spec::DSLをextendして、ControllerとかHelperクラスをテスト対象クラスに追加して、beforeとかafterとかのDSLを使えるようにしてくれるだけというシンプルな作りになっている。

使えるDSLはチートシートにまとめてくれている人がいるけど、ソースコメントにも使い方が書いてあるので数も多くないしRDocか直接のぞいて見てみるのがよさげ。

environments/test.rbなどに以下を書くと、context(describeのエイリアス)やshould(itのエイリアス)も使えるようになる。

config.minitest_spec_rails.mini_shoulda = true

Gemfile

gem 'rails', '4.0.0.beta1'
group :test do
  gem 'minitest-spec-rails'
  gem 'factory_girl_rails'
end

Factoryはこんな感じで定義しておく

FactoryGirl.define :tester1, class: User do
  name "tester1"
  password "tester1tester1"
  email "tester1@example.com"
end

まずは普通にモデルをテスト

test/models/user_test.rb

require 'test_helper'

describe User do
  context '#attributes' do
    it '#name is required' do
      tester1 = Factory.create :tester1
      
      proc = Proc.new do
        tester1.update(name: "")
      end
      proc.must_raise ActiveRecord::RecordInvalid
    end
  end
end
$ bundle exec rake test:models
Run options: --seed 30682

# Running tests:

.

Finished tests in 1.190526s, 6.1302 tests/s, 3.2388 assertions/s.

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

よさそう。 続いてAPIコントローラのテスト。 登録ユーザーの一覧をJSONで返すAPIが「Api::UsersController」クラスのindexメソッドに定義されているのでテストする。

test/controllers/api/users_controller_test.rb

require 'test_helper'

# describeメソッドにテストするコントローラクラスをセットすることで、テストメソッド内で"get :index"など書くと、
# ActionController::TestRequestリクエストを作って、
# コントローラのアクションを実行して、
# 結果をActionController::TestResponseで判定できる
describe Api::UsersController do
  before {
    20.times do |idx|
      User.create(
        name: "user#{idx}",
        password: "user#{idx}password",
        email: "user#{idx}@example.com"
      )
    end
  }

  it "#GET /api/users - とりあえず全件返す" do
    get :index
    res = JSON.parse(response.body)
    res["users"].size.must_equal 20
  end
end

ログインしないと叩けないAPIはテスト用にログインメソッド作ってやると書きやすい気がする。

test_helper.rbに追加

class ActionController::TestCase
  include ApplicationHelper

  def start_session_test(user_id)
    @controller.reset_session
    @controller.session[:login_id] = user_id
  end
end

これをリクエストを発行する前に実行すればいい

require 'test_helper'

describe Api::UsersController do
  before {
    @tester1 = Factory.create :tester1
  }

  it "#GET /api/my - ログインユーザー情報を返す" do
    start_session_test @tester1.id # tester1でログイン

    get :my
    res = JSON.parse(response.body)
    res["user"]["name"].must_equal "tester1"
  end
end

ActiveRecordで日付の範囲指定

ActiveSupportのコア拡張を使うとめちゃ綺麗に書けて感動した。

  • 今日
from = Time.now.at_beginning_of_day
to   = from + 1.day

items = Item.where(created_at: from...to).order(created_at: :desc)
  • 今月
from = Time.now.at_beginning_of_month
to   = from + 1.month

items = Item.where(created_at: from...to).order(created_at: :desc)

RailsのセッションをJSONで

Railsのセッション情報はこちらの記事にあるようにRubyのMarshalでシリアライズされ、Base64エンコードされたものがクッキーなどに保存される。

ただ、Ruby以外では復元が難しいため、Node.jsなど別言語で書かれたサーバーアプリケーションとセッションを共有しづらい。
なので、こちらの記事を参考にJSONで保存できるようにしてみた(というかほとんどそのまま)。

セッション情報の保存先にはredis-storeライブラリを使ってRedisに保存する。
redis-storeは、RubyGemsにあがっているものだとRails4で動かない(Rails3でもredisライブラリのバージョンが新しいと動かない)。
GitHubを見てみたら、forkされたものが最近のRackのバージョンアップ含めてRails4対応していたので、それを使う。

Gemfile

gem 'redis'
gem 'redis-store', github: 'bricker/redis-store'
gem 'redis-rails', github: 'bricker/redis-store'

config/initializer/session_store.rb

require 'action_dispatch/middleware/session/redis_store'

#
# デフォルトだとRubyのMarshalでセッションオブジェクトを保存するため、Node.js側で扱えない
# JSONで保存するためにモンキーパッチをあてる
#
module ActionDispatch
  module Session
    class RedisStore
      # override
      def get_session(env, sid)
        with_lock(env, [nil, {}]) do
          unless sid and session = @pool.get(sid)
            sid, session = generate_sid, {}
            unless /^OK/ =~ @pool.set(sid, JSON.generate(session))
              raise "Session collision on '#{sid.inspect}'"
            end
          end
          session = JSON.parse(session) unless session == {}
          if session.has_key?('flash')
            session['flash'] = ActionDispatch::Flash::FlashHash.new.update(Hash[*session['flash']])
          end
          [sid, session]
        end
      end

      # override
      def set_session(env, session_id, new_session, options)
        with_lock(env, false) do
          jsonize_session = JSON.generate(new_session)
          @pool.set session_id, jsonize_session, options
          session_id
        end
      end
    end
  end
end

# セッションの生存期間は1週間
Sample::Application.config.session_store :redis_store, servers: {
  host: "localhost",
  port: 6379,
  namespace: "_sessions",
  db: 10,
  expire_in: 60 * 60 * 24 * 7
}

redis-store、もう1年くらい更新されてないしissuesもpull reqも溜まってるけど作者の人やる気もうないのかな。。。