Something like let in Ruby
An idea:
class Object
def let(namespace, &block)
namespace_struct = Struct.new(*namespace.keys).new(*namespace.values)
namespace_struct.instance_eval(&block)
end
end
message = let(language: "Lisp", year: "1958", creator: "John McCarthy") do
"#{language} was created by #{creator} in #{year}"
end
Single-value scopping is more explicit because you name the variable(s) in the block arguments. This abstraction has been called as
, pipe
, into
, scope
, let
, peg
, ..., you name it, it's all the same:
class Object
def as
yield self
end
end
sum = ["1", "2"].map(&:to_i).as { |x, y| x + y } #=> 3
When to use RSpec let()?
I always prefer let
to an instance variable for a couple of reasons:
- Instance variables spring into existence when referenced. This means that if you fat finger the spelling of the instance variable, a new one will be created and initialized to
nil
, which can lead to subtle bugs and false positives. Sincelet
creates a method, you'll get aNameError
when you misspell it, which I find preferable. It makes it easier to refactor specs, too. - A
before(:each)
hook will run before each example, even if the example doesn't use any of the instance variables defined in the hook. This isn't usually a big deal, but if the setup of the instance variable takes a long time, then you're wasting cycles. For the method defined bylet
, the initialization code only runs if the example calls it. - You can refactor from a local variable in an example directly into a let without changing the
referencing syntax in the example. If you refactor to an instance variable, you have to change
how you reference the object in the example (e.g. add an@
). - This is a bit subjective, but as Mike Lewis pointed out, I think it makes the spec easier to read. I like the organization of defining all my dependent objects with
let
and keeping myit
block nice and short.
A related link can be found here: http://www.betterspecs.org/#let
Rspec how to determine if a let block has been defined?
Assuming that you call let
before including the shared examples, this would work:
shared_examples 'a shared example' do
let(:some) { 'fallback value' } unless method_defined?(:some)
end
describe 'with let' do
let(:some) { 'explicit value' }
include_examples 'a shared example'
it { expect(some).to eq('explicit value') }
end
describe 'without let' do
include_examples 'a shared example'
it { expect(some).to eq('fallback value') }
end
method_defined?
checks if a method called some
has already been defined in the current context. If not, the method is defined to provide a default value.
Another (usually easier) approach is to always define a default value and to provide the explicit value after including the shared examples: (thus overwriting the default value)
shared_examples 'a shared example' do
let(:some) { 'default value' }
end
describe 'with let' do
include_examples 'a shared example' # <- order is
let(:some) { 'explicit value' } # important
it { expect(some).to eq('explicit value') }
end
describe 'without let' do
include_examples 'a shared example'
it { expect(some).to eq('default value') }
end
Javascript testing equivalent of Ruby's rspec let
If you're looking for lazy variable evaluation in a Jasmine unit test, there are a slim few alternatives, but there is a fork made last year to let it work with v2.3+ by a fellow named Warren Ouyang (globetro).
There is also a closed issue on the Mocha Github describing the scenario exactly. Some people have posted examples that match the feature, alternatives, and explicit implementations of the feature that are more like RSpec there. (See here and here.)
describe('Thing', () => {
def('arg', () => null )
return describe('process', () => {
def('process', () => new Thing().process($arg) )
context('given 3', () => {
def('arg', () => 3 )
it('returns 12', () => expect($process).to.equal(12))
})
context('given 7', () => {
def('arg', () => 7)
it('returns 42', () => expect($process).to.equal(42))
})
})
})
Here's the above example on jsfiddle.net. This uses a more terse style of JavaScript.
Here's the previous example of using bdd-lazy-var/rspec on jsfiddle.net.
Edit: See other answer using CoffeeScript for brevity. This answer is JavaScript.
Store ruby code in a let() variable
Something like:
let(:block) { Proc.new{ |v| puts v } }
subject { [:a,:b].each_with_index { |*args| block.call args } }
Rspec yield in let helper
Just define a method:
def call_request
post :create, article: FactoryGirl.attributes_for(yield)
end
The only difference between let
and a normal method is that let
declarations are memoized per example. Since you intend to pass a block to it, the memoization isn't appropriate, anyway, and a method def will work better.
Using a loop variable in a let in a shared example call
Regarding your inability to call .times
on the instance variables you defined in before :context
blocks:
RSpec works in two phases. In the first phase it executes the Ruby code in the spec file; the let
and before
and it
methods store their blocks to be run later. In the second phase it actually runs the tests, i.e. the contents of let
and before
and it
blocks. The before :context
blocks don't define thost instance variables until the second phase, so the .times
statements, which run in the first phase, can't see the instance variables.
The solution is to put the filter options someplace that's initialized before RSpec gets to the .times
statements, like a constant.
Regarding include_examples
in a loop always using the same value of the loop variable:
include_examples
includes the given shared examples in the current context. If you include the same examples more than once, the examples themselves will be included multiple times, but let
s in the last inclusion will overwrite lets
in all of the previous inclusions. The RSpec documentation has a clear example.
The solution is to use it_behaves_like
instead of include_examples
. it_behaves_like
puts the included examples in a nested example group, so the let
s can't overwrite one another.
Applying those two solutions gives something like the following:
describe 'pdf' do
describe 'basic content' do
MAJOR_FILTER_OPTIONS = # code that initializes them
MAJOR_FILTER_OPTIONS.values.each do |filter_option|
it_behaves_like 'first_3_pages' do
let(:pdf) do
filter_options = filter_setting(filter_option)
ProjectReport::ReportGenerator.new.generate(project, filter_options, user).render
end
let(:page_analysis) { PDF::Inspector::Page.analyze(pdf) }
end
end
FILTER_OPTIONS_WITH_DOCU = # code that chooses them from MAJOR_FILTER_OPTIONS
FILTER_OPTIONS_WITH_DOCU.values.each do |filter_option|
it_behaves_like 'documentation_content' do
let(:pdf) do
filter_options = filter_setting(filter_option)
ProjectReport::ReportGenerator.new.generate(project, filter_options, user).render
end
let(:page_analysis) { PDF::Inspector::Page.analyze(pdf) }
end
end
end
end
RSpec: Accessing a `let` definition from within a block in an example
Use instance_exec
instead – it allows you to pass arguments into the block's scope:
my_object.instance_exec(my_var) { |v| @my_inst_var = v }
Alternatively, you could set the instance variable via instance_variable_set
:
my_object.instance_variable_set(:@my_inst_var, my_var)
Although both of the above work, altering the object's state that way can lead to brittle tests. You should consider changing the object so it becomes easier to test. (add a setter or pass the value upon initialization)
When to use RSpec's let! instead of before?
No, it is not a best practice to always use let!
rather than a before
block. You're correct:
- If you want to run a block of code before every example, and you need that code to return a value, use
let!
. - If you want to run a block of code before every example, but you don't need to pass a value from that code directly to your example, use
before
. (Usinglet!
would mislead the reader into thinking that its value was being used.)
Note that both let!
and before
blocks should be used sparingly in any case. They create the risk of later test writers adding tests in the same block that don't need the result of the let!
or before
, making those tests harder to understand and slower.
Related Topics
Undefined Method Error When Creating Delayed_Job Workers with Script/Delay_Job
Import CSV in Batches of Lines in Rails
How to Mix Required Argument and Optional Arguments in Ruby
Idiomatically Mock Openuri.Open_Uri with Minitest
Map Array of Ints to Nested Array Access
Ruby on Rails - £ Sign Troubles
How to Update a Model's Attribute with a Virtual Attribute
Copy One Slide from Google Slides into a New Presentation Using API
How to Fill Out Login Form with Mechanize in Ruby
Why Do Numeric String Comparisons Give Unexpected Results
Ruby Multiple Background Threads
How to Get Multiple-Line User Input in Ruby
How to Interpolate a Variable in a Ruby Regex
Why Doesn't Array Override the Triple Equal Sign Method in Ruby
Ruby 1.9 - No Such File to Load 'Win32/Open3'