There are several cases, where my specs always return true, making the test superfluous...
Take this Achievement model for instance:
class Achievement < ActiveRecord::Base
has_many :stages
def call_name_method
name
end
def name
key
end
end
# for simplicity sake, I'm just testing a method which simply calls another method.
achievement_specs.rb
require 'rails_helper'
describe Achievement do
describe '#call_name_method' do
subject { achievement.call_name_method }
let(:achievement) { create(:achievement) }
it 'calls #name' do
expect(achievement).to receive(:name)
subject
end
end
end
This test succeeds, but I can change it to expect(achievement).to receive(:foobar) and it will still succeed, although I am not calling foobar.
According to this answer, it is the correct syntax, but it somehow never fails. Is this a bug?
I also tried using .to have_received(:call_name_method), but that results in this error:
1) Achievement#call_name_method calls #name
Failure/Error: expect(achievement).to have_received(:call_name_method)
# expected to have received call_name_method, but that object is not a spy or method has not been stubbed.
The problem was in the rails_helper.rb
config.after(:each) do
DatabaseCleaner.clean
RSpec::Mocks.teardown # <-- this line was fault
end
After removing that line the specs worked as expected. A coworker accidentally commited this although it was not needed.
Since all test were 'succeeding', it did not catch our attention until now.
Related
In my model definition, I have
# models/my_model.rb
# == Schema Information
#
# Table name: my_models
#
# id :bigint not null, primary key
# another_model_id :bigint
# field_1 :string
# field_2 :string
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_my_models_on_another_model_id (another_model_id) UNIQUE
class MyModel < ApplicationRecord
belongs_to :another_model
def update_from_api_response(api_response)
$stderr.puts("UPDATE")
self.field_1 = api_response[:field_1]
self.field_2 = api_response[:field_2]
end
def update_my_model!(api_response)
ApplicationRecord.transaction do
$stderr.puts("HELLO")
update_from_api_response(api_response)
$stderr.puts("WORLD")
self.save!
end
end
end
I put in some puts statements to check whether my code entered the function. If everything works alright, the program should log "HELLO", "UPDATE", then "WORLD".
In my model spec I have
# spec/models/my_model_spec.rb
RSpec.describe MyModel, type: :model do
let(:my_model) { create(:my_model) }
let(:api_response) {
{
:field_1 => "field_1",
:field_2 => "field_2",
}
}
describe("update_my_model") do
it "should update db record" do
expect(my_model).to receive(:update_from_api_response)
.with(api_response)
expect(my_model).to receive(:save!)
expect{ my_model.update_my_model!(api_response) }
.to change{ my_model.field_1 }
end
end
end
The factory object for MyModel is defined like this (it literally does not do anything)
# spec/factories/my_models.rb
FactoryBot.define do
factory :my_model do
end
end
The output from the puts (this appears before the error message)
HELLO
WORLD
Interestingly, "UPDATE" is not printed, but it passes the receive test.
The change match test fails, and the output from the console is as follows
1) MyModel update_my_model should update db record
Failure/Error:
expect{ my_model.update_my_model(api_response) }
.to change{ my_model.field_1 }
expected `my_model.field_1` to have changed, but is still nil
# ./spec/models/my_model_spec.rb
# ./spec/rails_helper.rb
I suspected that it might have something to do with me wrapping the update within ApplicationRecord.transaction do but removing that does nothing as well. "UPDATE" is not printed in both cases.
I've also changed the .to receive(:update_from_api_response) to .to_not receive(:updated_from_api_response) but it throws an error saying that the function was called (but why is "UPDATE" not printed then?). Is there something wrong with the way I'm updating my functions? I'm new to Ruby so this whole self syntax and whatnot is unfamiliar and counter-intuitive. I'm not sure if I "updated" my model field correctly.
Thanks!
Link to Git repo: https://github.com/jzheng13/rails-tutorial.git
When you call expect(my_model).to receive(:update_from_api_response).with(api_response) it actually overrides the original method and does not call it.
You can call expect(my_model).to receive(:update_from_api_response).with(api_response).and_call_original if you want your original method to be called too.
Anyway, using "expect to_receive" and "and_call_original" rings some bells for me, it means you are testing two different methods in one test and the tests actually depends on implementation details instead of an input and an output. I would run two different tests: test that "update_from_api_response" changes the fields you want, and maybe test that "update_my_model!" calls "update_from_api_response" and "save!" (no need to test the field change, since that would be covered on the "update_from_api_response" test).
Thank you, the separate Github file works wonders.
This part works fine:
Put it in a separate expectation and it works fine:
describe("update_my_model") do
it "should update db record" do
# This works
expect{ my_model.update_my_model!(api_response) }.to change{ my_model.field_one }
end
end
How is it triggered?
But here is your problem:
expect(my_model).to receive(:update_from_api_response).with(api_response)
expect(my_model).to receive(:save!)
This means that you are expecting my model to have update_from_api_response to be called with the api_response parameter passed in. But what is triggering that? Of course it will fail. I am expecting my engine to start. But unless i take out my car keys, and turn on the ignition, it won't start. But if you are expecting the car engine to start without doing anything at all - then of course it will fail! Please refer to what #arieljuod has mentioned above.
Also why do you have two methods: update_from_api_response and update_my_model! which both do the same thing - you only need one?
SO...
I have a RSpec testing issue where I am attempting to configure a specific setup (without creating new data, but modifying instance data) and after a test case is ran, the data is not reverted. Please see a simple example below and note that foo is an ActiveRecord object...
require 'rails_helper'
RSpec.describe My::Code do
before(:context) do
#foo = FactoryGirl.create(:foo)
end
after(:context) do
#foo.destroy!
end
let(:foo) { #foo.clone }
describe 'something' do
# Imagine `something` just returns foo.bar, which is an over-
# simplification, but gives you an idea of the problem I am seeing
subject { My::Code.something(foo) }
context 'when foo has different property value' do
before(:each) do
foo.bar = false
end
it { is_expected_to be(false) }
end
context 'when foo has original property value' do
# This will fail, as foo's bar property is still false
it { is_expected_to be(true) }
end
end
...this is the gist of my situation and I cannot figure out why RSpec is not rolling back. A little of my rails_helper file...
config.use_transactional_fixtures = true
...
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
...any insight is much appreciated.
In Ruby on Rails, using clone, this seems to be the expected behavior:
Identical to Ruby's clone method. This is a “shallow” copy. Be warned that your attributes are not copied. That means that modifying attributes of the clone will modify the original, since they will both point to the same attributes hash. If you need a copy of your attributes hash, please use the dup method.
emphasis mine. So by using clone you are still pointing to the same attributes hash and modifying one modifies the other.
As you have found out, you can reload the object which gets rid of all your changes OR you can call dup instead of clone:
Note that this is a “shallow” copy as it copies the object's attributes only, not its associations.
In my Rails application I have a User model:
class User
def self.foo
User.all.each{ |user| user.bar }
end
def bar
end
end
In my spec file I want to check that foo calls bar for every user, so far that's what I have:
describe '::foo' do
let!(:users) { Fabricate.times(5, :user) }
it 'calls bar for every user' do
users.each do |user|
expect(user).to receive(:bar)
end
User.foo
end
end
Although the method gets called (I debugged it, so I'm sure of that) the spec is red.
Also I tried to write this code to understand where the problem was:
let!(:user) { Fabricate(:user) }
it 'fails' do
expect(user).to receive(:bar)
User.first.bar
end
it 'pass' do
expect(user).to receive(:bar)
user.bar
end
It seems that if I reference my instance directly it works, if I obtain it from the DB the expectation doesn't work.
I use mongoid, not sure if this is relevant.
I believe it cannot be done due to how RSpec works: When you set an expectation, RSpec essentially 'wraps' the object so that it can keep track of the messages it receives.
But when the implementation code fetches records from the database, they are not wrapped, so RSpec isn't able to record their messages.
RSpec does have a method allow_any_instance_of which can help in some cases, but its use is discouraged, and don't think it would be suitable here.
In this situation, I would suggest stubbing User.all to return some doubles (two should be sufficient). You can then verify that bar is called on each one.
Scenario
Have a race case where concurrency can cause a duplicate key error. Take for example:
def before_create_customer_by_external_id
end
def create_customer_from_external_id(external_id = nil)
#customer = current_account.customers.create!(external_id: external_id || #external_id)
end
def set_new_or_old_customer_by_external_id
if #customer.blank?
before_create_customer_by_external_id
create_customer_from_external_id
end
rescue ActiveRecord::RecordInvalid => e
raise e unless Customer.external_id_exception?(e)
#customer = current_account.customers.find_by_external_id(#external_id)
end
The Test
Now, to test the race case (based on the answer to Simulating race conditions in RSpec unit tests) we just need to monkey patch before_create_customer_by_external_id to call create_customer_from_external_id.
The Question
How can you do this without overriding the whole class and getting a "method not found" error?
After some digging, I came up with the following solution:
context 'with race condition' do
it 'should hit race case and do what is expected' do
ControllerToOverride.class_eval do
def before_create_new_customer_by_external_id
create_customer_from_external_id
end
end
# ...expect...
ControllerToOverride.class_eval do
undef before_create_new_customer_by_external_id
end
end
end
I verified that it was hitting the race case by using a code coverage tool and debug statements.
Happy to know if there's a cleaner way here.
Edit 2020-04-24
Per the comment, we should undef this method so it doesn't affect subsequent tests. Ref: https://medium.com/#scottradcliff/undefining-methods-in-ruby-eb7fba21f63f
I did not verify this, as I no longer have this test suite. Please let me know if it does/does not work.
A step on from monkey patching the class is to create an anonymous subclass:
context "with race condition" do
controller(ControllerToOverride) do
def before_create_customer_by_external_id
end
end
it "should deal with it " do
routes.draw { # define routes here }
...
end
end
This is not so very different to your solution but keeps the monkeypatch isolated to that context block.
You may not need the custom routes block - rspec sets up some dummy routes for the rest methods (edit, show, index etc)
If this context is inside a describe ControllerToOverride block then the argument to controller is optional, unless you have turned off config.infer_base_class_for_anonymous_controllers
I want to make sure my sweeper is being called as appropriate so I tried adding something like this:
it "should clear the cache" do
#foo = Foo.new(#create_params)
Foo.should_receive(:new).with(#create_params).and_return(#foo)
FooSweeper.should_receive(:after_save).with(#foo)
post :create, #create_params
end
But I just get:
<FooSweeper (class)> expected :after_save with (...) once, but received it 0 times
I've tried turning on caching in the test config but that didn't make any difference.
As you already mentioned caching has to be enabled in the environment for this to work. If it's disabled then my example below will fail. It's probably a good idea to temporarily enable this at runtime for your caching specs.
'after_save' is an instance method. You setup an expectation for a class method, which is why it's failing.
The following is the best way I've found to set this expectation:
it "should clear the cache" do
#foo = Foo.new(#create_params)
Foo.should_receive(:new).with(#create_params).and_return(#foo)
foo_sweeper = mock('FooSweeper')
foo_sweeper.stub!(:update)
foo_sweeper.should_receive(:update).with(:after_save, #foo)
Foo.instance_variable_set(:#observer_peers, [foo_sweeper])
post :create, #create_params
end
The problem is that Foo's observers (sweepers are a subclass of observers) are set when Rails boots up, so we have to insert our sweeper mock directly into the model with 'instance_variable_set'.
Sweepers are Singletons and are instantiated at the beginning of the rspec test. As such you can get to it via MySweeperClass.instance(). This worked for me (Rails 3.2):
require 'spec_helper'
describe WidgetSweeper do
it 'should work on create' do
user1 = FactoryGirl.create(:user)
sweeper = WidgetSweeper.instance
sweeper.should_receive :after_save
user1.widgets.create thingie: Faker::Lorem.words.join("")
end
end
Assuming you have:
a FooSweeper class
a Foo class with a bar attribute
foo_sweeper_spec.rb:
require 'spec_helper'
describe FooSweeper do
describe "expiring the foo cache" do
let(:foo) { FactoryGirl.create(:foo) }
let(:sweeper) { FooSweeper.instance }
it "is expired when a foo is updated" do
sweeper.should_receive(:after_update)
foo.update_attribute(:bar, "Test")
end
end
end