TempingでMixinするmoduleのテストをしてみる٩( ‘ω’ )و

RailsでモデルにMixinするモジュールをテストしたいとき、みなさんどうしてますか?

mock_model? stub_model? 自分のダミーテーブル作る? 各モデルでテストする?

どれも意外と面倒くさい

そんなときはTempingを使うと楽ちん

ActiveSupport::Concernのモジュールをテストしてみましょう

# app/modules/logical_delete_scopes.rb
module LogicalDeleteScopes
  extend ActiveSupport::Concern
  included do
    scope :without_deleted, -> { where(deleted_at: nil) }
    scope :only_deleted,    -> { where.not(deleted_at: nil) }
  end
end

ダミーのモデルを作る

# spec/support/dummy_model.rb
Temping.create :dummy_model do
  include LogicalDeleteScopes
  with_columns do |t|
    t.datetime :deleted_at
  end
end

テストコードを書く

# spec/modules/logical_delete_scopes_spec.rb
require 'rails_helper'
RSpec.describe LogicalDeleteScopes do
  let!(:enable_data)  { DummyModel.create(deleted_at: nil) }
  let!(:deleted_data) { DummyModel.create(deleted_at: DateTime.now) }

  example do
    expect(DummyModel.without_deleted).to match_array [ enable_data ]
    expect(DummyModel.only_deleted).to match_array [ deleted_data ]
  end
end

Tempingはtemporary tableを使ってるので物理テーブルにも残らない github.com

Happy Hacking٩( ‘ω’ )و

masterのBug Fixだけをreleaseブランチへマージする٩( ‘ω’ )و

GitHubにはrelease機能があります

Creating Releases - User Documentation

masterブランチにマージしてタグ付けてきたが、railsのブランチを見るとリリースブランチが存在します

f:id:murajun1978:20150417005931p:plain

いまさらですが、releaseブランチを作るGitLab Flowを導入しました ( ̄▽ ̄;)

そうするとmasterブランチの変更をreleaseブランチにマージしないとイケない時があります

例えば、Bug FixやSecurity Fixなどなど

では、masterブランチの特定commitだけmergeしてみよう

マージしたいcommitのidを確認する

$ git checkout master
$ git log --oneline
602b23d  bug fix / filter conditions
657c50a  remove duplicates
...

602b23dだけを1-0-stableにマージするときはgit cherry-pickを使います

$ git checkout 1-0-stable
$ git cherry-pick 602b23d

これで特定のcommitだけmergeできました

Happy Hacking٩( ‘ω’ )و

d(゚Д゚ )☆スペシャルサンクス☆( ゚Д゚)b

postd.cc

action_controllerの拡張gemをRSpecでTDD ٩( ‘ω’ )و

action_controllerの拡張gemを作成するときにrspecでテストする

ユーザ認証のサンプルgemをTDDで作っていきましょう

まずは、gemのひな形を作る

$ bundle gem user_auth

bundlerが1.9系なら以下のファイルが作成されてるはず

├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│   ├── console
│   └── setup
├── lib
│   ├── user_auth
│   │   └── version.rb
│   └── user_auth.rb
├── spec
│   ├── spec_helper.rb
│   └── user_auth_spec.rb
└── user_auth.gemspec

さて、ここから本題です

ゴールはこんな感じで、認証に失敗したら例外みたいなー

class ApplicationControlller < ActionController::Base
  before_action :authenticate_user!
end

TDDなのでテストから書いてくが、その前に開発時に必要なgemをインストール

# Gemfile
source 'https://rubygems.org'

gemspec

# この3つを追加
gem 'rails'
gem 'rspec-rails'
gem 'sqlite3'

ざっとテスト書いていきまーす

# spec/user_authenticate_spec.rb
require 'spec_helper'

RSpec.describe UserAuth::UserAuthenticate do
  describe ApplicationController, type: :controller do
    controller do
      def index
        render text: 'success!'
      end
    end

    context 'Authentication failure' do
      example do
        expect { get :index }.to raise_error
      end
    end
    
    context 'Authentication success' do
      let(:user) { User.create(email: 'user@example.com') }

      example do
        session[:user_id] = user.id
        get :index

        expect(response.body).to eq 'success!'
      end
    end
  end
end

テスト実行すると落ちます! uninitialized constant UserAuth::UserAuthenticate (NameError)

UserAuthenticateがないよーって言ってるので作りましょう

# lib/user_auth/user_authenticate.rb
module UserAuth
  module UserAuthenticate
  end
end

# spec/spec_helper.rb
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'user_auth'
# 追加
require 'user_auth/user_authenticate'

