RSpec let()を使用する場合


447

beforeブロックを使用してインスタンス変数を設定する傾向があります。次に、これらの変数を私の例全体で使用します。最近出会いましたlet()。RSpecのドキュメントによると、それは

...メモ化されたヘルパーメソッドを定義します。値は、同じ例の複数の呼び出しにわたってキャッシュされますが、例全体ではキャッシュされません。

これは、beforeブロックでインスタンス変数を使用する場合とどう違うのですか?また、いつlet()vs を使用する必要がありますbefore()か?


1
ブロックが各サンプルの前に実行される間、ブロックは遅延評価されます(それらは全体的に遅いです)。beforeブロックの使用は個人的な好み(コーディングスタイル、モック/スタブなど)によって異なります。Letブロックが通常推奨されます。letの
Nesha Zoric

beforeフックでインスタンス変数を設定することはお勧めできません。betterspecs.orgを
アリソン

回答:


604

私は常にletいくつかの理由でインスタンス変数を好む:

  • インスタンス変数は、参照されると存在するようになります。つまり、インスタンス変数のスペルを太った場合、新しい変数が作成されてに初期化されnil、微妙なバグや誤検知につながる可能性があります。letメソッドを作成するので、NameErrorスペルを間違えるとを取得します。スペックのリファクタリングも簡単になります。
  • before(:each)フックは例がフックで定義されたインスタンス変数のいずれかを使用しない場合でも、それぞれの例の前に実行されます。これは通常大したことではありませんが、インスタンス変数の設定に長い時間がかかる場合は、サイクルを浪費しています。によって定義されたメソッドのlet場合、初期化コードは、例がそれを呼び出した場合にのみ実行されます。
  • 例の参照構文を変更せずに、例のローカル変数からletに直接リファクタリングできます。インスタンス変数にリファクタリングする場合、例でオブジェクトを参照する方法を変更する必要があります(例:を追加@)。
  • これは少し主観的ですが、Mike Lewisが指摘したように、仕様を読みやすくしていると思います。私は、すべての依存オブジェクトを定義してletitブロックを簡潔かつ簡潔に保つ構成が好きです。

関連リンクはここにあります:http : //www.betterspecs.org/#let


2
私はあなたが言及した最初の利点が本当に好きですが、3番目の利点についてもう少し説明してもらえますか?これまでに見た例(mongoid仕様:github.com/mongoid/mongoid/blob/master/spec/functional/mongoid/…)は単一行のブロックを使用しており、「@」がないとどうなるのかわかりません読みやすい。
sent-hil

6
先ほど述べたように、これは少し主観的ですが、letすべての依存オブジェクトを定義し、before(:each)必要な構成または例で必要なモック/スタブをセットアップするために使用すると便利です。私はこれをすべて含む1つの大きなbeforeフックよりもこれを好みます。また、let(:foo) { Foo.new }ノイズが少なくなります(要点はさらに高くなります)before(:each) { @foo = Foo.new }。使用方法の例を次に示します。github.com
Myron Marston

例をありがとう、それは本当に役に立ちました。
send-hil

3
Andrew Grimm:真実ですが、警告は大量のノイズを生成する可能性があります(つまり、使用している宝石から、警告なしで実行できない)。さらに、私NoMethodErrorは警告を受け取るよりもを受け取ることを好みますが、YMMVです。
Myron Marston

1
@ Jwan622:最初に1つの例を書くことから始めます。この例にはfoo = Foo.new(...)foo後の行にユーザーがいます。その後、同じ例のグループで新しい例を記述しFooますが、これも同じ方法でインスタンス化する必要があります。この時点で、重複を排除するためにリファクタリングする必要があります。foo = Foo.new(...)例から行を削除してlet(:foo) { Foo.new(...) }、例の使用方法を変更しないw / oで置き換えることができますfoo。しかし、あなたがにリファクタリングあればbefore { @foo = Foo.new(...) }、あなたもからの例の参照を更新する必要があるfooのを@foo
マイロンマーストン

82

インスタンス変数を使用したとの違いlet()それでlet()あるレイジーに評価。これは、let()それが定義するメソッドが初めて実行されるまで評価されないことを意味します。

