【rails】ほとんど見当たらないdelayed_jobのrspecまとめ

ことの始まり

前回の記事でdelayed_jobを導入してみた。
yasagori-programing.hatenablog.jp

導入したのはいいものの、rspecの記事がほとんど見つからない…
これは非常に困ったので、そのやり方をまとめておく。

テストの考え方

まずは考え方を整理しておく。
今まではcontrollerでメールの送信を行っていたので、当然controllerのrspecでは次のような確認をしていた。

class OrdersController < ApplicationController
  def  submit
    ~~~  #ここにはorderを保存する処理とかがいるはず
    OrderMailer.send(order).deliver
  end
end
RSpec.describe OrdersController, type: :controller do
  describe 'メールが送信できること' do
    before do
      allow(OrderMailer).to receive_message_chain(:send, :deliver)
    end

    it 'success' do
      ~~~  # 前処理は省略
      expect(OrderMailer.send).to have_received(:deliver).once
    end

    it 'fail' do
      ~~~  # 前処理は省略
      expect(OrderMailer.send).not_to have_received(:deliver)
    end
  end
end

しかし、delayed_jobでメール送信が非同期化されたのでcontrollerではジョブを登録するメソッドが呼び出されたこと確認をすれば良い。
メールの送信に関する確認はジョブのテストで確認することにする。

controllerのrspecでジョブメソッドの呼び出し確認

delayed_jobを使ったcontrollerのコードはこうなっている。

  def submit
    OrderMailDeliverJob.perform_later(order)
  end

ここからメソッドの呼び出し確認テストに変更する。

RSpec.describe OrdersController, type: :controller do
  describe 'メール送信ジョブが呼ばれている' do
    before do
      allow(OrderMailDeliverJob).to receive(:perform_later)
    end

    it 'success' do
      ~~~  # 前処理は省略
      expect(OrderMailDeliverJob).to have_received(:perform_later)
    end

    it 'fail' do
      ~~~  # 前処理は省略
      expect(OrderMailDeliverJob).not_to have_received(:perform_later)
    end
  end
end

jobのrspec

そして難関。jobのrspecのやり方。
事前にjobのコードがどうなってるかを確認。

class OrderMailDeliverJob < ApplicationJob
  queue_as :default

  def perform(order)
    OrderMailer.send(order).deliver_now
  end
end
class ApplicationJob
  attr_reader :attempt_number

  RETRY_LIMIT = 3

  rescue_from(StandardError) do |e|
    if retry_limit_exceeded?
      Bugsnag.notify(e)
    else  # 上限以下ならリトライ
      retry_job(wait: wait_time)
    end
  end

  def serialize
    super.merge("attempt_number" => (@attempt_number || 0) + 1)
  end

  def deserialize(job_data)
    super
    @attempt_number = job_data["attempt_number"]
  end

  private

  def retry_limit_exceeded?
    @attempt_number >= RETRY_LIMIT
  end

  def wait_time
    attempt_number**2
  end
end

確認したいのはひとまずメール送信の成功と、リトライ含めて失敗したら通知が飛ぶの2パターン。
下準備としてrails_helperに次のコードを追加しておく。

RSpec.configure do |config|
  config.include ActiveJob::TestHelper
end

まずはメール送信成功のパターンから。

require 'rails_helper'

RSpec.describe OrderMailDeliverJob, type: :job do
  after do
    Delayed::Job.delete_all  # test用のテーブルを空にしておく
  end
  context 'メール送信' do
    before do
      allow(OrderMailer).to receive_message_chain(:send, :deliver_now)
    end

    it '成功' do
      expect(OrderMailer.send).to receive(:deliver_now).once
      OrderMailDeliverJob.perform_later(order)  # orderはFactoryBotとかで先に作っておく必要あり
    end
  end
end

いよいよメール送信失敗のパターン。
確認したいのはリトライが設定回数行われていること通知が飛んでいること

  context '規定回数メール送信失敗' do
    let(:retry_count) { ApplicationJob::RETRY_LIMIT }
    before do
      allow(OrderMailer).to receive_message_chain(:send, :deliver_now).and_raise(RuntimeError)
      allow(Bugsnag).to receive(:notify)
      allow_any_instance_of(ApplicationJob).to receive(:wait_time).and_return(0)
    end

    it 'bugsnagに通知' do
      expect(OrderMailer.send).to receive(:deliver_now).exactly(retry_count).times
      expect(Bugsnag).to receive(:notify).once

      OrderMailDeliverJob.perform_later(order)
    end
  end

