I find it much easier to just set a variable in my code than use let
. let
is finicky and always tells me ways in which I'm using it wrong.
When I use a simple variable declaration in my specs like
tx_good = makeTransaction1()
, everything works fine.
But when I use let
like so
let(:tx_good) { makeTransaction1() }
I will invariably get some error like this telling me it can't go here or there...
`let` and `subject` declarations are not intended to be called
in a `before(:context)` hook, as they exist to define state that
is reset between each example, while `before(:context)` exists to
define state that is shared across examples in an example group.
Given how finicky using let
is, I'm forced to wonder is it worth the extra effort and care I must put forward to use it. Does anyone know how much processing time is really saved using let vs. merely assigning a variable up front?
I want to follow good testing protocol, so I'm hoping someone can convince me of why I should use let
like (seemingly) everyone else does.
You're using this stuff wrong, and I understand your frustration. So let me give you a condensed manual for using let
s in RSpec.
Main value in using let
does not come from saved processing power. It integral part of the wider RSpec philosophy. I'll try to explain and hopefully it'll be easier for you to progress...
let
is lazywhatever you define inside the block will be called if and only if it is actually used in the spec:
context do
let(:foo) { sleep(10000) } # will not happen
specify { expect(1).to eq(1) }
end
context do
specify do
foo = sleep(10000) # you'll wait
expect(1).to eq(1)
end
end
Use let!
, which is eager (i.e. not lazy) version of let
let
is memoizedWhatever is defined inside the block will happen only once (in the scope of the context):
context do
let(:random_number) { rand }
specify do
expect(random_number).to eq(random_number) # will always pass
end
end
If you don't want this feature, define a method:
context do
def random_number
rand
end
specify do
expect(random_number).to eq(random_number) # sometimes pass, mostly fail
end
end
let
in lower level contexts overwrites let
definitions from higher level:context do
let(:x) { 1 }
specify { expect(x).to eq(1) # pass
context 'with different x' do
let(:x) { 2 }
specify { expect(x).to eq(2) # pass
end
context do
specify { expect(x).to eq(1) # pass
end
end
^ this allows you to compose the specs in a way, where only relevant "pieces" of the setup is mentioned in the context, for example:
context do
let(:x) { 1 }
let(:y) { 1 }
let(:z) { 1 }
specify { expect(foo(x, y, z)).to eq(3) }
context 'when z is nil'
let(:z) { nil }
specify { expect(foo(x, y, z)).to raise_error) } # foo doesn't work with z = nil
end
context 'when x is nil'
let(:x) { nil }
specify { expect(foo(x, y, z)).to eq(15) }
end
end
subject
is a magic let
# writing
subject { foo(x) }
# is almost the same as writing
let(:subject) { foo(x) }
subject
is a reserved concept in RSpec, it's a "thing you test" so you could write the example with `foo(x, y, z) like this:
context do
let(:x) { 1 }
let(:y) { 1 }
let(:z) { 1 }
subject { foo(x, y, z) }
specify { expect(subject).to eq(3) }
context 'when z is nil'
let(:z) { nil }
specify { expect(subject).to raise_error) } # foo doesn't work with z = nil
end
context 'when x is nil'
let(:x) { nil }
specify { expect(foo(subject)).to eq(15) }
end
end
let
andsubject
declarations are not intended to be called in abefore(:context)
hook, as they exist to define state that is reset between each example, whilebefore(:context)
exists to
define state that is shared across examples in an example group.
you're doing something like
before do
let(:x) { ... }
end
just don't do it, you define let
inside describe
and context
, but you can use them (not define them, use what is defined) inside before
and specify
:
let(:name) { 'Frank' }
before do
User.create name: name
end
specify do
expect(User.where(name: name).count).to eq(1)
end