38

I'm using RSpec (2.10.1) to test validations on a model and have extracted some code to share with other model validations. The validations were first written on the Companies table, so the code looks like this:

# support/shared_examples.rb
shared_examples "a text field" do |field, fill, length|
  it "it should be long enough" do
    @company.send("#{field}=", fill * length)
    @company.should be_valid
  end

  etc...
end

and the usage is:

# company_spec.rb
describe Company do
  before { @company = Company.new( init stuff here ) }

  describe "when address2" do
    it_behaves_like "a text field", "address2", "a", Company.address2.limit
  end

  etc...
end

I'd like to pass the @company as a parameter to the shared example so I can reuse the code for different models, something like this:

# support/shared_examples.rb
shared_examples "a text field" do |model, field, fill, length|
  it "it should be long enough" do
    model.send("#{field}=", fill * length)
    model.should be_valid
  end

  etc...
end

and the usage is:

# company_spec.rb
describe Company do
  before { @company = Company.new( init stuff here ) }

  describe "when address2" do
    it_behaves_like "a text field", @company, "address2", "a", Company.address2.limit
  end

  etc...
end

However, when I do this I get undefined method 'address2' for nil:NilClass. It appears @company is not being passed (not in scope?) How do I get something like this to work?

Nakilon
  • 34,866
  • 14
  • 107
  • 142
George Shaw
  • 1,771
  • 1
  • 18
  • 32

1 Answers1

57

The problem is that self within the example group is different from self within a before hook, so it's not the same instance variable even though it has the same name.

I recommend you use let for cases like these:

# support/shared_examples.rb
shared_examples "a text field" do |field, fill, length|
  it "it should be long enough" do
    model.send("#{field}=", fill * length)
    model.should be_valid
  end
end

# company_spec.rb
describe Company do
  describe "when address2" do
    it_behaves_like "a text field", "address2", "a", Company.address2.limit do
      let(:model) { Company.new( init stuff here ) }
    end
  end
end
Myron Marston
  • 21,452
  • 5
  • 64
  • 63
  • 1
    Some pieces don't make sense to me. If it is that `self` is different, where is it different? Why does `@company.send()` and `@company.should` work in `shared_examples`? In your suggestion, I can replace `Company.new()` with `@company` (keeping the `before` block) and that works. My understanding is missing something about what exactly is going on here. It seems that `self` is only different in a particular place (the line `it_behaves_like` up to `do`). – George Shaw Jul 06 '12 at 17:02
  • 6
    There are two basic values `self` takes on in RSpec, and it's analogous to the two values of self in a ruby class definition. Between a `describe`/`context`/`shared_examples_for` and its corresponding `end` (but not in the `it` blocks), `self` is the example group--just like `self` in a class body (but not in a method definition) is the class itself. `self` in an `it`/`let` block or a `before`/`after`/`around` hook or is the example--just like `self` in a class's instance method definition is the instance of the class. – Myron Marston Jul 12 '12 at 05:24
  • 3
    Another way to think of it: there's a two-pass process that RSpec performs. First, it evaluates all the nested `describe`/`context`/`shared_examples_for` blocks in order to define all the examples; at this point, `self` in these contexts is the corresponding example group. Secondly, RSpec runs all the defined examples (the `it` blocks). Each defined example is evaluated in an instance of the corresponding example group, and `self` is the example. – Myron Marston Jul 12 '12 at 05:27
  • 1
    Thanks. I'm relatively new to RSpec, but I find this behavior has a bad smell. Side effects of the implementation should not impede the programmer. Adding the let() destroys the conciseness of the parameters on the line. It seems that if RSPec does not find a defined variable in the scope of `it-self` then it should look in the scope of `describe-self`. Or, there should be something to obtain variables from the other self (i.e. `otherself.company`). My code uses `subject(@company)`, and I've since found that I can just use `subject` to replace `model`, though that has a code smell of its own. – George Shaw Jul 12 '12 at 17:11
  • @MyronMarston: AFAICS, your alternative does not allow reuse of the `Company` setup code across multiple `it_behaves_like` invocations. AIUI that's the whole point of the question; the original poster wants to be able to put it in a `before` block for reuse (and so do I :-/) – Adam Spiers Feb 18 '13 at 13:11
  • 1
    That's not how I read the question, but here's how you can move it into a `before` block or into a `let` in the `Company` example group: https://gist.github.com/myronmarston/4986865 . I still tend to favor a `let` method over instance variables for cases like these, because instance variables spring into existence when referenced, which is what caused the original poster's problem: the two `@company` variables in the examples are separate (one of which is nil) but that's not obvious. – Myron Marston Feb 19 '13 at 15:31
  • Thanks, great tip about using `let` for this. – Chris Salzberg Jun 05 '13 at 01:37
  • I guess let works but it is not nearly as readable as just passing a variable would be. Thanks anyway! – José Fernandes Aug 29 '13 at 16:22
  • Providing context to shared group using blocks is officially documented here: https://relishapp.com/rspec/rspec-core/docs/example-groups/shared-examples#providing-context-to-a-shared-group-using-a-block – ilvez May 08 '19 at 07:30