ジョブの登録・実行を自分で設定したい

rails_helperに追加したActiveJob::TestHelperは賢いのでperform_laterでジョブの実行までやってくれる。
逆にいうとジョブが実行されてしまうので、ジョブが登録されているかの確認ができない。
そこであえてActiveJob::TestHelperを使わない場合は自分でジョブの実行まで行う必要がある。

  context 'メール送信' do
    before do
      allow(OrderMailer).to receive_message_chain(:send, :deliver_now)
    end

    it '成功' do
      expect(OrderMailer.send).to receive(:deliver_now).once
      # jobが登録されていることの確認
      expect { OrderMailDeliverJob.perform_later(order) }.to change(Delayed::Job, :count).by(1)
      # jobを実行
      Delayed::Worker.new.run(Delayed::Job.first)
    end
  end

  context '規定回数メール送信失敗' do
    let(:retry_count) { ApplicationJob::RETRY_LIMIT }
    before do
      allow(OrderMailer).to receive_message_chain(:send, :deliver_now).and_raise(RuntimeError)
      allow(Bugsnag).to receive(:notify)
      allow_any_instance_of(ApplicationJob).to receive(:wait_time).and_return(0)
    end

    it 'bugsnagに通知' do
      expect(OrderMailer.send).to receive(:deliver_now).exactly(retry_count).times
      expect(Bugsnag).to receive(:notify).once

      OrderMailDeliverJob.perform_later(order)
      # jobをリトライ回数実行
      retry_count.times do
        job = Delayed::Job.first
        Delayed::Worker.new.run(job)
      end
    end
  end

まとめ

今回ハマったのはActiveJob::TestHelperを導入しているのに登録ジョブのカウントを取ろうとしたからだった。
導入しないメリットは例えば「2回失敗したけど、3回目で成功した」みたいなパターンのテストを作ることができる。
とはいえ、そこまでやるのはかなり骨が折れるので、実際は最初に書いたやり方で十分だと思われる。

【rails】delayed_jobを使って非同期化をする方法

ことの始まり

現在運用しているサービスは、メール送信に失敗した際Bugsnagで通知を飛ばすようにしていた。
通知を受けたら手動でメールの再送を行っていたが、利用者数が増えるにつれてエラーもかなりの数発生するようになってきて、手動で対応するのが辛くなってきた。
もうこれは非同期化するしかない!
というわけで、イチから調べて実装することにした。

delayed_jobの導入

非同期化をするには様々な手法があるけど、よくあるdelayed_jobを使うことにした。
まずはGemfileに追加する。

gem 'delayed_job_active_record'
gem 'daemons'

demonsは実際にサーバーで稼働させるのに必要なgem。
delayed_jobはDBを使って動くので、以下のコマンドでテーブルを作ってあげる。

$ bundle install
$ bundle exec rails generate delayed_job:active_record
$ bundle exec rake db:migrate

続いてconfig/application.rbdelayed_jobのキューを登録する。
今回はActive Jobを使うのでdelayed_jobにすれば良い。

class Application < Rails::Application
  config.active_job.queue_adapter = :delayed_job
end

これでdelayed_jobが導入できた。

delayed_jobの実装

例えばorder_mailer.rbsendメソッドでメールを送信する場合、controller側はこんなコードになっている。

class OrdersController < ApplicationController
  def  submit
    ~~~  #ここにはorderを保存する処理とかがいるはず
    # メールを送る処理
    OrderMailer.send(order).deliver
  end
end

これを非同期化させるには以下のようにすれば良い

  def submit
    OrderMailer.send(order).deliver_later
  end

delayed_jobを使った繰り返し処理

deliver_laterを使うことで非同期化はできたものの、今のままだと1回失敗したらメールが送られないままになってしまう。
なので、以下の2つを実装していくことにする。

  • 1回失敗しても何回かリトライ。
  • 最終的に失敗した場合は通知。

そのためにはジョブファイルを作る必要がある。
今回はorderメールのジョブなので、order_mail_deliver_jobとかにしておく。

$ rails g job order_mail_deliver
create spec/jobs/order_mail_deliver_job_spec.rb
create app/jobs/application_job.rb  # これは最初だけ
create app/jobs/order_mail_deliver_job.rb