テストを実行! uninitialized constant ApplicationController (NameError)

ApplicationControllerがないって言ってるので、ダミーのコントローラーを作ります

# spec/support/action_controller.rb
require 'action_controller/base'

class ApplicationController < ActionController::Base
end

# spec/spec_helper.rb
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'user_auth'
# 追加
require 'support/action_controller'

テストを実行! 'method_missing': undefined method 'controller'

RSpecのcontrollerメソッドがないっていってる。

これはrspec-railsメソッドなので、spec_helperに追加

# spec/spec_helper.rb
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'user_auth'
require 'support/action_controller'
# 追加
require 'rspec/rails'

テストを実行! undefined method 'application' for Rails:Module

applicationがないって言ってるのでダミーのRails::Applicationを作ります

# spec/support/application.rb
require 'action_dispatch'

module Rails
  class App
    def env_config; {} end
    def routes
      return @routes if defined? @routes
      @routes = ActionDispatch::Routing::RouteSet.new
      @routes.draw do
        resources :posts
      end
      @routes
    end
  end

  def self.application
    @app ||= App.new
  end
end

# spec/spec_helper.rb
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'user_auth'
# action controllerより前に追加
require 'support/application'
require 'support/action_controller'
require 'rspec/rails'

テストを実行! uninitialized constant User

Userがないって言ってるのでダミーのUserモデルを作ります

# spec/support/active_record.rb
require 'active_record'

ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')

class User < ActiveRecord::Base; end

class CreateAllTables < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :email
    end
  end
end

# spec/spec_helper.rb
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'user_auth'
require 'user_auth/user_authenticate'
require 'support/application'
require 'support/action_controller'
require 'rspec/rails'
# 追加
require 'support/active_record'

RSpec.configure do |config|
  config.before :all do
    CreateAllTables.up unless ActiveRecord::Base.connection.table_exists? 'users'
  end

  config.before :each do
    User.delete_all
  end
end

テストを実行! For instance, 'include Rails.application.routes.url_helpers'.

そのまま、ダミーのApplicationControllerにincludeしてあげましょう

# spec/support/action_controller.rb
require 'action_controller'
class ApplicationController < ActionController::Base
  include Rails.application.routes.url_helpers
end

テストを実行! expected Exception but nothing was raised

キタ——(゚∀゚)——!!

ダミーのApplicationControllerにauthenticate_user!メソッドを追加しましょう

# spec/support/action_controller.rb
require 'action_controller'
class ApplicationController < ActionController::Base
  include Rails.application.routes.url_helpers

  before_action :authenticate_user!
end

テストを実行! undefined method 'authenticate_user!'

authenticate_user!メソッドを実装します

# lib/user_auth/user_authenticate.rb
module UserAuth
  module UserAuthenticate
    def authenticate_user!
    end
  end
end

# lib/railtie.rb
require 'rails/railtie'
require 'active_support'

module UserAuth
  class Railtie < Rails::Railtie
    ActiveSupport.on_load :action_controller do
      require 'user_auth/user_authenticate'
      send :include, UserAuth::UserAuthenticate
    end
  end
end

# lib/user_auth.rb
require "user_auth/version"
# 追加
require 'user_auth/railtie'
...

テストを実行! expected Exception but nothing was raised

authenticate_user!メソッドでユーザを取得する

# lib/user_auth/user_authenticate.rb
module UserAuth
  module UserAuthenticate
    def authenticate_user!
      User.find(session[:user_id])
    end
  end
end

テストを実行!

f:id:murajun1978:20150415030247p:plain

