1

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 }.

1
  • Did you find the solution you were looking for? What did your spec end up looking like? Commented Apr 23, 2016 at 23:16

3 Answers 3

3

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
Sign up to request clarification or add additional context in comments.

Comments

1

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

1 Comment

Yes, it works. Unfortunately, now before is used which is a bit of a performance loss, and mixing let and before although they are used for the same purpose makes the code harder to read.
0

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

3 Comments

It's a way to circumvent the problem. The downside is that this spec is not DRY, because {a: 1, b: 2} is repeated.
I would counter by saying I think that DRYness, although a virtue in application code itself, takes a backseat to readability in specs. I think being clear and succinct upfront about what methods/parameters are under test can help reduce cognitive overhead. Imagine if you had multiple contexts for 'my spec' (with perhaps some of them nested), and in each context going down you merged some elements into the var hash to eventually complete to your final data under test in the it block: you'd have to keep a mental model of what elements are in var when it finally gets tested.
let(:foo) { bar(var) } is useless here, you can simply remove it and call var directly.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.