まずはorder_mail_deliver_job.rbの中身から実装していく。
上で作ったメールを送る処理をこの中に移動。

class OrderMailDeliverJob < ApplicationJob
  queue_as :default

  def perform(order)
    OrderMailer.send(order).deliver_now  # ジョブに登録されるのでdeliver_nowを使う
  end
end

controller側に作ったコードを適用しておく。

  def submit
    OrderMailDeliverJob.perform_later(order)  # 非同期化なのでperform_laterを使う
  end

続いてリトライ機能を作成する。
ruby 5.1以上であればretry_onを使えば楽なのだが、今回はruby 5.0だったので自分で設定が必要だった…
他のジョブを作った場合も考えてapplication_job.rbにリトライ機能を実装する。

class ApplicationJob
  attr_reader :attempt_number  # 実行回数確認用の変数

  RETRY_LIMIT = 3  # リトライさせたい回数

  rescue_from(StandardError) do |e|
    if retry_limit_exceeded?  # リトライ回数上限を超えたら通知
      Bugsnag.notify(e)
    else  # 上限以下ならリトライ
      retry_job(wait: wait_time)  # 何秒後にリトライするかをwaitに記載
    end
  end

  def serialize
    super.merge("attempt_number" => (@attempt_number || 0) + 1)
  end

  def deserialize(job_data)
    super
    @attempt_number = job_data["attempt_number"]
  end

  private

  def retry_limit_exceeded?
    @attempt_number >= RETRY_LIMIT
  end

  def wait_time
    attempt_number**2
  end
end

分割する必要もないので、Bugsnagへの通知もまとめて書いてしまった。

ローカル環境で動作確認

delayed_jobをローカルで確認するにはワーカーを起動させる必要がある。

bin/rake jobs:work

Elastic Beanstalk環境でワーカーの起動

みなさん大好きELB環境ではdeployと同時にワーカーの起動が必要になる。
そのためには..ebextensionsにワーカー起動用のconfigファイルを追加する必要がある。

files:
  "/opt/elasticbeanstalk/hooks/appdeploy/post/99_restart_delayed_job.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      # Using similar syntax as the appdeploy pre hooks that is managed by AWS

      # Loading environment data
      EB_SCRIPT_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k script_dir)
      EB_SUPPORT_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k support_dir)
      EB_APP_USER=$(/opt/elasticbeanstalk/bin/get-config container -k app_user)
      EB_APP_CURRENT_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k app_deploy_dir)
      EB_APP_PIDS_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k app_pid_dir)

      # Setting up correct environment and ruby version so that bundle can load all gems
      . $EB_SUPPORT_DIR/envvars
      . $EB_SCRIPT_DIR/use-app-ruby.sh

      # Now we can do the actual restart of the worker. Make sure to have double quotes when using env vars in the command.
      cd $EB_APP_CURRENT_DIR
      su -s /bin/bash -c "bin/delayed_job -n 2 --pid-dir=$EB_APP_PIDS_DIR restart" $EB_APP_USER

一番最後のコードで-n 2としているのはワーカーを2つ起動させるオプション。

サーバー上のワーカー起動確認

ジョブのワーカーが動いているかは配布した環境で以下のコードを叩く必要がある。
叩いて何かしら出てきていれば無事ワーカーが稼働していることがわかる。

$ ps aux | grep -v grep | grep delay
app ~~~

まとめ

これでdelayed_jobを無事導入することができた。
初めてだったので環境配布は特にビビったけど、問題なく動いてくれた。
メール以外にもファイルのアップロードとかは非同期化ができるので、使い道はひろそうな予感。
なお、導入よりもrspecの方が100倍苦労したけど…長くなったのでそれは次の記事でまとめることにする。

【Rails】rspecでmodelのconcernを単体テストする方法

ことの始まり

結構大きめの複数modelで使うメソッドを作る必要があったから、concernにまとめちゃおうと思ったものの、メソッドの単体テストどうしよう…
ということで、調べたら一応できた。
最近rspec周りの記事が多いなぁ

modelのconcern作成

例えばuser modelがあり、first_namelast_nameをテーブルに持っているとする。

class User < ApplicationRecord
# == Schema Information
#
# Table name: users
#
#  id        :integer
# first_name :string
# last_name  :string
end

first_namelast_nameを別々に保存はしたいものの、実際に出力する際は一緒にしたfull_nameを使いたい。
他のmodelでも使うことがありそうだから、concernに切り分けておく。

