6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-05-22

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

let再定義

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

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を再定義しようとすると困ります。

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 の例

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
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
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が含まれている場合に ArgumentErrorraiseすることを、その直前でGreenになるような配列に最低限の改変(+ [nil])を行った場合を作成することでテストしようとしています。が、循環参照で死にます。

なぜダメなのか

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

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

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 が使えるということになります。

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() にして解決です。

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

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

まとめ

  • letdefine_method

  • describe expect はクラス定義

  • describeの内外は継承の関係

  • 内側の define_method から外側の define_method を利用するには superを使えばいい

    • super には引数明示が必要
    • 引数なしなら super()
  • そんなコードを書かない

6
2
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
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?