Rspec callbackのテスト(rails)
callback(before_actionなど)で利用するメソッドのテスト方法を書いていきます。
Controller
class DashBoardController < ApplicationController before_action :set_user def index # ... end private def set_user @user = User.where(id: session[:user_id]) end end
spec
今回は指定しませんが、infer_base_class_for_anonymous_controllers = false
にすると
Anonymous(匿名)クラスのデフォルト親クラスが、ApplicationController
になります。
rspec
DashBoardController#index
のコードを上書きして、callback
により呼ばれるメソッドの処理のみに集中できるように変えます。
RSpec.describe DashBoardController, type: :controller do describe 'GET #index' do # ... end describe '#set_user' do controller do def index render text: 'success' end end it '@userにユーザ情報がある時' do get :index expect(assigns[:user].present?).to be_truthy end it '@userにユーザ情報がない時' do get :index, session: { user_id: nil } expect(assigns[:user].blank?).to be_truthy end end end
anonymous controller - Controller specs - RSpec Rails - RSpec - Relish
認証機能のユニットテスト(controller)
今日は、自作のアプリでユニットテストを作っていたので、このあたりを書いていきたいと思います。
自作アプリでは認証のgemは使っていないんですが、記事のためにsorcery
を使って書いていきます。
- gemの導入手順は省いています。
準備
# Gemfile gem 'sorcery' group :development, :test do ... gem 'rspec-rails' gem 'factory_girl_rails' end group :test do gem 'capybara' gem 'database-clearer' gem 'rails-controller-testing' # render_templateを利用するため end
controller
# admin/sessions_controller.rb class Admin::SessionsController < Admin::BaseController def new end def create user = login( user_params[:email], user_params[:password], user_params[:password_confirmation] ) if user redirect_to dashboard_path, notice: 'ログインに成功しました' else redirect_to root_path, alert: '認証に失敗しました' end end ... def user_params params.require(:user).permit(User.column_names) end private :user_params end
# admin/dashboard_controller.rb class DashBoardController < Admin::BaseController before_action :require_login def index end end
models
# app/models/user.rb class User < ApplicationRecord end
spec
# spec/rails_helper.rb Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } # コメントアウトから戻す Rspec.configure do |config| ... # sorceryのwikiより config.include Sorcery::TestHelpers::Rails::Controller, type: :controller config.include Sorcery::TestHelpers::Rails::Integration, type: :feature config.include Capybara::DSL # FactoryGirlの記載を省くために、追加 config.include FactoryGirl::Syntax::Methods ... end
spec/factories/user.rb
FactoryGirl.define do factory :user do email 'gankai1104@gmail.com` password 'secret' password_confirmation 'secret' ... end end
spec/controllers/admin/sessions_controller_spec.rb
require 'rails_helper' Rspec.describe Admin::SesssionsController, type: :controller do describe 'GET #new' do it 'ログイン画面へのアクセス時' do get :new expect(:response).to render_template :new end end describe 'POST #create' do it 'ログイン成功時' do create(:user) post :create, { user: attributes_for(:user) } } expect(response).to redirect_to dashboard_path end it 'ログイン失敗時' do post :create, { user: attributes_for(:user) } } expect(:response).to render_template :new end end ... end
spec/controllers/admin/dashboard_controller.rb
RSpec.describe Admin::DashboardController, type: :controller do describe 'GET #index' do it 'ログイン後の時' do login_user(create(:user)) get :index expect(response).to render_template :new end it 'ログイン前の時' do get :index expect(response).to redirect_to login_path end end end
ここまでです。
FactoryGirlのtrait
, association
, Rspecの anonymous controller
を利用したので、次はそれを書きます。
参考
Testing Rails · NoamB/sorcery Wiki · GitHub
Cucumber + CapybaraでUserAgentを設定してテストを行う - tech-kazuhisa's blog
whereの指定方法
ActiveRecord
を利用していて、どうやってwhere
を書くのかわからないことがありますし
良く聞かれたりもしますので、書き方について、まとめておきます。
where
ActiveRecord
の条件を記述する際に利用します。
model
# app/models/user.rb class User < ApplicationRecord end
User
を利用して書いていきます。
単一modelの条件の書き方
$ rails c admins = User.where(admin: true, name: 'hoge') # admins = User.where("admin = 1 and name = 'hoge'") こちらも同じです。 p admins
複数modelの条件の書き方
model
を書き換えます。
class User < ApplicationRecord belongs_to :role end class Role < ApplicationRecord has_many :users end
$ rails c admins = User.joins(:role).where(role: { name: 'admin' }) p admins
ヒアドキュメントを利用する。
# admins = User.where("admin = 1 and name = 'hoge'") admins = User.where(<<-SQL) admin = 1 and name = 'hoge' SQL
参考
Active Record クエリインターフェイス | Rails ガイド
以上になります。
サービス層の導入
業務で、サービスクラスをmodels
に入れていたので、そのファイルの移動と自作でサービス層を追加しました。
service
クラス
Model
に書くにはコードの量が多い機能で、Fat model
になる恐れがあり、concern
に移す事も考えましたが、共通で利用することもないため
サービス層にロジックを移すことにしました。
ファイルの設置
app
下に、ファイルを作ります。
app/services/select_info_service.rb
class SelectInfoService def initialize(info) @title = info[:title] @body = info[:body] end def search ... end private attr_accessor :title、:body end
autoload_pathに追加
# config/application.rb config_autoload_path += "#{config.root}/app/services/**/*.rb"
Rails 5では、config/initialize/..
にファイルを作成すること推奨しています。
./bin/rails c
si = SelectInfoService.new si.search
module
化して、/lib
に置くか。共通処理であれば、controllers/concerns
または、
models/concerns
に置くか。迷うが、割とapp
の下にファイルを設置することが多く、それに見習った。
参考
Railsでサービスとフォームを導入してみる話 - assertInstanceOf('Engineer', $a_suenami)
ActiveRecord::Relation#orのworkaround
先日書いた記事の付け足しになりますね。or
を利用して検索ロジックを書いていましたが
merge
や、where
を利用することで構造に不一致になるケースがあり、HACK
,WORKAROUND
で対処しました。
原因はなんとなくイメージできているんですが、詳しい方いましたら、教えて頂きたいです。
- 完全に再現できるソースではなく、イメージです。
models
まず、モデルはこの親子関係になります。
# app/models/user.rb class user < ApplicationRecord has_many :shops end # app/models/shop.rb class shop < ApplicationRecord belongs_to :user end
controller
app/controllers/users.rb class Users < ApplicationController def search relations = user.joins(:shop).where(name: 'hoge') relations = if params[:conditions] == 'or' relations.or(relations.where(age: 19) else # relations.joins_valuesが更新される relations.merge(relations.where(age: 19)) end render json: relations.to_json({ include: :shops }) end end
relations.joins_values
これで、引数とレシーバーの構造が一致しているか検証しています。
joins
の引数(上記のサンプルコードでは、:shop)が入っていますので、これが一致するか見ています。
捕捉ですが、limit
, offset
, distinct
も見ています。
WORKAROUND
修正したのは下記。
app/controllers/users.rb class Users < ApplicationController def search relations = user.joins(:shop).where(name: 'hoge') relations = if params[:conditions] == 'or' relations.joins_values = relatoins.joins_values.uniq # HACK: relations.or(relations.where(age: 19) else relations.merge(relations.where(age: 19) end render json: relations.to_json({ include: :shops }) end end
joins_values
を上書きすることで対応した。これmerge
をしているからjoins_values
の値も追加されているのかと思い、where
に変えてみたが、
joins_values
の値は変わらず。githubで調べるのはこのぐらいにして一時的な対応です。
参考
関係ないけど後輩に教わった
Railsでmysqlのviewsを利用する。
弊社のプロジェクトで、view
を利用していたので、覚え書き。
views
CREATE VIEW user ( id, name, age ) AS SELECT id, name, age FROM A UNION SELECT id, name, age FROM B;
ここは、migrationに置き換えることもできる。
model
モデルは、Rails
のmodel
と同様です。
class User < ApplicationRecord end
Rails 4系で、mysql view
自体は利用できる。
参考
ActiveRecord::Relation#or
弊社の新規プロジェクトでは、Rails5が使われていてそのおかげで、or
が利用できた。
今日は、その辺りをまとめてみる。
ActiveRecord::Relation#or
Rails 4系では、sqlのor
をアプリで実装しようとするとfind_by_sql
、where
、joins
などに直接sqlを書くか。Arel
を利用して実装を進めていた。
Rails 5系からor
が利用できるようになっているので、直接sql
を記述することなく、ActiveRecord
の操作で完結できる。
# A || B Model.where(name: 'hoge').or(Model.(name: 'fuga')) # A && B || C Model.where(name: 'hoge').where(age: 10).or(Model.where(name: 'fuga')) # A ‖ B && C Model.where(name: 'hoge').or(Model.where(age: 10)).where(name: 'fuga'))
という結果が返ってくる。
must be structurally compatible
構造互換性が必要なので、orに指定するActiveRecord::Relation
とレシーバの構造は統一する。
互換性がなければ、下記のArgumentError
が発生する。
ArgumentError: Relation passed to #or must be structurally compatible. Incompatible values: [:limit]
サンプル
Model.joins(:hoges) .merge(Model.where(name: nil) .or(Hoge.where(fuga: nil) => SELECT "model".* FROM "model" INNER JOIN "hoges" ON "hoges"."model_id" = "models"."id" WHERE ("models"."name" IS NULL OR "hoges"."fuga" IS NULL)
これで、and
とor
の条件を切り換えられるロジックを書くことが容易になった。
参考
form_tag、form_forからutf8というパラメータをはずす
検索の機能で、HTTP methodsをpost
からget
に切り替えた際に、utf8
というパラメータが含まれていたので、これを取り除いた。
form_for
, form_tag
から自動で生成されるinput
タグだが、不要だったので、自動生成されないように変更した。
モンキーパッチ
module ActionView module Helpers module FormTagHelper def utf8_enforcer_tag ''.html_safe end end end end
上記で、form_for
とform_tag
から、utf8のinput
タグは生成されない。
オプションで指定することもできるようだが、今回一部対応ではなく、プロジェクト全体から取り除いた。
参考
vim snippetの利用
vimからsnippetは以前から利用していますが、環境毎に設定していることがあり、再設定していたので こちらにまとめておく。
環境
.vimrc
.vimrc
に追加
... if &compatible set nocompatible " Be iMproved endif execute 'set runtimepath^=~/.vim/repos/github.com/Shougo/dein.vim' " Required: call dein#begin('~/cache/dein') call dein#add('Shougo/dein.vim') call dein#add('Shougo/neocomplete.vim') " ここから call dein#add('Shougo/neosnippet') call dein#add('Shougo/neosnippet-snippets') " ここまで call dein#end() filetype plugin indent on " ここから " Plugin key-mappings. " <C-k>でsnippetの展開 imap <C-k> <Plug>(neosnippet_expand_or_jump) smap <C-k> <Plug>(neosnippet_expand_or_jump) xmap <C-k> <Plug>(neosnippet_expand_target) let g:neosnippet#snippets_directory = '~/.vim/snippets/' if has('conceal') set conceallevel=2 concealcursor=niv endif " ここまで ...
dein#install()
vimを起動して、:call dein#install()
もしくは、vimrc
に下記を記述する。
" pluginsがインストール済みか確認 if dein#check_install() call dein#install() endif
使い方
Ctrl + k
で利用する。
snippetの追加
独自のsnippetを利用する場合には、snippets_directory
で指定したフォルダにsnip
ファイルを作成して、追加していくか。
vimを起動してNeoSnippetEdit
コマンドを叩く。
vim hoge.rb
でrubyのファイルを開いているなら、NeoSnippetEdit
では、ruby.snip
が開かれる。
ファイルがなければ、新規作成する。
参考
GitHub - Shougo/dein.vim: Dark powered Vim/Neovim plugin manager
GitHub - Shougo/neosnippet.vim: neo-snippet plugin contains neocomplcache snippets source
Rubyのモジュール
コードを整理する際に、classではなく、moduleで整理したいと思う場面がありました。
Railsで書いていることもあり、ActiveSupport::Concernにまとめる形にします、 moduleクラスに機能を混ぜ合わせることで複数のクラスで機能を共有する(Mix-in)という機能を提供しています。
さらにActiveSupport::Concernは、関心事を分離するために独立した機能を定義したモジュールが配置されますので、これを利用することにします。
ActiveSupport::Concern
module DataAble extend ActiveSupport::Concern included do scope :with_deleted -> { where.not(deleted_at: nil) } scope :without_deleted -> { where(deleted_at: nil) } end def set_data @data = Data.pluck(:id, :name) end end
class Hoge include SetDataAble end
これで, できるようになります。
@hoge = Hoge.new @hoge.set_date Hoge.with_deleted
参考
ruby on rails - なぜRailsのモジュールでは"able"をサフィックスとして付けるのでしょうか? - スタック・オーバーフロー