RSpecのletとlet!の違い

RSpecの`let`は遅延評価で`let!`は事前評価というのがわかったとしても、具体的な挙動は試してみないとわかりにくい。

気をつけたいところとしては、`let!` が事前評価をするとはいっても、変数の依存関係の解決は`let`同様に動的に行えるということ。
つまり、`let!`を使ったとしても、`let!`変数の評価中にまだ評価・宣言されていない`let/let!`変数が登場してもエラーにはならない。
その際には、`let`と同様のプロセスで、深さ優先探索のようにして処理の途中に出会った`let/let!`変数の評価をその都度行っていくだけである。
別の言い方をすると、`let!`を使おうとも、`let!`変数の依存関係がプログラムオーダー順に解決されるように宣言位置を調節する必要はない。

RSpecでは個々のテストを実行する前に、プリプロセス（後述）を行なった後、`RSpec.describe`句の中身全体をプログラムオーダー順に上から見ていくフェーズが入る。
このフェーズでは、`before`句もしくは`let!`句に出くわした場合には評価が開始される一方で、`let`句は無視される。
このテストの実行前の評価機会の有無が`let`と`let!`の違いの本質であり、評価開始後の処理のされ方は両者ともに全く同じである。

```spec/let_test1.rb
RSpec.describe do
  let(:var1) { # スルーされる
    puts "evaluating var1"
    var3 + 2
  }
  before :each do
    puts "invoke before block"
  end
  let(:var2) { # スルーされる
    puts "evaluating var2"
    var3 + 1
  }
  let(:var3) { # スルーされる
    puts "evaluating var3"
    var4 + 1
  }
  let(:var4) { # スルーされる
    puts "evaluating var4"
    100
  }
  it do
    puts "======"
    puts "#{var1}" # var1 -> var3 -> var4
    puts "#{var2}" # var2 -> var3（初期化済み）
    puts "#{var3}" # var3(初期化済み）-> var4（初期化済み）
    puts "#{var4}" # var4（初期化済み）
  end
end
```

```
$ bundle exec rspec spec/let_test1.rb
invoke before block
======
evaluating var1
evaluating var3
evaluating var4
103
evaluating var2
102
101
100
```

```spec/let_test2.rb
RSpec.describe do
  describe do
    let!(:var1) { # var1 -> var3 -> var4
      puts "evaluating var1"
      var3 + 2
    }
    before do
      puts "invoke before block"
    end
    let!(:var2) { # var2-> var3（初期化済み）
      puts "evaluating var2"
      var3 + 1
    }
    let!(:var3) { # var3(初期化済み）-> var4（初期化済み）
      puts "evaluating var3"
      var4 + 1
    }
    let!(:var4) { # var4（初期化済み）
      puts "evaluating var4"
      100
    }
    it do
      puts "======" # これの実行時には全て初期化済み
      puts "#{var1}"
      puts "#{var2}"
      puts "#{var3}"
      puts "#{var4}"
    end
  end
end
```

```
$ bundle exec rspec spec/let_test2.rb
evaluating var1
evaluating var3
evaluating var4
invoke before block
evaluating var2
======
103
102
101
100
.

Finished in 0.00471 seconds (files took 0.12489 seconds to load)
1 example, 0 failures
```

## 補足：ここでいうプリプロセスとは何か
主にdescribe, contextなどのネストの解消と、同一名称の変数の統合処理である。
`let_test3.rb`は、`it`の実行前に`let_test4.rb`のように変形されると考えられる。
（ただし、RSpecのコードを読んだわけではないので確証はない）

```let_test3.rb
RSpec.describe do
  let!(:var2) {
    puts "evaluating var2 (100)"
    100
  }
  describe do
    let!(:var2) {
      puts "evaluating var2 (0)"
      0
    }
    let!(:var1) {
      puts "evaluating var1"
      var2
    }
    before {
      puts "before"
    }
    let!(:var2) {
      puts "evaluating var2 (1)"
      1
    }
    let!(:var2) {
      puts "evaluating var2 (2)"
      2
    }
    it do
      puts "#{var1}"
    end
  end
end
```

```let_test3_preprocessed.rb
RSpec.describe do
  let!(:var2) {
    puts "evaluating var2 (2)"
    2
  }
  let!(:var1) {
    puts "evaluating var1"
    var2
  }
  before {
    puts "before"
  }
  it do
    puts "#{var1}"
  end
end
```

```
$ bundle exec rspec spec/let_test3.rb
evaluating var2 (2)
evaluating var1
before
2
.

Finished in 0.00399 seconds (files took 0.12003 seconds to load)
1 example, 0 failures
```