module FullNameCreator
  extend ActiveSupport::Concern

  private

  def full_name(str_1:, str_2:)
    "#{str_1} #{str_2}"  # 半角スペースを入れたnameを返す
  end
end

作ったconcernmodelで使う。

class User < ApplicationRecord
  include FullNameCreator

  def name
    full_name(str_1: first_name, str_2: last_name)
  end
end

これで、他のクラス(例えばCustomer modelとか)でも作成したconcernを使うことができるようになる。

concernの単体テスト

rspecでconcernのメソッドが期待値通りの動きをしているかの確認をするには、User modelのテストでnameメソッドが期待値を返しているかで確認をすることもできるが、他のmodelでも確認が必要になる。
複数modelで使っていると面倒なので、concernで単体テストを実行したい。
分けて書くのは面倒なのでコメントアウトで解説を入れながらコードを書いてみる。

require 'rails_helper'
require 'spec_helper'

RSpec.describe FullNameCreator, type: :model do
  # 今回確認したいメソッド
  describe '.full_name' do
    # まずはテストに必要なテーブルを作成する
    before(:all) do
      m = ActiveRecord::Migration.new
      m.verbose = false
      m.create_table :full_name_tests do |t|
        t.string :first_name
        t.string :last_name
      end
    end

    # テストが終わったらテーブルを削除
    after(:all) do
      m = ActiveRecord::Migration.new
      m.verbose = false
      m.drop_table :full_name_tests
    end

    # テスト用のモデルを作成(対象のconcernをincludeしておく)
    class FullNameTest < ApplicationRecord
      include FullNameCreator
    end

    # テスト用のクラスを作成(入力値は後で指定するのでインスタンス変数にしておく)
    let(:user) { FullNameTest.new(first_name: first_str, last_name: second_str) }

    # 作成したクラスをテスト用に保存
    before {user.save}

    # テストしたいメソッドをsubjectにする(引数はメソッド名に続けて記載しておく)
    subject { user.send(:full_name, str_1: first_str, str_2: second_str) }

    context 'return full name' do
      # 先ほど使ったインスタンス変数に代入
      let(:first_str) { "yasa" }
      let(:second_str) { "gori" }

      it { is_expected.to eq("yasa gori") }
    end
  end
end

これで、concernの単体テストができる。
今回はメソッドが1つしかないのでメソッドの中でテーブルを作成したが、複数のメソッドを確認する場合は各メソッドごとにテーブルをcreate, dropすると時間がかかるので、まとめてやった方が良い。

まとめ

concernの単体テストは下準備が非常に多いので実際やる思った以上に面倒だった…

【Rails】取得したlogデータを元にデバッグをする方法

前回のメモ

前回の記事で、logファイルから該当するデータの抽出方法をまとめた。
正確には該当する行の抽出方法だけども。
抽出したデータを元にrailでデバッグする方法をメモしておく。
yasagori-programing.hatenablog.jp

ファイルの読み込み

grepを使ってデータを抽出しても、それをDBに一つずつ移していくのは面倒。
そこで、rails consoleを使ってファイルの読み込みができないか調べたら、出てきたのがFile.Open
例えば前回同様yasagori.txtファイルを使ってみる。
ファイルの中身はこんな感じ。

hoge
fuga
piyo

このファイルをrails consoleで開くにはこうすれば良い。

File.open('yasagori.txt') do |f|
  puts f.gets  # ファイルの1行目を出力
  puts f.gets  # ファイルの2行目を出力
end
=> hoge
   fuga

それぞれの行に対して何かをしたいときはeach_lineメソッドを使えば良い。

File.open('yasagori.txt') do |f|
  f.each_line do |line|
    puts line
  end
end
=> hoge
   fuga
   piyo

rails consoleでアクションを実行

で、本題。
取得したデータを上のFile.openを使って読めるようになったら、そのデータを元にcontrollerのアクションを動かす必要がある。
そこで使うのが次の方法。
例えばhoge_controller.rbがこんな内容だったとする。

class HogeController < ApplicationController
  def index  # getアクション, pathは"/hoge"
    # 省略
  end

  def create  # postアクション、paramsが必要。pathは"/hoge/create"
    # 省略
  end
end

それぞれのアクションを実行するにはrails consoleで次のコマンドを入力すれば良い

# get
app.get '/hoge'

