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のブランチを見るとリリースブランチが存在します
いまさらですが、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
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٩( ‘ω’ )و