LoginSignup
16
13

More than 5 years have passed since last update.

let, インスタンス変数, ローカル変数, 定数 in RSpec

Last updated at Posted at 2017-06-23

注意: 詳細な話は
RSpecのletを使うのはどんなときか?(翻訳)

「RSpec で example の外で定義したローカル変数を使うのはアリか?」に対する僕の見解と解決策
あたりを見てもらう方が早いです

別に目新しい何かを語るわけではないので結論から話すとletでいいぞっていう話です。

概要

レビュー時に「この変数をletで定義する意味がないと思う」というレビューをもらった。

describe 'test' do
  let :user_name do 'test' end
  let :password do 'password' end

  it 'username' do
    expect(User.new(user_name, password).user_name).to eq('test')
  end

  it 'password' do
    expect(User.new(user_name, password).password).to eq('password')
  end
end

とりあえず共通化できる変数はまとめてちゃえの精神の元,宣言していたのでいまいちletを使う
理由というを即答えることができなかった。
この規模のテストならこうもかけるし

describe 'test' do
  user_name='test'
  password='password'

  it 'username' do
    expect(User.new(user_name, password).user_name).to eq('test')
  end

  it 'password' do
    expect(User.new(user_name, password).password).to eq('password')
  end
end

こうもかける

describe 'test' do
  # eachでもいいしallでも良い
  before(:each)do
    @user_name='test'
    @password='password'
  end

  it 'username' do
    expect(User.new(@user_name, @password).user_name).to eq('test')
  end

  it 'password' do
    expect(User.new(@user_name, @password).password).to eq('password')
  end
end

正直動くしどれでもいい、脳死でletでええやんけ
さぁどれが良いのだろうか。

letの場合の挙動

letのドキュメントはこれかな?
ざっくりいうと「同じexample内は最初に呼ばれた値をキャッシュして使い回すよ。でも違うexampleの場合また呼び直すよ。(で同じexmaple内ではそれを使い回す)」
すなわちbefore(:each)的な動きをする。
当然it内で色々letの値をこねくり回して放置しても次のit内では新しくletないのblockで初期されるって寸法なわけだ。話の分かるやつだ。

インスタンス変数(before block)

beforeブロックで定義できる。

describe 'test' do
  @test = true
  it 'test' do
     expect(@test).to eq(true)
  end
end

こういうのはあかんみたい。 いまいちインスタンス変数のスコープがわからん

before構文は色々種類があるのでどれで初期化するのかで挙動が変わるけど

describe 'test' do
  before do
    test = true
  end

  it 'test' do
    expect(@test).to eq(true)
  end
end

こういう感じでローカル変数を定義するのはできないようなのでbefore blockで変数を定義して、it内で利用するならインスタンス変数しかないのかな。

これでもいいという人は多そうだけど、なんかなんでもかんでも@つけるのダサくないですか?
後、@をつけて回るのめんどくさいです。

ローカル変数

まぁ動くけど下の例のような場合ちょっと怖い

describe 'test' do
  test = true
  it 'test' do
     expect(test).to eq(true)
          test = false
  end

  context 'true' do
    it 'test' do
      expect(test).to eq(true)
    end
  end
end

要は意図しない形で変数が書き換えられた時対応が難しいという問題がある。

じゃぁ定数?

describe 'test' do
  test = true.freeze
  it 'test' do
     expect(test).to eq(true)
          test = false
  end
...

warningは出るけど再代入はできるのでちょっと怖いね。
つまりローカル変数を使用するようなケースは複数のexampleやitブロック間で共有する変数を使用したくてなおかつインスタンス変数やグローバル変数を使用したくない場合?
クソテスtあまり考えたくないケースですね(棒)

letを使用する理由

基本的にletでやっとけば変数は安全に初期化されるし、定数(freeze)よりも安全性が高いのではないかという気がしてきたが他にletを利用した方が良い理由としては以下の点が挙げられる。

  • typoにすぐ気づける (代入文でない限りtypoした場合「undefined local variable or method」のエラーがでる。インスタンス変数の場合nilと評価される)
  • ローカル変数からすぐ変更できる。(インスタンス変数ではないので@とかつける必要ない)
  • 無駄な初期化が防げる(遅延評価なので使われない場合初期化処理は実施されない)
  • かっこいい!(ブロックはかっこいい!)

上記理由は
RSpecのletを使うのはどんなときか?(翻訳)
にありますので詳しくはそちらを参考にしてください。

定数だろうがなんだろうがletで定義しときゃよしなにしてくれるって!
当然遅延評価なので、letで初期化した結果が他の場所で影響してくる場合注意する必要が出てくるのかな?

遅延評価が原因でテストが失敗する例

describe 'test' do
  let :test do @test = 'test'; 'test' end

  it 'test' do
    expect(@test).to eq('test')
  end
end

上の例が遅延評価が原因でテストが失敗する例
実用性?犬にでも食わせておけ。

Failures:

  1) test test
     Failure/Error: expect(@test).to eq('test')

       expected: "test"
            got: nil

@testがexpect時に宣言されていないのでnilになってしまう。

すぐに評価してほしい場合は

describe 'test' do
  let! :test do @test = 'test' end
...

上のようにlet!を使用しよう。その場合どうも挙動を見る限りit,exampleが実行される直前にlet block内が評価されているように見える。(つまり無駄な初期化が発生する)

ざっと調べた感じがこんな感じ。

楽しいtest lifeを

16
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
13