少しだけ動作が速くなるコードの書き方3つ
レコードがあることを条件にしたいとき、findの代わりにexists?を使う
if User.find_by(email: "...") # something.todo end
のとき、findやfind_byではActiveRecordのインスタンスが生成されてしまう分遅いので、存在確認をしたいだけなら下記のようにexistsを使うとよいようです。
if User.exists?(email: "...") # something.todo end
多数のレコードを処理するとき、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
正規表現にマッチするかどうか調べるとき、=~よりmatch?の方が速い
str =~ /regexp/
より
str.match?(/regexp/)
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に配列を渡す形となります。
参考
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
で指定するので注意。
参考
配列の要素全てが該当する条件を記述する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を書くことができます。
参考
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で指定したい場合に覚えておくと便利な気がする。
参考
テスト中のオブジェクトに実際の動作をさせないで返り値を指定する
特定のオブジェクトにあるメソッドの中身を実行せず結果だけ返して欲しい場合
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)
この引数にはハッシュの一部だけを指定したりいろんなバリエーションがあります。
例外を起こしたい場合
and_raiseを使います。
allow(some_object).to receive(:method_name).and_raise(SomeError.new)
参考
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