おーるぐりーんヾ( ̄∇ ̄=ノ

どうですか?

TDDなら次に何をするかが明確になりますよね?

テストを書いて、エラーがでたら原因を調べて解消していく

テストが失敗(エラーじゃないよ)したらロジックを書く!

そしてグリーンになればオッケーって感じです

サンプルコードは雑ですがね...

Happy Hacking٩( ‘ω’ )و

ActiveSupport::PerThreadRegistryでThread.currentより安全にグローバルな値を保持する╭( ・ㅂ・)و ̑̑

Railsでグローバルデータを保持したいときに、Thread.currentを使えば簡単ですよね

Thread.current[:current_user] = user
Thread.current[:current_user] #=> user

僕も気軽に使ってましたが、最近こんな記事を見つけました。

Better globals with a tiny ActiveSupport module - Weissblog

Thread.currentを使わない方が良い理由

  • キーがかぶってしまったらデータを上書きされるで!
  • 構造化されてないで!

ActiveSupport::PerThreadRegistryを使うと同じcurrent_userでもデータを担保できます

class RequestRegistry
  extend ActiveSupport::PerThreadRegistry
  attr_accessor :current_user
end

class PhoneNumberApiRegistry
  extend ActiveSupport::PerThreadRegistry
  attr_accessor :current_user
end

RequestRegistry.current_user = user1
RequestRegistry.current_user #=> user1

PhoneNumberApiRegistry.current_user = user2
PhoneNumberApiRegistry.current_user #=> user2

てな感じです

ここらを参考にすれば良いみたいです

rails/runtime_registry.rb at master · rails/rails · GitHub

rails/explain_registry.rb at master · rails/rails · GitHub

Happy Hacking٩( ‘ω’ )و

ActiveSupport::OrderedOptionsを使ってHashで.(ドット)を使えるようにする٩( ‘ω’ )و

もはやActiveSupportなしでは生きていけない僕

Hashをイイ感じに拡張してるActiveSupport::OrderedOptionsです

github.com

# activesupport/lib/active_support/ordered_options.rb
module ActiveSupport
  class OrderedOptions < Hash
    ...
  end
end

普通のHashだとこんな感じ

hash = {}
hash[:name] = 'murajun1978'

hash[:name] #=> 'murajun1978'

ActiveSupport::OrderedOptionsを使うとこうなる

hash = ActiveSupport::OrderedOptions.new
hash.name = 'murajun1978'

hash.name   #=> 'murajun1978'
hash[:name] #=> 'murajun1978'

.(ドット)でvalueを取得できるようになりましたーヾ( ̄∇ ̄=ノ

チェインしたいときはこんなかんじ

hash = ActiveSupport::OrderedOptions.new
hash.user =  ActiveSupport::OrderedOptions.new
hash.user.name = 'murajun1978'

hash.user.name #=> 'murajun1978'

既存のHashを使いたいときはActiveSupport:: InheritableOptionsを使う

hash = ActiveSupport:: InheritableOptions.new({name: 'murajun1978'})

hash.name #=> 'murajun1978'

I18nのconfigを定義するとこで使われてるねー

github.com

module I18n
  class Railtie < Rails::Railtie
    config.i18n = ActiveSupport::OrderedOptions.new
    config.i18n.railties_load_path = []
    config.i18n.load_path = []
    config.i18n.fallbacks = ActiveSupport::OrderedOptions.new

    ...
  end
end

次はThreadについて書くよてい(多分、明日の神戸.rb Meetup #15で)

Happy Hacking٩( ‘ω’ )و

Array#extract_options!で引数からオプションをひっこぬく٩( ‘ω’ )و

Railsでのvalidationを設定する時はこう書きますよね?

validates :name, :kana,
  presence: true

validatesメソッドに、symbolとhashが複数指定されています

オプション部分はHashですねー

引数とオプション(Hash)を分離したいときに大活躍するのがArray#extract_options!です

extract_options!のコードを見てみましょう

# activesupport/lib/active_support/core_ext/array/extract_options.rb
class Array
  def extract_options!
    if last.is_a?(Hash) && last.extractable_options?
      pop
    else
      {}
    end
  end
end

last.is_a?(Hash) && last.extractable_options?で配列の末尾のオブジェクトがHashかチェックしてます

HashだったらpopメソッドでHashを引っこ抜いてます

Hashでなかったら空のHashを返してます

メソッド!がついてるのは破壊的メソッドだからですね

Railsコンソールで確認してみましょう

args = [:name, :kana, {presence: true}]
options = args.extract_options!

args    #=> [:name, :kana]
options #=> {:presence=>true}

おおー、引数とoption(Hash)を分離できましたヾ( ̄∇ ̄=ノ

validatesのコードはこんなかんじ( ˘ω˘)

# activemodel/lib/active_model/validations/validates.rb
def validates(*attributes)
  defaults = attributes.extract_options!.dup
  [...]
end

extract_options!のコードを見ればわかりますが、Hashは末尾に指定しないとダメなので気を付けよう

[:name, {presence: true}, :kana].extract_options! #=> {}

Happy Hacking٩( ‘ω’ )و

Enumerable#partitionでブロックの評価が真と偽の要素に分ける٩( ‘ω’ )و

いままでブロックで評価して真のものはselectで、それ以外はrejectとか2回やってた...

でも、これ1回でできたらおしゃれですよねー

そんなときはEnumerable#partition

こんなかんじ( ˘ω˘)

array = ["cat", 1, 2, "dog", 0.1]

numbers, non_numbers = array.partition { |v| v.is_a?(Numeric) }

numbers     #=> [1, 2, 0.1]

non_numbers #=> ["cat", "dog"]

Happy Hacking٩( ‘ω’ )و