Something Like Let in Ruby

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. Since let creates a method, you'll get a NameError 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 by let, 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 my it 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 lets 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 lets 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. (Using let! 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



Leave a reply



Submit