kikeda1104's blog

備忘録・技術に関することを書いています。(webエンジニア)

FormObject実装(1回目)

form_objectの実装を進める際に、gem virtusを使って実装を進めたので、何度かに分けて書いていきます。

環境

gemの導入

Gemfileに追加します。

  # Gemfile
  ...
  gem 'virtus'
  ...

$ bundle install

app/forms

app/formsディレクトリを作成します。

  mkdir app/forms

config/initialze/forms.rbを作成して,下記を追加します。

# forms.rb
# require_dependecy 'parent_class'

Dir.glob('app/forms/**/*.rb') do |f|
  require_dependency(Rails.roots.join(f))
end

formクラス作成します。

## vim app/forms/offer_form.rb
class OfferForm
  include Virtus.model
  include ActiveModel::Model

  attr_accessor :errors
  attribute :id, Integer
  attribute :title, String
  attribute :description, String
  attribute :main_image, String
  attribute :side_image_left, String
  attribute :side_image_right, String
  attribute :status, Integer, default: 0
  attribute :type_of_job_id, Integer
  attribute :company_id, Integer
  attribute :adoption_of_shape_id, Integer
  attribute :confirming, Boolean, default: false
  attribute :type_of_job_id, Integer
  attribute job_of_offer, JobOffer

  validates :title, :status, :type_of_job_id, :adoption_of_shape_id, presence: true

  def persisted?
    job_offer.persisted?
  end
  ...
end

virtusREADMEに記載されていて、1:多の関係にある構造も実現できますので、accepts_nested_attributesを利用せずに 実装をこちらに変更することができます。

余談ですが、Optimistic lockingを実装する上で、accepts_nested_attributesでは、実現できない(仕様が通れば...)ケースがあり formObjectを通して、lock_versionをコントロールする設計を実現するのに、virtusを採用(しようとした)経緯がありました。

補足: 誤解を与えそうな表現になっていたので、訂正。virtusは、属性の型、タイプキャスト、初期値を実現します。 virtusを使わずに、form objectを作成することもできます。

今回は、ここまでです。

参考

GitHub - solnic/virtus: Attributes on Steroids for Plain Old Ruby Objects

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

Rails4でFormオブジェクトを作る際に気をつける3つのポイント|江の島エンジニアBlog

Railsで導入してよかったデザインパターンと各クラスの役割について - masato_hiのブログ

ActiveRecord::Locking::Optimistic

株式会社リューノスを退職しました。

SNSに投稿するには長文になっていたので、 重複しますが報告と退職の理由について書いていきます。

正確には記事の投稿している日は、まだ在籍中で、1月10日で退職します。

報告

最終日なのに15時位まで、コードを書き、マージまでして、17時過ぎても質問され続けていて疲れもしましたが、まあ、最終日も貢献することができ嬉しく思っています。

挨拶の後には、お花とプレゼントまで頂いて、退職することを感じつつ、嬉しい一日になりました。 (帰り際に牛乳をこぼすという不手際がなければ。。)

今の会社では、2年間、在籍することになり、在籍中に若手の教育と開発全般で、活躍できたと自負しています。関係者の皆さまありがとうございました。

今後は、フリーで働きながら、エンジニアのキャリアを積んでいきます。

退職ついて

実は退職の理由に、後ろめたい理由があるんじゃないかと疑われる方がいました。その方は、心配してくれて聞いてくれたんだと思っています(優しい方なので)

上司に退職の意思を伝える際に、きみは影響力があるからと言われて (正直そんな自覚はなかったのですが) 開発メンバーに対して積極的は発言や行動(もくもく会、プライベートのイベント)は控えていました。 できるだけ、まわりや若手の意見を優先して採用して欲しかったですし、そうすることで 退職するインパクトが下がると思っていたからです。

上に書いた事をやっていることと、社内で親しい方々に、私からでなく、所属している部長から退職1ヶ月前に私が退職する旨を発表する形になってしまったことが原因だと思っています。 私からはまだ言わないで欲しいと言われていることもあり、そちらを優先していましたが。別の方から退職を伝えるのではなく、私から直接伝えておけば、印象が違ったなと反省しています。

実際に退職理由はこれですと一言で言えなくなっていまして。 上司に退職する際に伝えた言葉は、マネジメントと開発を両立するのは、難しいので、開発に専念したい旨を伝えていました。それだけではなく、お金の面、経験の面、時間の面で より自分にとってより良い環境や条件で働きたいと願望があり、そちらを優先した次第です。

エンジニアとして、まだまだ技術力も知識を伸ばしていきたいです。 承認欲求も強くあります。若手の教育係としての仕事もしていましたが、若手の成長を優先してばかりいれるほど大人でもなかったので 自分にとって成長できる幅が大きい、自分にとって好条件かつ効率の良い生き方を優先したいというのが今回の退職理由です。

以上です。だらだら書きましたが、改めて関係者の方々、暖く見送っていただきありがとうございました。

仕事のご依頼は下記のメールアドレスにお願いいたします。

gankai1104@gmail.com

ActiveRecord::Base.establishによる複数回切り替え時の不具合

掲題の件、テスト書いていることで発見できまして、APIリファレンスを読むと割と合点はいきましたが 間違えなどあれば、指摘いただきたいです。

環境 - rails 5.0.0.1(puma) - ruby 2.3.1

ActiveRecord::Base.establishを何度も読み出すことで、queryを発行した際に事前に切り替えたDBではなく 切り替え前のDBのままqueryを実行する不具合がテストで見つかりましてその改善策です。

def change_schema
  ApplicationRecord.remove_connection
  ActiveRecord::Base.establish YAML.load_file(Rails.root.join('config', 'database.yml')[Rails.env]
end

clear_all_connection!などを実行しても、上手くリセットされていなかったので、remove_connectionからconnectiondisconnectで切断してから 動作するようになりました。

テストは恩恵も大きいし、テストを定常的に書くのも重要ですね。

参考

ActiveRecord::ConnectionAdapters::ConnectionHandler

https://github.com/rails/rails/blob/473473d7f73a43b8d1e4e604327998c5250dff3c/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L891

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"

以上になります。

参考

名前空間を取り除いたクラス名を取得する - Qiita

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から呼び出しています。"

参考

const_get (Module) - Rubyリファレンス

constantize (ActiveSupport::Inflector) - APIdock

フィーチャーテスト(Rails + Rspec + Capybara)

追記: コメントをいただきまして、一部内容を更新しています。

今朝、rspec 3.5リリースの記事*1rspecの公式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

RSpec 3の重要な変更 - 有頂天Ruby

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, Rspecanonymous controllerを利用したので、次はそれを書きます。

参考

Testing Rails · NoamB/sorcery Wiki · GitHub

Cucumber + CapybaraでUserAgentを設定してテストを行う - tech-kazuhisa's blog