woshidan's loose leaf

ぼんやり勉強しています

少しだけ動作が速くなるコードの書き方3つ

レコードがあることを条件にしたいとき、findの代わりにexists?を使う

if User.find_by(email: "...")
  # something.todo
end

のとき、findやfind_byではActiveRecordインスタンスが生成されてしまう分遅いので、存在確認をしたいだけなら下記のようにexistsを使うとよいようです。

if User.exists?(email: "...")
  # something.todo
end

speakerdeck.com

多数のレコードを処理するとき、eachではなく、find_in_batchesを使って一度に生成するインスタンス数を制限する

User.all.each do |user|
  # something.todo
end

としたいとき、

User.find_in_batches(of: 100) do |users|
  users.each do |user|
    # something.todo
  end
end

qiita.com

正規表現にマッチするかどうか調べるとき、=~よりmatch?の方が速い

str =~ /regexp/

より

str.match?(/regexp/)

speakerdeck.com

Railsのuniquenessバリデーションについて

uniquenessバリデーションを使うと、その項目の中で一意であるようにバリデーションがかけられます。

class User
  validates :email, uniqueness: true
end

とすると、emailはUserの中で一意となるようにRails側でバリデーションをかける。

これを、Userが会社に属していて会社の中で一意になるようにバリデーションをかけようとすると

class User
  validates :email, uniqueness: { scope: :company_id }
end

のように、scopeを使って、一意になる範囲を指定する。これをさらに、(例がいいかは少しおいておいて)会社の中で複数の部署に属するユーザはその都度User登録しようとかいう話になって、ある会社のある部署の中で一意というのが書きたいときは、

class User
  validates :email, uniqueness: { scope: [:company_id, :group_id] }

のように、scopeに配列を渡す形となります。

参考

railsguides.jp

310nae.com

RSpecでテストデータ用にファイルを読み込む

RSpecでテスト用のファイルを読み込む

RSpecでテスト用のファイルを読み込む際はデフォルトでは spec/fixtures/files にファイルを置いて

file_fixture("example.txt").read

とすると、ファイルの中身がspec中で呼び出せる。

ファイル中にHTMLやJSONのレスポンスを入れておいてHTTPリクエストをモックしたい場合などに使える。

既にfixturesに他にファイルがあってそれらに合わせてファイルの場所を既定のパスから変更したい場合は

RSpec.configure do |config|
  config.file_fixture_path = "spec/custom_directory"
end

で可能となる。

RSpecでファイルをアップロードする

テストデータでファイルを使いたい状況としては、HTTPリクエストのモック以外にもファイルのアップロードのテストなどがある。 その場合は fixture_file_upload を以下のように使う。

post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png')

このメソッドで使うファイルのパスは config.file_fixture_path ではなく、config.fixture_path で指定するので注意。

参考

relishapp.com apidock.com qiita.com

配列の要素全てが該当する条件を記述するallマッチャ

scopeみたいにフィルタリングした要素について、フィルタリング対象の要素について

let(:included) { Item.new(price: 250) }
let(:not_included) { Item.new(price: 300) }
let(:array) { [included, not_included] }

subject(array.filtering)

it '250円以下の品物が絞り込まれる' do
  expect(subject).to contain_exactly(included)
  expect(subject).not_to contain_exactly(not_included)
end

のように、フィルターの境界値の値を持つ要素をフィルタリング前のコレクションに含めておいて、その要素が含まれる/含まれない、といった形でspecを書いていくこともできますが、allマッチャを使うことで

let(:sample1) { Item.new(price: 250) }
let(:sample2) { Item.new(price: 300) }
let(:array) { [sample1, sample2] }

subject(array.filtering)

it '250円以下の品物が絞り込まれる' do
  expect(subject.map(&:price)).to all(be <= 250)
  expect(subject.count).to eq 1
end

元の要素ではなく、絞り込まれた結果に注目してspecを書くことができます。

参考

relishapp.com

Railsのルーティングの細かいところについて

最近細かく引っかかっていたので。

URLヘルパのnew, editの修飾子は前に付く

xxxx_new_pathかnew_xxxx_pathかよく迷っていたけど、

new_xxxx_path, edit_xxxx_path

のほうが正解。namespaceで

# routes.rb
namespace 'admin' do
  resources xxxxs
end

となって、xxxxの前にadminが付くようになっても

