Thomas Schreiter
Thomas Schreiter

Reputation: 810

How to create nested lets in rspec?

Is it possible to modify an existing let variable, such as adding an entry to a hash:

describe 'my spec' do
  let(:var) { { a: 1, b: 2 } }
  let(:foo) { bar(var) }

  context 'when c is defined' do
    let(:var) { var.merge c: 3 }  # This does not work because it will be evaluated recursively
    # ... actual tests using foo ... 
  end

end

I want var to be { a: 1, b: 2, c: 3 }.

Upvotes: 1

Views: 3041

Answers (3)

Pere Joan Martorell
Pere Joan Martorell

Reputation: 3152

If you have nested RSpec contexts to test the behaviour of different parameters, you can partially override the parent parameters by merging the changed parameters into super() in your let(:foo) block:

describe 'My::Class' do
  let(:foo) do
    {
      'some_common_param' => 'value'
      'role'              => 'default',
    }
  end

  context 'with role => web' do
    let(:foo) do
      super().merge({ 'role' => 'web' })
    end

    it { should compile }
  end
end

Upvotes: 3

Paul Fioravanti
Paul Fioravanti

Reputation: 16793

Just re-define var in each context in the form it is needed in. foo is evaluated lazily so it will use the appropriate version of var defined in each context when it is eventually called in the it block:

describe 'my spec' do
  let(:foo) { bar(var) }

  context 'when c is defined' do
    let(:var) { { a: 1, b: 2, c: 3 } }

    it 'returns some result with c' do
      expect(foo).to eq('bar with c') # or whatever it returns
    end 
  end

  context 'when d is defined' do
    let(:var) { { a: 1, b: 2, d: 4 } }

    it 'returns some result with d' do
      expect(foo).to eq('bar with d') # or whatever it returns
    end  
  end
end

Edit: if you really want nested lets, then I'd say either go with Igor's answer, or if the base definition of var won't be tested in the specs, then put it in a separate let statement (if it is tested, then you'll get unavoidable repetition as per the final example):

describe 'my spec' do
  let(:base_var) { { a: 1, b: 2 } }
  let(:foo) { bar(var) }

  context 'when c is defined in var' do
    let(:var) { base_var.merge(c: 3) }

    it 'returns some result with c' do
      expect(foo).to eq('bar with c') # or whatever it returns
    end 
  end

  context 'when d is defined in var' do
    let(:var) { base_var.merge(d: 4) }

    it 'returns some result with d' do
      expect(foo).to eq('bar with d') # or whatever it returns
    end  
  end

  context 'when no changes made from base_var to var' do
    let(:var) { base_var }

    it 'returns some result from just bar' do
      expect(foo).to eq('just bar') # or whatever it returns
    end  
  end
end

Upvotes: 0

Igor Belo
Igor Belo

Reputation: 738

Yes, you created a circular dependency, it won't work.

I think the best solution is to set the var content static on contexts that it needs to be changed:

...
let(:var) { { a: 1, b: 2, c: 3 } }
...

If you really need to merge the hash with something else, the following workaround will do the trick, changing the existing hash in place with the before callback:

describe 'my spec' do
  let(:var) { { a: 1, b: 2 } }
  let(:foo) { bar(var) }

  context 'when c is defined' do
    before { var.merge! c: 3 }
  end
end

Upvotes: 1

Related Questions