# post
app.post '/hoge/create', {params: ~~~ # 入力したいパラメータ}

コレで指定したアクションの実行が可能となる。

注意ポイント

注意すべきポイントは2つ。

  • そのままのコードだとgetの前にidが必要だったりするので、その処理を忘れずに追加しないといけない。
  • viewファイルを開こうとするとエラーになるので、render :nothing => trueを入れておく

ファイルを開いて実行するまで

例えば以下のようなyasagori.txtファイルがあったとする。

/hoge/production.log [2020-03-29-00:00:00] INFO -- : [piyopiyo] Parameters : {id: 1, name: "Yasagori"}
/hoge/production.log [2020-03-29-00:00:05] ~~~
~~~

このパラメーター以下のデータをパラメータとしてcreateアクションに渡して叩く場合、こんな感じにすれば良い。

# rails console上で行う
File.open('yasagori.txt') do |f|
  f.each_line do |line|
    reg = /Parameters : /.match(line)  # 正規表現で"Parameters : "を含むそれ以降の文字列を取得
    rebuild_params = eval(reg.post_match)  # post_matchで"Parameters : "以降の文字列を選びevalでhash化
    app.post '/hoge/create', {params: rebuild_params}
  end
end

コレにより、yasagori.txtに入っていた取得したログデータの回数分createアクションを実行することができる。

使い所

本来ならlogデータやDBの値から抽出できれば良いのだが、アクションの途中で出てきて保存していないデータなどを復元したいときなどに使える。
ちなみに、DBに保存しないデータは新たにlogファイルを作ってそこに入れるようにしてあげれば良い。

# config/development.rbに追記
   # another_loggerメソッドを定義。yasagori.logに吐き出す
  config.another_logger = Logger.new(Rails.root.join('log/yasagori.log'))

# hoge_controller.rbに追記
  def create
    ~~~
    piyo = ~~~  # DBに保存されないpiyo変数を見たい
    Rails.application.config.another_logger.info(piyo)
  end

上の場合はpiyo変数に入った値がyasagori.logにごっそり出てくる。

まとめ

DBやlogに残らないデータを復元しようとすると、

  • logから入力データを復活
  • アクションを再び実行
  • DBに保存されないから、logファイルを別に作って吐き出す

という手間がかかるので、初期段階で必要になりそうなものはDBに保存するようにするべき。

【Linux】該当文字列を含むファイルを探すgrepコマンド

ことの始まり

サーバー上に残っているLOGから該当の処理を探す必要が出た。
普段サーバーの中からデータを漁ることなんてしなかったのでめちゃくちゃ手間取ったので、忘れないようにメモを残しておく。

grepコマンド

grepの基本的な使い方はコレ。

grep 正規表現文字列 ファイル名

例えば~/Projects/hogeディレクトリ内にあるyasagori.txtファイルからfugaという文字列を含む行探したければこうすればいい。

grep 'fuga' ~/Projects/hoge/yasagori.txt

ここでのfugaは正規表現で、文字列を含む行を検索している。
1行に書かれている文字列がわかっているなら、シングルクォートは不要。
grepには色々なオプションが存在するので組み合わせると便利。

複数条件で検索

fugapiyoの2つの文字列を含む行を探すには、パイプで繋いであげれば良い

grep 'fuga' yasagori.txt | grep 'piyo'

含まない検索

'fuga'は含むがpiyoは含まない行を探すには-vオプションを使う。

grep 'fuga' yasagori.txt | grep -v 'piyo'

複数ファイルから検索

指定のディレクトリ以下にあるファイル・ディレクトリから検索をしたいときは、次のように書いてあげれば良い。

# Projects以下にあるファイルから検索
grep 'fuga' ~/Projects/*

grepと一緒に使うと便利なコマンド

コレだけでもかなり便利なコマンドだが、組み合わせるとさらに便利な使い方ができる。

findコマンド

findはファイルを検索するコマンド。
grepと組み合わせることで「ディレクトリ以下にある一致する文字列を含むファイルから該当の行」をそれぞれ出すことができるようになる。

# Projectsディレクトリ内にある全てのファイル(さらに一階層したのディレクトリ内とかも)から探す
grep 'fuga' `find ~/Projects/* -type f`

検索条件が1つならfindを先頭に持ってきたこの書き方の方が便利。

find ~/Projects -type f -exec grep "fuga" {} \;

便利な理由は、「出力結果にフォルダ名とかを出さないで済む」から。

wcコマンド

wcコマンドは行数・単語数・バイト数を数えるコマンド。
grepした結果が多い場合、コレを使うことで該当箇所がいくつあるのかわかる。

grep 'fuga' `find ~/Projects/* -type f` | grep -v 'piyo' | wc
=>   33695 2462550 53006581
# この場合該当箇所が33695行あるということ。

teeコマンド

teeは出力しながら指定したファイルに書き出すコマンド。
grepで複数条件検索すると通常の>で書き出せないのでコレを使った。

pwd
=> ~/Projects
grep 'fuga' `find ~/Projects/* -type f` | grep -v 'piyo' | tee yasagori_2.txt
# ~/Projects/yasagori_2.txtが新たに作成されてgrepの検索結果が中に保存される

まとめ

普段あまり使ってこなかったgrepコマンド、調査とかを行うときはめっちゃ便利。
というか使えないと話にならない…

【rails】rspecでcontrollerのconcern単体テストをやる方法

ことの始まり

巨大なcontrollerを分割した際、共通で使うメソッドはconcernにまとめておこうと考え。
controllerの単体テストでわざわざconcernの内容も一緒にやるのは馬鹿らしいので、concernの単体テストをやりたくなったが、ちょっと困ったので色々調べた。
せっかくなので、忘れないようにまとめておこう。

ベースとなるconcern

今回作ったのはこんな感じのconcern。

module HogeConcern
  extend ActiveSupport::Concern

  def fuga(order)
    # ハッシュを返し、nameキーに'maccho_'をつける
  end

  def piyo
    piyo_params = params.permit(~~) # permitの中は省略
    # 頭に'maccho_'をつけたstrを返す
  end
end

fugaメソッドは引数にorderを持っている。
一方で、piyoメソッドはpost時に渡されたparamsをそのまま使っている。

引数がある場合

まずは引数がある場合から。
とりあえずテストのベースとなる部分を作っておく。

require 'rails_helper'

describe HogeConcern do
  context '.fuga' do
    let(:order) do
      FactoryBot.buid(:order, {name: hoge_name})
    end
    
    subject { fuga(order) }
    
    context 'hoge_name' do
      let(:hoge_name) { 'yasagori' }
      it { is_expected.to eq({ name: 'maccho_yasagori' }) }
    end
  end
end

当然、このままだと通らない。
まずは、structを使ってテスト用のクラスを作る。
クラスを作る際に、HogeConcernを忘れずにincludeしておく。
そして、作成したクラスをnewする。

  context '.fuga' do
    let(:test_class) { Struct.new(:concern) { include HogeConcern } }
    let(:concern) { test_class.new }
    let(:order) do
      FactoryBot.buid(:order, {name: hoge_name})
    end
    
    subject { concern.fuga(order) }
    
    context 'hoge_name' do
      let(:hoge_name) { 'yasagori' }
      it { is_expected.to eq({ name: 'maccho_yasagori' }) }
    end
  end

concern自体はcontrollerにincludeして使われているので、単純にメソッドを呼び出しても使えない、ということ。
なのでstructを使って擬似的にクラスを作ってあげてからincludeすることでメソッドを呼び出せるようになる。
呼び出すときはsubjectのような書きっぷりが必要(メソッドの呼び出しだから)

post parameterを使う場合

問題はこっち。
controllerがないのにどうやってpost parameterを作ればいいんだ…
結論を先に言うと、擬似的にcontrollerを作ってあげる必要があった。

describe HogeConcern, type: :controller do # typeをcontrollerにする
  context '.piyo' do
    controller(ApplicationController) do
      include HogeConcern

      def fake_action
        @piyo = piyo
      end
    end

    before { routes.draw {
      post 'fake_action' => 'anonymous#fake_action'
    } }

    subject { controller.instance_variable_get("@piyo") }
    
    context 'piyo_str' do
      before { post :fake_action, params: { name: 'yasagori' } }
      it { is_expected.to eq('maccho_yasagori') }
    end
  end
end

まずcontrollerを設定する。
concernのテストをしたいので、includeを忘れずに。
その中で、呼び出すメソッドを定義しておく。
定義したメソッドはroutes.drawでrouteの定義付をしてあげる。
subjectはメソッドの中で定義したインスタンス変数を呼び出すためcontroller.instance_variable_getを使った。
これで、controllerテストの中でconcernのテストを行う必要がなくなる。

まとめ

結果的にconcernの単体テストでpost parameterを呼び出そうとすると、controllerの設定が必要になってしまった。
が、それぞれのcontrollerのなかでテストを行う必要がなくなったので、最終的なコードは短くまとめられ、すっきりさせられた。

【ruby】rubyでよく使うメソッドまとめ その3

よく使うメソッド

実業務でもたまに使うけど、どっちかっていうとAtCorderの問題でよく使うやつをまとめておく。
今までのまとめはこちら

yasagori-programing.hatenablog.jp

文字の出現回数を数えたい

countメソッド

文字列の中から特定の文字をの出現回数を数えるなら'count'メソッドで十分。

s = 'hogehoge'
p s.count('h')
=> 2

じゃあ文字列もってやるとおかしなことになる。

s = 'hogehoge'
p s.count('hoge')
=> 8   # 絶対違う

そんな時はscanメソッドを使う。

scanメソッド

文字列の中から特定の文字列を抽出し、その配列を返す。
返されるのが配列なので、出現回数を数えるならlengthを使えばOK。

s = 'hogehoge'
p s.scan('ho')
=> ["ho", "ho"]
p s.scan('ho').length
=> 2

配列の処理

任意の数字の配列

1からnまでの数字が入った配列を作るにはこんな書き方ができる。

arry = (1..n).to_a
arry = [*1..n]

下の方がシンプルで良いかも。

ダブりをなくす

配列の中から重複をなくすにはuniqメソッドを使えば良い。

arry = [1, 1, 2, 2, 3]
p arry.uniq
=> [1, 2, 3]

順列・組合せ

permutationメソッド

ある配列の中身の順列を求めるにはpermutationメソッドを使うと便利。
そのままだとEnumeratorオブジェクトが返ってきてしまうので、.to_aで返り値を配列に入れる等の処理が必要。

arry = [1, 2, 3]
p arry.permutation.to_a
=> [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
p arry.permutation(2).to_a  #順列の個数を指定できる
=> [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]

combinationメソッド

順列ではなく組合せを求めるにはcombinationメソッドを使えば良い。
こちらもEnumeratorオブジェクトが返ってきてしまうので、.to_a等をつける必要がある。

arry = [1, 2, 3, 4]
p arry.combination(3).to_a
=> [[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]]

配列の何番目かを知りたい

indexメソッド

条件に一致するものが指定の配列の何番目(index)かを調べるメソッド。

arry = [1, 2, 3]
lists = arry.permutation.to_a
# lists = [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
p lists.index([2,1,3])
=>2  # index番号を返している

数が増えてくるとindexは時間がかかるので、bsearch_indexを使うと良い。

bsearch_indexメソッド

やることはindexと同じだが、二部探索を行うので処理時間が短く済む。
一方で、bsearchするため事前にsortしておく必要がある。

arry = [1, 2, 3]
lists = arry.permutation.to_a
p lists.bsearch_index([2,1,3])
=>2 

繰り返し処理

injectメソッド

rubyでよく使うメソッドまとめ その1でも出てきたが、繰返し処理をスタイリッシュに書くならinjectメソッドが便利

# 配列オブジェクト.inject {|初期値, 要素| ブロック処理 }
nums = [1, 2, 3, 4]
p nums.inject {|sum, num| p sum += num }
=> 1, 3, 6, 10と順に表示
p nums.inject(5) {|sum, num| p sum += num }  # 初期値を指定
=> 6, 8, 11, 15と順に表示
# 最後の結果だけ取り出すならシンボル表記でOK
p nums.inject(:+)
=> 10
p nums.inject(5, :+)  # 初期値を指定
=> 15

最小公倍数・最大公約数

最小公倍数

プロコンでよく出てくるが、lcmメソッドを使えば一発。
複数の値の最小公倍数を使いたい時はinjectメソッドと組み合わせれば良い。

p 3.lcm(7)
=> 21  # 3と7の最小公倍数
nums = [2, 3, 7]
p nums.inject(:lcm)
=> 42

最大公約数

gcdメソッドを使えば一発。

p 25.gcd(30)
=> 5
nums = [1000, 20000, 200]
p nums.inject(:gcd)
=> 200

まとめ

複数の値の最小公倍数を求めたいときにinjectと組合せると、めちゃくちゃ楽に出すことができる。
処理時間もそこまでかからないし、これは便利。