new_admin_xxxx_path, edit_admin_xxxx_path

とnew, editが前に付く。

namespaceとscopeの作るパスとアクションの対応の違い

# routes.rb
namespace 'admin' do
  resources xxxxs
end

とすると、

コントローラのクラスのアクション パス名 名前付きルーティングヘルパー
Admin::XxxxsController#index admin/xxxxs admin_xxxxs_path

という風になるが、

# routes.rb
scope 'admin' do
  resources xxxxs
end

とすると、

コントローラのクラスのアクション パス名 名前付きルーティングヘルパー
XxxxsController#index admin/xxxxs xxxxs_path

という風に、コントローラや名前付きルーティングヘルパーはそのままでパスだけ admin がつくようになる。

配列でのパス指定

<%= link_to 'Edit Xxxx', edit_xxxx_path(@xxxx) %>

というのを

<%= link_to 'Edit Xxxx', [:edit, @xxxx] %>

のように書ける。これは、

form_for [:admin, @article]

のように、名前空間の中に入っているパスをform_forで指定したい場合に覚えておくと便利な気がする。

参考

railsguides.jp

railsguides.jp

テスト中のオブジェクトに実際の動作をさせないで返り値を指定する

特定のオブジェクトにあるメソッドの中身を実行せず結果だけ返して欲しい場合

allowとreceiveを使ってスタブの指定をします。

allow(some_object).to receive(:method_name).and_return(return_value)

特定のクラスのインスタンス全てにあるメソッドの中身を実行せず結果だけ返して欲しい場合

allow_any_instance_of(ClassName).to receive(:method_name).and_return(return_value)

以下のようなコントローラのアクションに対するテストなどでテストコードではないところで生成するインスタンスに対して

# コントローラのコード
def index
  api_client = SomeApiClient.new(token)
  @result = api_client.search # 外部APIへリクエストを飛ばす
end
# テストコード
allow_any_instance_of(SomeApiClient).to receive(:search).and_return(["foo", "bar"])

といった具合に使います。

引数があるメソッドの場合に引数を指定したい場合

メソッドの呼び出しに伴う引数を指定したい場合はwithを使います。

allow(some_object).to receive(:method_name).with(arg).and_return(return_value)

複数の引数を指定したい場合は

allow(some_object).to receive(:method_name).with(arg1, arg2, ...).and_return(return_value)

どの引数を受け取ってもいい場合は

allow(some_object).to receive(:method_name).with(anything()).and_return(return_value)

この引数にはハッシュの一部だけを指定したりいろんなバリエーションがあります。

relishapp.com

例外を起こしたい場合

and_raiseを使います。

allow(some_object).to receive(:method_name).and_raise(SomeError.new)

参考

relishapp.com

ActiveRecordで関連しているモデルに対してメソッドを定義する

Userと1対多関連の関係にあるCommentというモデルがあるとする。

あるUserに関連したコメントに対して user.commentes.some_method のようにスコープやメソッドを定義したいという場合があって、その場合以下のように書ける。

class User < ApplicationRecord
  has_many :comments do # has_many関連を定義した部分にブロックを渡すとuser.comments.shortest_commentのように書ける
    def shortest_comment
      self.order(Arel.sql('length(contents) asc')).first
    end
  end
end
class Comment < ApplicationRecord
  belongs_to :user

  def self.longest_comment # クラスメソッドでallを使うと呼び出された時点でのhas_many関連などの上にメソッドチェインをつなげることができる
    all.order(Arel.sql('length(contents) desc')).first
  end
end

確認用のテストコード

RSpec.describe Comment, type: :model do
  let(:user) { create(:user) }
  let!(:comment1) { create(:comment, contents: '' * 3, user: user)}
  let!(:comment2) { create(:comment, contents: '' * 5, user: user)}
  let!(:another_user_comment) { create(:comment, contents: '' * 7, user: create(:user)) }
  let!(:other_user_comment) { create(:comment, contents: '' * 1, user: create(:user)) }

  describe '.longest_comment' do
    it 'userのコメントの中で一番長いコメントが返ってくる' do
      expect(user.comments.longest_comment.contents).to eq comment2.contents
    end
  end

  describe '.shortest_comment' do
    it 'userのコメントの中で一番短いコメントが返ってくる' do
      expect(user.comments.shortest_comment.contents).to eq comment1.contents
    end
  end
end

参考

qiita.com