との違いはbefore、変数のグループを「カスケード」スタイルで定義する優れた方法letlet()提供することです。これを行うことにより、コードを簡略化することで、スペックが少し良くなります。


1
ええ、それは本当に有利なのでしょうか?コードは、例に関係なく実行されています。
send-hil

2
IMOの方が読みやすく、読みやすさはプログラミング言語の大きな要素です。
Mike Lewis

4
Senthil-let()を使用する場合、すべての例で実際に実行されるとは限りません。怠惰なので、参照されている場合にのみ実行されます。サンプルグループのポイントは、いくつかのサンプルを共通のコンテキストで実行させることなので、一般的にはこれはそれほど重要ではありません。
David Chelimsky、2011

1
だから、let毎回何かを評価する必要があるなら、それを使うべきではないということですか?たとえば、親モデルでいくつかの動作がトリガーされる前に、データベースに子モデルが存在する必要があります。親モデルの動作をテストしているため、テストでその子モデルを参照しているとは限りません。現在はlet!代わりにメソッドを使用していますが、その設定を入れる方がより明示的でしょうbefore(:each)か?
ガー

2
@gar-親をインスタンス化するときに必要な子の関連付けを作成できるFactory(FactoryGirlなど)を使用します。この方法で行う場合、let()を使用しても、セットアップブロックを使用しても、問題にはなりません。サブコンテキストの各テストでEVERYTHINGを使用する必要がない場合は、let()が便利です。セットアップには、それぞれに必要なものだけが含まれている必要があります。
Harmon

17

rspecテストでインスタンス変数のすべての使用法を完全に置き換えて、let()を使用しました。小さなRspecクラスを教えるためにそれを使用した友人のための簡単な例を書きました:http ://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html

ここでの他の回答のいくつかが言うように、let()は遅延評価されるため、ロードが必要なものだけをロードします。仕様を乾燥させ、読みやすくします。実際、Rspec let()コードをコントローラーで使用できるように、inherited_resource gemのスタイルで移植しました。http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

遅延評価に加えて、もう1つの利点は、ActiveSupport :: Concern、およびload-everything-in spec / support /動作と組み合わせて、アプリケーションに固有の独自の仕様mini-DSLを作成できることです。RackおよびRESTfulのリソースに対してテストするためのものを作成しました。

私が使用する戦略は、ファクトリーエブリシング(Machinist + Forgery / Faker経由)です。ただし、before(:each)ブロックと組み合わせて使用​​して、サンプルグループのセット全体のファクトリをプリロードし、仕様をより高速に実行することができます:http : //makandra.com/notes/770-taking-advantage -of-rspec-s-let-in-before-blocks


Hey-Shengさん、この質問をする前に、実際にいくつかのブログ投稿を読みました。あなた# spec/friendship_spec.rb# spec/comment_spec.rb例について、彼らはそれを読みにくくしていると思いませんか?私はusersどこから来たのかわからないので、深く掘り下げる必要があります。
sent-hil

最初から数十人が私がこのフォーマットをもっと読みやすくするために私が示した人々、そしてその数人はそれを使って書き始めました。let()を使用して十分なスペックコードを取得したので、これらの問題のいくつかにも遭遇しました。私はこの例に行き、最も内側の例グループから始めて、自分自身を元に戻します。これは、高度にメタプログラマブルな環境を使用するのと同じスキルです。
Ho-Sheng Hsiao

2
私が遭遇した最大の落とし穴は、誤って件名{}ではなくlet(:subject){}を使用することです。subject()の設定はlet(:subject)とは異なりますが、let(:subject)はそれをオーバーライドします。
Ho-Sheng Hsiao

1
コードを「掘り下げる」ことができる場合は、let()宣言を使用してコードをスキャンするほうがはるかに高速です。コードに埋め込まれている@variablesを見つけるよりも、コードをスキャンするときにlet()宣言を選択する方が簡単です。@variablesを使用すると、どの行が変数への割り当てを参照し、どの行が変数のテストを参照するかについて、適切な「形状」がありません。let()を使用すると、すべての割り当てがlet()で行われるため、宣言が置かれている文字の形状によって「瞬時に」知ることができます。
Ho-Sheng Hsiao

