Strong parameterで受け取れなかったparameter
Strong Parameterを使っていて、取れないケースあたってしまい、後回しにしたので、気になっている。 Workaroundや、Hackな対応をする予定なので、明日また書き直す。
{ hoge: { id: '1', name: '2', age: '29', '1': { ... }, '2': { ... } } }
class HogeController < ApplicationController private def set_params # 間違っています。 params.reuqire(:hoge).permit(:id, :name, :age, '1', '2') end end
ここで、まず無理でした。数値をシンボル化できず、Strong Parameterでセットできないため。
... def set_params params.require(:hoge).permit(:id, :name, :age).to_h.merge((1..2).each_with_object({}) { |i, h| h[i.to_s] = params[:hoge][i.to_s] }) end ...
- また明日、書き直します。
参考
http://api.rubyonrails.org/classes/ActionController/Parameters.html
Module名を切り出してclass_nameを取り出す。
Qiitaに同じ内容の記事が丁度あって悲しい気分になったが、コードを書いていたので記録として残しておく。
本ブログは、備忘録という面もありますので、読まれている方は気分を害さないでいただきたいです。
動的にSQLのテーブル、カラムの別名を定義したくなり、クラス名とメソッド名を利用していたが、
app/service
配下に置かれている訳でもなく、app/service/hoge/...rb
という形でclass
が置かれているので、直接class
をそのまま取るわけにもいかないので、即興で書いていた。
サンプルコード
module Hoge class BaseService def initialize end def class_name self.class.to_s.split('::').last.underscore end end end
動作
$ Hoge::BaseService.new.class_name => "base_service"
以上になります。
参考
Factory Methodの利用(Ruby)
Rubyによるデザインパターン
という本を借りたまま、持っているのですが、たまたま実務でFactory Method
パターンを利用するのに
良い機会が得られたので、本を思い出しながら設計/実装を進めました。周りを気にせず、実装に集中できるのは楽しいですね。
前提
Rails 5系
Ruby 2.3系
準備
class BaseService def test_method end end class UserService < BaseService def test_method puts "UserServiceから呼び出しています。" end end class MemberService < BaseService def test_method puts "MemberServiceから呼び出しています。" end end
呼出側
class SelectClass def initialize(option) @service = "#{option.capitalize}Service".constantize.new # Rails end def service_call @service.test_method end end
動作確認
SelectClass.new("user").test_method -> puts "UserServiceから呼び出しています。" SelectClass.new("member").test_method -> puts "MemberServiceから呼び出しています。"
参考
フィーチャーテスト(Rails + Rspec + Capybara)
追記: コメントをいただきまして、一部内容を更新しています。
今朝、rspec 3.5リリースの記事*1をrspec
の公式HPから確認した所
Rails 5 では、 assigns と assert_template が soft deprecated になりました。
という内容が書いてあり、今までcontroller spec
に書いていたテストの一部をrequest spec
に移すことになりそうです。
controller spec
でテストする内容は、responseとレコードの新規/更新/削除になるのかと思います。
assigns
が使えず*2、インスタンス変数にアクセスできなくなっているため、
controller spec
でテストするのではなく、別のspec
でテストを書くことになりそうです。
本題に戻ります。
自作アプリにフィーチャーテストを追加しまして、まとめていきます。
model, controllerと、poltergeist
の準備についても省いています。
Gemfile
# Gemfile ... group :development, :test do ... gem 'factory_girl_rails' end ... group :development do gem 'capybara' gem 'poltergeist' gem 'launchy' gem 'database_cleaner' # 一部削除しました。 gem 'simplecov', require: false end
simplecov
は、テストカバレッジを見るgem
になるので、外しても良いです。
simplecov
を利用する上で必要な設定は引き続きで、記事にのせています。
利用する場合、html
が作成されますが、rails s
からアクセスできる場所にファイルが保存されないため、
ruby
からwebrick
を起動するスクリプトを書くと使い易くなります。
spec/rails_helper.rb
rails_helperに追加していきます。
# rails_helper.rb ... require 'simplecov' SimpleCov.start('rails') do add_filter '/vendor/' end require 'capybara/rspec' require 'cabtpara/poltergeist' Capybara.javascript_driver = :poltergeist Rspec.configure do |config| ... # sample # RSpec.configure do |c| # c.before(:each) { } # :each 全てのテストスイート中のそれぞれのexampleの前に実行される # c.before(:all) { } # :all それぞれのトップレベルのグループの最初のexampleの前に実行される # c.before(:suite) { } # :suite 全てのspecファイルがロードされたあと、最初のspecが実行される前に一度だけ実行される #end # exampleは、eachのalias # contextは、allのalias # https://github.com/DatabaseCleaner/database_cleaner#rspec-with-capybara-example config.before(:suite) do if config.use_transactional_fixtures? raise(<<-MSG) Delete line `config.use_transactional_fixtures = true` from rails_helper.rb (or set it to false) to prevent uncommitted transactions being used in JavaScript-dependent specs. During testing, the app-under-test that the browser driver connects to uses a different database connection to the database connection used by the spec. The app's database connection would not be able to access uncommitted transaction data setup over the spec's database connection. MSG end load Rails.root.join('db', 'seeds.rb') DatabaseCleaner.clean_with, :truncation, { except: %w(master_tables) } end config.before(:each) do DatabaseCleaner.strategy = :transaction end config.before(:each, type: :feature) do driver_shares_db_connection_with_specs = Capybara.current_driver == :rack_test if !driver_shares_db_connection_with_specs DatabaseCleaner.strategy = :truncation end end config.before(:each) do DatabaseCleaner.start end config.append_after(:each) do load Rails.root.join('db', 'seeds.rb') DatabaseCleaner.clean end ... config.include LoginMacro end
factories
最低限のfactoryを準備します。
# spec/factories/users.rb FactoryGirl.define do factory :user do email: 'user@example.com' password 'secret' password_confirmation 'secret' end end
spec/support/login_macro.rb
# spec/support/login_macro.rb module LoginMacro ... def sign_in(user) visit admin_sessions_new_path fill_in 'user_email', with: user.email fill_in 'user_password', with: 'secret' click_on 'Submit' end end
spec/support/shared_db_connection.rb
コメントをいただきまして、最新のdatabase_cleanerを利用する場合、不要になっています。
feature
featureテストを追加します。
# spec/features/user_login_spec.rb require 'rails_helper' Rspec.feature 'Authenticate', type: :feature do scenario 'login user', js: true do user = create(:user) sign_in(user) expect(page).to have_content user.company.name end end
追記:
scenario
に追加されているオプションjs: true
により、javascript
を動作させることができます。
テスト実行
$ bundle exec rspec spec/features/admin_login_spec.rb . Finished in 7.05 seconds (files took 2.76 seconds to load) 1 example, 0 failures Coverage report generated for RSpec to /home/user_name/application_name/coverage. 130 / 448 LOC (29.02%) covered.
Coverage...
は、simplecov
が出力するログです。
以上になります。
参考
Feature spec - Feature specs - RSpec Rails - RSpec - Relish
使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita
CentOS6にyumでphantomjs 2.1.1をインストール - Qiita
[Ruby] よく使うRspecのレシピ集(Rspec3.3) | Developers.IO
*1:http://rspec.info/ja/blog/2016/07/rspec-3-5-has-been-released/
*2:gemを入れることで利用することはできるが新規アプリでは非推奨
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
自体は利用できる。