RSpecでlet(:foo)を再定義したい場合

RSpecを利用していて、`let(:foo)`な`foo`の再定義がしたくなったときに少々困ったののまとめ

## `let`再定義
`let`の再定義はRSpecではよく行うと思います。

```ruby
RSpec.describe "Hoge.hoge" do
  subject { Hoge.hoge(foo) }

  let(:foo) { 100 }

  it { expect { subject }.not_to raise_error }

  context "when foo is not a number" do
    let(:foo) { "foo" }

    it { expect { subject }.to raise_error(ArgumentError) }
  end
end
```

`let(:foo)`で定義されている `foo` はまるで変数のように見える何かで、評価は `it` の実行時に行われます。

### 困る場合
外で定義した`foo`を利用して`foo`を再定義しようとすると困ります。

```ruby
RSpec.describe "Hoge.hoge" do
  subject { Hoge.hoge(foo) }

  let(:foo) { 100 }

  it { expect { subject }.not_to raise_error }

  context "when foo is negative" do
    let(:foo) { foo * -1 }           # これ

    it { expect { subject }.to raise_error(ArgumentError) }
  end
end
```

これをやろうとすると、`foo`が再帰的に評価されて `stack level too deep.` と言われて死にます。

「そんな`let`書くなよ」という話もあるのですが、こういうことをしたくなった理由は `shared_examples` でした。

### `shared_examples` の例

```ruby:strings_converter_spec.rb
RSpec.describe StringsConverter do
  let(:converter) { StringsConverter.new }
  let(:items) { ["rock", "paper", "scissor"] }
  subject { converter.run(items) }

  it_behaves_like "a strict-typed Converter"
end
```
```ruby:integers_converter_spec.rb
RSpec.describe IntegersConverter do
  let(:converter) { IntegersConverter.new }
  let(:items) { [100, 200, 300] }
  subject { converter.run(items) }

  it_behaves_like "a strict-typed Converter"
end
```
```ruby:strict_converter_behaviour.rb
RSpec.shared_examples "a strict-typed Converter" do
  describe "#run" do
    subject { converter.run(items) }

    it "is implemented" { expect { subject }.not_to raise_error(NotImplementedError) }

    context "when argument `items` include other types" do
      # ここでitemsを it_behaves_like 元の定義に基づいて再定義したい
      let(:items) { items + [nil] }

      it { expect { subject }.to raise_error(ArgumentError) }
    end
  end
end

```

ここでは各 `Converter` にわたす配列の要素の型は各specファイルで定義しつつ、共通のふるまいだけを切り出そうとしています。
処理する配列に`nil`が含まれている場合に `ArgumentError` を`raise`することを、その直前でGreenになるような配列に最低限の改変(`+ [nil]`)を行った場合を作成することでテストしようとしています。が、循環参照で死にます。

## なぜダメなのか
`context`や`describe`はクラス定義を行っています。`context`がネストされると、内側で定義されたcontextクラスは外側のものを継承します。
そして `let()` はおおよそ `define_method()` のエイリアスです。

```ruby
describe "some Foo" do
  let(:name) { "hoge" }
  describe "when name is fuga"
    let(:name) { "fuga" }
  end
end
```

は

```ruby
class SomeFoo
  define_method :name { "hoge" }
  class WhenNameIsFuga < SomeFoo
    define_method :name { "fuga" }
  end
end
```

っぽくなります。

つまり `let(:foo) { foo }` と書いてしまうと同じクラスの中で自分自身を最初に見つけるので、無限に再帰が起きることになります。
記述形式から期待するような `foo = foo + 1` とは全く挙動が異なるのがポイントでした

## どうすればいいのか

- `let`が変数定義ではなく動的なメソッドである
- `it_behaves_like` で最低でも一つ間にクラス生成を挟んでいて、外側を継承している

つまり `let` の中で外側のcontextのものを参照するためには `super` が使えるということになります。

```ruby:strict_converter_behaviour.rb
RSpec.shared_examples "a strict-typed Converter" do
  describe "#run" do
    subject { converter.run(items) }

    it "is implemented" { expect { subject }.not_to raise_error(NotImplementedError) }

    context "when argument `items` include other types" do
      # super を呼んで定義済み配列GET～！
      let(:items) { super + [nil] }

      it { expect { subject }.to raise_error(ArgumentError) }
    end
  end
end

```

と思いきや、今度は

```
implicit argument passing of super from method defined by
  define_method() is not supported. Specify all arguments explicitly.
```

などと怒られます。`define_method` 内で`super`する場合は明示的にarityを指定する必要があるので、 `super()` にして解決です。

```ruby
    context "when argument `items` include other types" do
      # super() で引数がないことを示す
      let(:items) { super() + [nil] }

      it { expect { subject }.to raise_error(ArgumentError) }
    end
```



## まとめ
- `let` は `define_method`
- `describe` `expect` はクラス定義
- `describe`の内外は継承の関係
- 内側の `define_method` から外側の `define_method` を利用するには `super`を使えばいい
  - `super` には引数明示が必要
  - 引数なしなら `super()`

- ~~*そんなコードを書かない*~~