Why isn't rspec memoizing this? - ruby

I have a test which is a bit like the following. The details isn't important, but I have a method which takes about 10 seconds, and gets back some data which I want to use a bunch of times in a bunch of tests. The data won't be any more fresh - I only need to fetch it once. My understanding of let is that it memoizes, so I would expect the following to only call slow_thing once. But I see it called as many times as I refer to slowthing. What am I doing wrong?
describe 'example' do
def slow_thing
puts "CALLING ME!"
sleep(100)
end
let(:slowthing) { slow_thing }
it 'does something slow' do
expect(slowthing).to be_true
end
it 'does another slow thing' do
expect(slowthing).to be_true
end
end
When I run the test, I see CALLING ME! as many times as I have assertions or use slowthing.

The documentation states values are not cached across examples:
The value will be cached across multiple calls in the same example but not across examples. [Emphasis mine.]
E.g., also from the docs:
$count = 0
describe "let" do
let(:count) { $count += 1 }
it "memoizes the value" do
count.should == 1
count.should == 1
end
it "is not cached across examples" do
count.should == 2
end
end
From https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let

Related

Is it possible to create a new RSpec matcher by composing existing ones?

In one of my specs, I find myself repeating lines like these often:
expect(result.status).to be(:success)
expect(result.offers).not_to be_empty
expect(result.comments).to be_empty
To make my tests more succinct and readable, I want to compose these into a line like this:
expect(result).to be_successful
I can do this by creating a custom matcher:
matcher :be_successful do
match { |result|
result.status == :success &&
result.offers.length > 0 &&
result.comments.empty?
}
end
But I now have a failing test, and the failure message is completely useless. All it says now is Expected #<Result ...> to be successful.
I know I can override the failure message, but now this solution is getting more complicated than it's worth for saving 2 lines for every spec example. The original 3 lines generated useful failure messages, all I wanted to do was combine them into 1 line.
I could move the 3 lines into a separate function (e.g. assert_successful) and call that from each spec example, but I'd like to keep the matcher syntax.
Can this be done?
According to this
You could do something like this:
RSpec::Matchers.define :be_successful do
match do |result|
result.status == :success &&
result.offers.length > 0 &&
result.comments.empty?
end
failure_message do |result|
"Should have #{result} equal to be successful"
end
failure_message_when_negated do |result|
"Should not have #{result} to be successful"
end
end
If you reuse this test elsewhere more than 3 times, then it makes sense to create a new matcher and override the failure messages(It's not an overhead). If you use this test only one time then it makes sense to keep it without overly abstract it.
One way of organizing this is: Put the real testing code into
a method _check_ok(expected, actual) of its own,
which returns either [true] or [false, message].
This is called as follows:
RSpec::Matchers.defined :check_ok do |expected|
match do |actual|
return _check_ok(expected, actual)[0]
end
failure_message do |actual|
return _check_ok(expected, actual)[1]
end
end
This repeats the _check_ok call in the failure case,
which normally isn't a problem.

How does rpsec's "let" use memoization?

It says in "RSpec Core 3.5":
Use let to define a memoized helper method
I don't find their example very clear. Are they saying that the expression for :count is only evaluated once for a spec, and again between every spec?
I'm interested particularly in understanding how memoization with let would work with an ActiveRecord object.
Are they saying that the expression for :count is only evaluated once
for a spec, and again between every spec?
answer from docs:
The value will be cached across multiple calls in the same example but
not across examples.
So yes, it will be evaluated once for every example.
In other words the value will be evaluated once per it block.
I find their example super expressive, look:
$count = 0
RSpec.describe "let" do
let(:count) { $count += 1 }
# count will not change no matter how many times we reference it in this it block
it "memoizes the value" do
expect(count).to eq(1) # evaluated (set to 1)
expect(count).to eq(1) # did not change (still 1)
end
# count will be set to 2 and remain 2 untill the end of the block
it "is not cached across examples" do
expect(count).to eq(2) # evaluated in new it block
end
end
We referenced count twice in memoizes the value example, but it was evaluated only once.

Rspec mocking block on stub not being yielded

So i have two calls to a method called retry_with_timeout that takes a block and executes until the block returns true or a value other than nil (i.e false will result in a loop) or until a timeout occurs
Sample class:
def do_the_thing
retry_with_timeout(10, 5) do
case something
when 1
false
when 2
false
else
raise
end
end
retry_with_timeout(30, 10) do
case something_else
when 1
false
when 2
false
when 3
true
else
raise
end
end
end
Spec class:
it "should pass when the thing is 3" do
model = test_model #creates a double and stubs all of the necessary common methods
t_model.stub(:retry_with_timeout).with(10, 5).ordered
t_model.stub(:retry_with_timeout).with(30, 10).and_yield().ordered
expect { t_model.do_the_thing }.to be(true)
end
I get an error because the '3' case isn't in the first block, thus the 'else' is called...
I need to skip the first and evaluate in the second block.... I have tried EVERYTHING and I am LOSING MY MIND!!!! Can anyone help me?
Ok, so I've answered my own question... Turns out there are some features that aren't documented... In order to return then yield, one must do it the following way:
t_model.stub(:some_method).and_return("Cool", "Awesome", and_yield(foo))
#Just for informations' sake
t_model.stub(:some_other_method).and_return("FOO", "BAR", raise_error)
its added as a return item for some reason and ISN'T DOCUMENTED ANYWHERE!!!!!

initializing for autogenerating rspec tests

I'm new to Rspec and trying to have Rspec create new tests for something variable. Here's what I've tried:
require 'rspec'
describe 'a test' do
#array = []
before(:all) do
#array = [1,3,4,6,9,2]
end
#array.each do |i|
it { i.should > 3 }
end
it { #array.should have(4).items }
end
Unfortunately, the array doesn't seem to get filled before the .each block runs. Is there a way to make it work?
The before block doesn't get executed until just before RSpec starts to execute the it blocks. But you're iterating through #array in the outer body of describe, so #array is still nil at that point. Within the last it block, #array has been initialized.
You can put pretty much any code you want within an it block. For example, you could write:
it "should have elements > 3" do
#array.each do |i|
i.should > 3
end
end
Or, if you want separate it calls, you can just populate #array in the describe block itself, as in:
describe 'a test' do
#array = [1,3,4,6,9,2]
#array.each do |i|
it { i.should > 3 }
end
it { #array.should have(4).items }
end
although you might want to rework it in this case so that you pass a string argument to it indicating which element of the array (i.e. what index) you're operating on.
As for dynamically generating it statements based on data defined in the let/before hierarchy, I don't think that's possible, because you only have access to that data within an it block and it is not part of the acceptable DSL within an it block.
Here's the best that I could come up with so far. It's dirty, but if generating tests while iterating an array that isn't pre-defined is what you're after, then you can try to loop a number that you guess will overshoot the filled indexes.
require 'rspec'
describe 'a test' do
before(:all) do
#array = [1,3,4,6,9,2]
end
16.times do |i|
it 'has a desc' do
next if #array[i].nil?
#array[i].should > 3
end
end
end
The caveat here is how you're gonna deal with the tests for indexes past the array's filled index. So far, I've found using next to be the cleanest as it just results in another passed test. using return or break results in proc-closure or jump errors. I also tried wrapping my method in a begin,rescue block but it seemed it couldn't capture any errors within the it block.
Not sure if you managed to come up with a better answer, but wondering if this would solve the problem for you:
RSpec.describe 'a test' do
array = (1..100).to_a.sample(6)
array.each do |i|
it 'has a desc' do
expect(i).to be > 3
end
end
end
I think the answer depends on how/why you want the data to vary.

Is it possible for RSpec to expect change in two tables?

RSpec expect change:
it "should increment the count" do
expect{Foo.bar}.to change{Counter.count}.by 1
end
Is there a way to expect change in two tables?
expect{Foo.bar}.to change{Counter.count}.by 1
and change{AnotherCounter.count}.by 1
I prefer this syntax (rspec 3 or later):
it "should increment the counters" do
expect { Foo.bar }.to change { Counter, :count }.by(1).and \
change { AnotherCounter, :count }.by(1)
end
Yes, this are two assertions in one place, but because the block is executed just one time, it can speedup the tests.
EDIT: Added Backslash after the .and to avoid syntax error
I got syntax errors trying to use #MichaelJohnston's solution; this is the form that finally worked for me:
it "should increment the counters" do
expect { Foo.bar }.to change { Counter.count }.by(1)
.and change { AnotherCounter.count }.by(1)
end
I should mention I'm using ruby 2.2.2p95 - I don't know if this version has some subtle change in parsing that causes me to get errors, it doesn't appear that anyone else in this thread has had that problem.
This should be two tests. RSpec best practices call for one assertion per test.
describe "#bar" do
subject { lambda { Foo.bar } }
it { should change { Counter.count }.by 1 }
it { should change { AnotherCounter.count }.by 1 }
end
If you don't want to use the shorthand/context based approach suggested earlier, you can also do something like this but be warned it will run the expectation twice so it might not be appropriate for all tests.
it "should increment the count" do
expectation = expect { Foo.bar }
expectation.to change { Counter.count }.by 1
expectation.to change { AnotherCounter.count }.by 1
end
Georg Ladermann's syntax is nicer but it doesn't work. The way to test for multiple value changes is by combining the values in arrays. Else, only the last change assertion will decide on the test.
Here is how I do it:
it "should increment the counters" do
expect { Foo.bar }.to change { [Counter.count, AnotherCounter.count] }.by([1,1])
end
This works perfectecly with the '.to' function.
The best way I've found is to do it "manually":
counters_before = Counter.count
another_counters_before = AnotherCounter.count
Foo.bar
expect(Counter.count).to eq (counters_before + 1)
expect(AnotherCounter.count).to eq (another_counters_before + 1)
Not the most elegant solution but it works
After none of the proposed solutions proved to actually work, I accomplished this by adding a change_multiple matcher. This will only work for RSpec 3, and not 2.*
module RSpec
module Matchers
def change_multiple(receiver=nil, message=nil, &block)
BuiltIn::ChangeMultiple.new(receiver, message, &block)
end
alias_matcher :a_block_changing_multiple, :change_multiple
alias_matcher :changing_multiple, :change_multiple
module BuiltIn
class ChangeMultiple < Change
private
def initialize(receiver=nil, message=nil, &block)
#change_details = ChangeMultipleDetails.new(receiver, message, &block)
end
end
class ChangeMultipleDetails < ChangeDetails
def actual_delta
#actual_after = [#actual_after].flatten
#actual_before = [#actual_before].flatten
#actual_after.map.with_index{|v, i| v - #actual_before[i]}
end
end
end
end
end
example of usage:
it "expects multiple changes despite hordes of cargo cultists chanting aphorisms" do
a = "." * 4
b = "." * 10
times_called = 0
expect {
times_called += 1
a += ".."
b += "-----"
}.to change_multiple{[a.length, b.length]}.by([2,5])
expect(times_called).to eq(1)
end
Making by_at_least and by_at_most work for change_multiple would require some additional work.
I'm ignoring the best practices for two reasons:
A set of my tests are regression tests, I want them to run fast, and
they break rarely. The advantage of having clarity about exactly
what is breaking isn't huge, and the slowdown of refactoring my code
so that it runs the same event multiple times is material to me.
I'm a bit lazy sometimes, and it's easier to not do that refactor
The way I'm doing this (when I need to do so) is to rely on the fact that my database starts empty, so I could then write:
foo.bar
expect(Counter.count).to eq(1)
expect(Anothercounter.count).to eq(1)
In some cases my database isn't empty, but I either know the before count, or I can explicitly test for the before count:
counter_before = Counter.count
another_counter_before = Anothercounter.count
foo.bar
expect(Counter.count - counter_before).to eq(1)
expect(Anothercounter.count - another_counter_before).to eq(1)
Finally, if you have a lot of objects to check (I sometimes do) you can do this as:
before_counts = {}
[Counter, Anothercounter].each do |classname|
before_counts[classname.name] = classname.count
end
foo.bar
[Counter, Anothercounter].each do |classname|
expect(classname.count - before_counts[classname.name]).to be > 0
end
If you have similar needs to me this will work, my only advice would be to do this with your eyes open - the other solutions proposed are more elegant but just have a couple of downsides in certain circumstances.

Resources