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
テストを実行!
おーるぐりーんヾ( ̄∇ ̄=ノ
どうですか?
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です
# 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を定義するとこで使われてるねー
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٩( ‘ω’ )و
Adequate Recordでキャッシュされないケースヘ(^o^)ノ
第65回 Ruby関西 勉強会に参加したよヘ(^o^)ノのつづき
スライドに、Adequate Recordでキャッシュされないケースを書いたけど、 ざっくりしすぎなのでまとめてみる( ˘ω˘)
find_byを例にactiverecordのコードを読みながら確認していく
コードはこんな感じ(コメントの*1
とかは説明しやすいのでつけてるだけ)
# activerecord/lib/active_record/core.rb def find_by(*args) # :nodoc: # *1 return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any? # *2 return super if default_scopes.any? hash = args.first # *3 return super if hash.values.any? { |v| v.nil? || Array === v || Hash === v } # We can't cache Post.find_by(author: david) ...yet # *4 return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) } ... end
*1
で判定していること
current_scopeがある
current_scope
でスコープがないことを確認してる
引数がHash以外
!(Hash === args.first)
でチェックしてる
こんなのはキャッシュしてくれない
Post.find_by('created_at < ?', 1.weeks.ago)
composed_ofが設定されている
reflect_on_all_aggregations.any?
でチェックしてる
設定されているだけでキャッシュしてくれない
*2
で判定してること
default_scopeが設定されている
設定されているだけでキャッシュしてくれない
*3
で判定していること
引数のハッシュ値(Value)がnil
, Array
,Hash
こんなのはキャッシュしない
Post.find_by(title: nil) Post.find_by(title: ['ruby', 'rails']) # Hashがくるケースが思い浮かばない...
*4
で判定していること
Hashのキーにモデルのカラム以外が指定されている
以上の条件に当てはまる場合に、super
すなわち本家のfind_by
をよんでます
なので、単一テーブル継承(STI)やPolymorphicは、設定されているだけではキャッシュの対象外とはなりません
ここがややこしそうだ。。。
間違えてたら教えてくださいねー
Happy Hacking٩( ‘ω’ )و
神戸.rb Meetup #13に参加したよヘ(^o^)ノ
神戸.rb Meetup #13に参加しましたー
毎回思うがホント勉強になる
僕の思いもみんなに伝えられたし良かった
今日はActiveSupport::StringInquirerについて調べた
結構こんなの書いちゃいがちですよね?
if Rails.env == 'production' ... end
それが、こう書ける
if Rails.env.production? ... end
これを使ってこんなの実装してみた
priority_typeってフィールドがあって、'low'や'high'なんかの文字列がセットされてるとする
class Task < ActiveRecord::Base def priority priority_type.inquiry end end
lowかどうかチェックしてみる
task.priority_type #=> "low" task.priority.low? #=> true task.priority.middle? #=> false
==で判定するよりグッと意図が伝わりやすくなってると思う(多分...
Happy Hacking٩( ‘ω’ )و