開発者ブログ by playground

playgroundの技術系ブログ

Rubyistの長年の悩み「RSpecのlet!が紛らわしい」を解決した話

こんにちは、playgroundエンジニアの海老沼です。

最近Angularをよく書くようになって、Railsではなかなか味わえないSPA(Single Page Application)の良さがだんだんわかってきました。

ebkn12 (Kenichi Ebinuma) · GitHub


Railsのテストを書くためのgemには代表的なものとしてminitestやRSpecがありますが、playgroundではRSpecを採用しています。

皆さん、let!使っていますか?

letlet!を同時に使うとゲシュタルト崩壊しそうになりますよね。

そんな悩みを、今回はrubyのaliasを使うことで解決しました。


まず最初に軽くletlet!について解説します。

letは普通の定義と違い、呼び出したときに初めて処理が実行されます(遅延評価と呼ばれることが多いです)。

例えばこんな感じの例がわかりやすいです。

describe 'test' do
  let(:user) { create(:user) }
  let(:post) { create(:post, user: user) }

  it { expect(user.posts.first).to eq(post) }
end
Failure/Error: it { expect(user.posts.first).to eq(post) }
expected: #<Post .....>
     got: nil

user.postsが呼ばれた時点ではまだpostを実行していないので、まだcreate(:post, user: user)は実行されていません。よって、user.posts.firstは[]になってしまいます。

上記の問題を解決するためによく使われるのがlet!で、これは遅延評価にならず、普通の変数定義と同じような間隔で使うことができます。

上記の例では、

let(:post) { create(:post, user: user) }
↓
let!(:post) { create(:post, user: user) }

このように記述することでテストが通るようになります。

ただこの使いわけをしたときに、letとlet!での定義が多くなってきて混在してくるとわかりにくくなりますよね。

そこで、playgroundではlet!makeとして扱えるようにしました。

makeという名前は、使役の役割をもつlet O Vmake O Vから持ってきました。

make = 強制的にさせるlet = 許可するというニュアンスをもつため、意味的にもletlet!の関係に近いからです。

では、その方法について書いていきます。


実装するためにRubyのaliasメソッド(alias_methodでも可)を使いました。

使い方は簡単で、alias 新しい名前 元の名前とするだけです。

(例)

class Dog
  def speak
    puts 'Bow wow'
  end

  alias bark speak
end

dog = Dog.new
dog.speak
#=> "Bow wow"

dog.bark
#=> "Bow wow"

これを使い、let!に対してmakeをエイリアスとして設定する、ということをやっていきます。

まず、let!がどこで定義されているのかを調べる必要があります。

探してみたらここにありました。

github.com

def let!(name, &block)
  let(name, &block)
  before { __send__(name) }
end

重要なのはこれがどのClass、Moduleに定義されているかです。

見てみると、RSpec::Core::MemoizedHelpers::ClassMethodsmoduleに定義されていました。

同じ位置にaliasを記述するとこんな感じになります。

module RSpec
  module Core
    module MemoizedHelpers
      module ClassMethods
        alias make let!
      end
    end
  end
end  

ファイル名はspec/lib/alias_make.rbとしました。

この場合spec/lib以下を読み込む必要があるので、spec/rails_helper.rbに以下のように追記します。

Dir[Rails.root.join('spec/lib/**/*.rb')].each { |f| require f }

これでmakeをlet!の代わりに使用できるようになりました。

比べてみても、let!よりmakeのほうがわかりやすいですね

# let!を使った場合
let(:user)    { create(:user) }
let!(:post)   { create(:post, user: user) }
let(:like)    { create(:like, user: user, post: post) }
let(:comment) { create(:comment, user: user, post: post) }
# makeを使った場合
let(:user)    { create(:user) }
make(:post)   { create(:post, user: user) }
let(:like)    { create(:like, user: user, post: post) }
let(:comment) { create(:comment, user: user, post: post) }

最後まで読んでいただき、ありがとうございました!