1
特に、鉱山(gedit)などの一部のエディターはインスタンス変数を強調表示するため、インスタンス変数について同じ引数を簡単に選択できます。私はlet()過去数日間使用しており、個人的にはMyronが述べた最初の利点を除いて、違いは見られません。そして、私は手放すことについてはあまり確信がありません。おそらく私は怠惰であり、さらに別のファイルを開かなくてもコードを前もって見るのが好きだからです。コメントしてくれてありがとう。
send-hil

13

letは遅延評価され、副作用メソッドを入れないことを覚えておくことが重要です。そうしないと、letからbefore(:each)に簡単に変更できなくなります。レット使用できますletの代わりに、各シナリオの前に評価されます。


8

一般に、let()はより優れた構文であり、@name記号を入力する手間が省けます。しかし、注意を払う必要があります!私を発見したlet()追加場合:あなたはそれを使用しようとするまで、変数が実際に存在していないので...物語記号を伝えるにも微妙なバグ(あるいは少なくとも頭掻く)を導入putsした後、let()変数が正しいことを確認するためにはスペックを可能に合格しputsますが、仕様がなければ失敗します-この微妙な点を見つけました。

またlet()、すべての状況でキャッシュされないようです。私はブログにそれを書きました:http : //technicaldebt.com/?p=1242

多分それは私だけですか?


9
let常に1つの例の期間中の値を記憶します。複数の例の値をメモすることはありません。before(:all)対照的に、では、初期化された変数を複数の例で再利用できます。
Myron Marston

2
letを使用したい場合(現時点ではベストプラクティスと見なされているようです)、特定の変数をすぐにインスタンス化する必要がある場合は、それlet!が目的です。 relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/...
ジェイコブ

6

letは本質的にProcとして機能します。また、そのキャッシュ。

letですぐに見つけた1つの問題...変更を評価しているSpecブロックで。

let(:object) {FactoryGirl.create :object}

expect {
  post :destroy, id: review.id
}.to change(Object, :count).by(-1)

あなたはletあなたのexpectブロックの外で呼び出すことを確認する必要があります。つまりFactoryGirl.create、letブロックで呼び出しています。私は通常、オブジェクトが永続化されていることを確認することでこれを行います。

object.persisted?.should eq true

そうしないと、letブロックが初めて呼び出されたときに、遅延インスタンス化のためにデータベースの変更が実際に発生します。

更新

メモを追加するだけです。この回答では、コードゴルフ、またはこの場合はrspecゴルフのプレーに注意してください。

この場合、オブジェクトが応答するメソッドを呼び出すだけです。したがって_.persisted?、オブジェクトの_メソッドをその真実として呼び出します。私がやろうとしているのは、オブジェクトをインスタンス化することだけです。あなたは空と呼ぶことができますか?またはnil?あまりにも。ポイントはテストではありませんが、それを呼び出すことによってオブジェクトに生命をもたらします。

だからあなたはリファクタリングできません

object.persisted?.should eq true

することが

object.should be_persisted 

オブジェクトがインスタンス化されていないので...その遅延。:)

アップデート2

レッツを活用この問題を完全に回避する必要がある、インスタントオブジェクト作成の構文。ただし、非バンレットレットの遅延の目的の多くは無効になります。

また、一部のインスタンスでは、追加のオプションが提供される場合があるため、letではなく実際に対象の構文を活用したい場合があります。

subject(:object) {FactoryGirl.create :object}

2

ジョセフへの注意-データベースオブジェクトを作成している場合、before(:all)それらはトランザクションでキャプチャされず、テストデータベースに残骸を残す可能性が高くなります。before(:each)代わりに使用してください。

letとその遅延評価を使用するもう1つの理由は、この非常に不自然な例のように、コンテキストでletをオーバーライドすることにより、複雑なオブジェクトを取得して個々の部分をテストできるようにするためです。

