Is there a way I can declare the same helper method, using let, inside of the same example group, but let it return different values in different expectation?
It would be something like
describe MyClass do
subject(:my_obj) { described_class.new(my_var) }
describe '#my_method' do
context 'some context' do
let(:my_var) { 0 }
# expectation that involves my_var
let(:my_var) { 1 }
# expectation that involves my_var
end
end
end
A solution would be to provide different contexts for each of the expectation, but is it possible to solve this without adding more contexts?
Maybe you can define an array an iterate over it, simply:
describe MyClass do
subject(:my_obj) { described_class.new(my_var) }
describe '#my_method' do
context 'some context' do
[0,1].each do |n|
let(:my_var) { n }
# expectation that involves my_var
end
end
end
end
You could use the access to an example object and its metadata in the let block in combination with the tests metadata:
let(:my_var) do |example|
example.metadata[:my_var]
end
context 'some context' do
it "my_var is 0", my_var: 0 do # <= here we set the necessary metadata
...
end
it "my_var is 1", my_var: 1 do # <= ... and again
...
end
end
...
Unlike explicit sub-contexts, this way of writing specs is relatively rarely used, so it adds some mental overhead for anyone reading this code for the 1st time. But the thing is that deeply nested contexts aren't "free" either (at some nesting level it's just quite hard to understand what's going on), so flattening the spec with some "magic" might make sense - depending on the use case. Personally I'd use sub-contexts if I can and some white magic only if I must...
I would like to check if my class is creating a new object and returning an instance of it. My idea was to combine change matcher with be_instance_of matcher but RSpec doesn't allow to do it.
expect { subject.call }.to change { Model.count }.by(1).and be_an_instance_of(Model)
I don't want to split it into two different expects with one without the block to avoid multiple invocations of the same method.
What is the general approach to this kind of situations? How should I handle my case?
You can define subject as subject.call and do this:
specify do
expect { subject }.to change { Model.count }.by(1)
expect(subject).to be_an_instance_of(Model)
end
So if previously your subject was
subject { Foo.new }
make it
subject { Foo.new.call }
let and subject calls are memoized, so it'll be only called once.
There is only one problem: if the first expectation fails - the second will not run (and this is SomethingToAvoidInSpecs™) so consider aggregating failures to remedy it.
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.
Can someone explain to me what is wrong here. Learning RSpec - I am receiving a failed test with message - expected: "Miller" got: nil. I thought that the second 'before block' would simply merge the middle_name with the original #valid_attributes. What is the correct way to do this. I know that I can simply say p.middle_name to assign it but I am trying to learn the concepts of RSpec.
describe Person do
describe "Validations" do
subject { p }
before { #valid_attributes={first_name: "Joe", last_name: "Sample"} }
...
context "with optional middle name" do
let(:p) { Person.new(#valid_attributes) }
before { #valid_attributes.merge({middle_name: "Miller"}) }
its(:middle_name) { should eq("Miller") }
end
end
end
Your problem: Hash#merge returns a new hash, you want Hash#update.
How to do it better? This is not just an advice for testing, but for programming in general: if you update and reuse variables you're gonna hit some problems with state; take a more functional approach. In this particular case you should use factory_girl or similar gem to easily create objects without having a #valid_attributes being updated who knows where.
Feature: test randomness
In order to make some code testable
As a developer
I want Array#sample to become Array#first
It would be possible if one could access instance inside Klass.any_instance.stub block. Something like this:
Array.any_instance.stub(:sample) { instance.first }
But that afaik is not possible.
Anyway, scenarios wanted!
I found a hacky solution, which I've tested on rspec versions 2.13.1 and 2.14.4. You'll need the binding_of_caller gem.
Helper method - this should be callable by your rspec example:
# must be called inside stubbed implementation
def any_instance_receiver(search_limit = 20)
stack_file_str = 'lib/rspec/mocks/any_instance/recorder.rb:'
found_instance = nil
# binding_of_caller's notion of the stack is different from caller(), so we need to search
(1 .. search_limit).each do |cur_idx|
frame_self, stack_loc = binding.of_caller(cur_idx).eval('[self, caller(0)[0]]')
if stack_loc.include?(stack_file_str)
found_instance = frame_self
break
end
end
raise "instance not found" unless found_instance
return found_instance
end
Then in your example:
Array.any_instance.stub(:sample) do
instance = any_instance_receiver
instance.first
end
I've set a limit on the stack searching, to avoid searching a huge stack. I don't see why you'd need to increase it, since it should always be around cur_idx == 8.
Note that using binding_of_caller is probably not recommended in production.
For those stumbling across this now, Rspec 3 implements this functionality via the first argument in the block passed to stub:
RSpec::Mocks.configuration.yield_receiver_to_any_instance_implementation_blocks = true # I believe this is the default
Array.any_instance.stub(:sample) { |arr| arr.first }
I found this here.