【Rspec】letで作成したデータが無い???

Categories:Ruby on Rails

「あれ、rspecで書いた通るはずのテストが通らない・・?」

業務でrspecを書いた時に遭遇したこの事象。原因はlet / let!に関する理解の甘さでした。n番煎じではありますが、備忘としてまとめておきたいと思います。

let

letはrspecリポジトリにおいて、ClassMethodsモジュール下に実装されています。

      module ClassMethods
        ...
        def let(name, &block)
          # We have to pass the block directly to `define_method` to
          # allow it to use method constructs like `super` and `return`.
          raise "#let or #subject called without a block" if block.nil?

          # A list of reserved words that can't be used as a name for a memoized helper
          # Matches for both symbols and passed strings
          if [:initialize, :to_s].include?(name.to_sym)
            raise ArgumentError, "#let or #subject called with reserved name `#{name}`"
          end

          our_module = MemoizedHelpers.module_for(self)

          # If we have a module clash in our helper module
          # then we need to remove it to prevent a warning.
          #
          # Note we do not check ancestor modules (see: `instance_methods(false)`)
          # as we can override them.
          if our_module.instance_methods(false).include?(name)
            our_module.__send__(:remove_method, name)
          end
          our_module.__send__(:define_method, name, &block)

          # If we have a module clash in the example module
          # then we need to remove it to prevent a warning.
          #
          # Note we do not check ancestor modules (see: `instance_methods(false)`)
          # as we can override them.
          if instance_methods(false).include?(name)
            remove_method(name)
          end

          # Apply the memoization. The method has been defined in an ancestor
          # module so we can use `super` here to get the value.
          if block.arity == 1
            define_method(name) { __memoized.fetch_or_store(name) { super(RSpec.current_example, &nil) } }
          else
            define_method(name) { __memoized.fetch_or_store(name) { super(&nil) } }
          end
        end

詳しい処理の解説はこちらがわかりやすいですが、実装内容をざっくりまとめると「引数で渡されたnameを、いつでも呼び出された時に評価できるようにプログラムとして覚えておく」というイメージです。遅延評価と呼ばれる所以ですね。

let!

let!もletと同じくClassMethodsモジュールに実装されており、「letで名前を登録してbeforeですぐ評価!」という一種のラッパーメソッドになっています。先にも書いたように、letで宣言した値はそれが本当に必要とされる時まで評価が遅延されますが、let!はそのメソッド内に「本当に必要とされる時」を作ることで値の評価までまるっと行っているのですね。

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

つまり、テストの実行にあたって「ユーザー情報は前提条件として定義されていて欲しい!」というような場合は、let!で書いておく必要があります。私はそれを知らず、letを使って書いてハマりました^^;

参考

返信がありません

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です