context "foo" do
  let(:params) do
     { :foo => foo,  :bar => "bar" }
  end
  let(:foo) { "foo" }
  it "is set to foo" do
    params[:foo].should eq("foo")
  end
  context "when foo is bar" do
    let(:foo) { "bar" }
    # NOTE we didn't have to redefine params entirely!
    it "is set to bar" do
      params[:foo].should eq("bar")
    end
  end
end

1
以前の+1(:all)バグは、開発者の多くの時間を浪費してきました。
Michael Durrant 2014年

1

デフォルトの「前」はを意味しますbefore(:each)。Ref The Rspec Book、copyright 2010、page 228。

before(scope = :each, options={}, &block)

「it」ブロックにデータを作成するメソッドをbefore(:each)呼び出さなくても、サンプルグループごとにデータをシードするために使用しletます。この場合、「it」ブロック内のコードは少なくなります。

letいくつかの例で一部のデータが必要な場合に使用します。

beforeとletはどちらも「it」ブロックを乾燥させるのに最適です。

混乱を避けるために、「let」はと同じではありませんbefore(:all)。「Let」は、各例(「it」)のメソッドと値を再評価しますが、同じ例の複数の呼び出しにわたって値をキャッシュします。詳しくは、https//www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-letをご覧ください。


1

ここで反対意見:5年のrspecの後、私はあまり好きではありませんlet

1.遅延評価はテストのセットアップを混乱させることが多い

セットアップで宣言されているものの一部が実際には状態に影響を与えていない場合、セットアップに理由を付けるのは難しくなります。

結局、欲求不満から、誰かが仕様を機能させるために(遅延評価なしで同じこと)に変更letlet!ます。これがうまくいくと、新しい習慣が生まれます:新しい仕様が古いスイートに追加されて機能しない場合、作者が最初に試みるのはランダムなlet呼び出しに前髪を追加することです。

間もなく、パフォーマンス上のメリットはすべてなくなります。

2.特別な構文は、rspecを使用していないユーザーには珍しい

私は、rspecのトリックよりも、Rubyをチームに教えたいと思っています。インスタンス変数またはメソッド呼び出しは、このプロジェクトおよび他のあらゆる場所で役立ちletます。構文は、rspecでのみ役立ちます。

3.「メリット」により、優れた設計変更を簡単に無視できます

let()何度も作成したくない高価な依存関係に適しています。またsubject、と適切にペアリングされ、複数の引数を持つメソッドへの繰り返しの呼び出しをドライアップできます。

高価な依存関係が何度も繰り返され、署名が大きいメソッドは、どちらもコードを改善できるポイントです。

  • おそらく、依存関係を残りのコードから分離する新しい抽象化を導入できます(つまり、必要なテストの数が少ないということです)
  • テスト中のコードが多すぎる
  • プリミティブの長いリストではなく、よりスマートなオブジェクトを注入する必要があるかもしれません
  • 多分私は教えないでくださいの違反があります
  • おそらく、高価なコードをより速くすることができます(まれに-ここで時期尚早な最適化に注意してください)

これらすべてのケースで、rspecマジックの鎮静剤を使用して困難なテストの症状に対処するか、原因に対処することができます。過去数年間を前者に費やしすぎたような気がしますが、今はもっと良いコードが欲しいです。

元の質問に答えるには、私はしたくないのですが、それでも使用しますlet。私はほとんどの場合、チームの他のメンバーのスタイルに合わせるために使用します(世界中のほとんどのRailsプログラマーは今やrspecのマジックに深く関わっているようです)。制御できないコードにテストを追加するときや、より適切な抽象化にリファクタリングする時間がないとき、つまり、唯一のオプションが鎮痛剤であるとき、それを使用することがあります。


0

letコンテキストを使用して、API仕様でHTTP 404応答をテストするために使用します。

リソースを作成するには、を使用しますlet!。しかし、リソース識別子を保存するには、を使用しますlet。それがどのように見えるか見てみましょう:

let!(:country)   { create(:country) }
let(:country_id) { country.id }
before           { get "api/countries/#{country_id}" }

it 'responds with HTTP 200' { should respond_with(200) }

context 'when the country does not exist' do
  let(:country_id) { -1 }
  it 'responds with HTTP 404' { should respond_with(404) }
end

これにより、仕様がクリーンで読みやすくなります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.