I'm getting intermittent test failures when using instance_double.
I have a file with 4 specs in it. Here is the source:
require 'rails_helper'
describe SubmitPost do
before(:each) do
#post = instance_double('Post')
allow(#post).to receive(:submitted_at=)
end
context 'on success' do
before(:each) do
allow(#post).to receive(:save).and_return(true)
#result = SubmitPost.call(post: #post)
end
it 'should set the submitted_at date' do
expect(#post).to have_received(:submitted_at=)
end
it 'should call save' do
expect(#post).to have_received(:save)
end
it 'should return success' do
expect(#result.success?).to eq(true)
expect(#result.failure?).to eq(false)
end
end
context 'on failure' do
before(:each) do
allow(#post).to receive(:save).and_return(false)
#result = SubmitPost.call(post: #post)
end
it 'should return failure' do
expect(#result.success?).to eq(false)
expect(#result.failure?).to eq(true)
end
end
end
This is a Rails 4.1.4 application. Internally, SubmitPost sets submitted_at and calls save on the passed-in Post. My Post model looks like this:
class Post < ActiveRecord::Base
validates :title, presence: true
validates :summary, presence: true
validates :url, presence: true
validates :submitted_at, presence: true
scope :chronological, -> { order('submitted_at desc') }
end
It's super vanilla.
When I run rake, rspec, or bin/rspec, I get all all four tests failing 20% - 30% of the time. The error message is always:
Failure/Error: allow(#post).to receive(:submitted_at=)
Post does not implement: submitted_at=
If I label one of the specs with focus: true, that one spec will fail 100% of the time.
If I replace instance_double with double, all specs will succeed 100% of the time.
It appears that instance_double is having some difficulty inferring the methods available on the Post class. It also appears to be somewhat random and timing-based.
Has anyone run into this issue? Any ideas what might be wrong? Any sense of how to go about troubleshooting this? Naturally, inserting a debugging breakpoint causes the specs to pass 100% of the time.
The problem you are seeing is that ActiveRecord creates column methods dynamically. instance_double uses 'Post' to look up methods to verify you are stubbing them correctly (unless the class doesn't exist yet or has not been loaded).
When a prior spec loads the model, ActiveRecord will create those dynamic methods so your spec passes as RSpec can then find the methods (with a respond_to? call). When run in isolation the model hasn't been previously used and so ActiveRecord will not have created the dynamic methods yet and your test fails as you're experiencing.
The workaround for this is to force ActiveRecord to load the dynamic methods when they are called in your spec:
class Post < ActiveRecord::Base
def submitted_at=(value)
super
end
end
See the RSpec documentation for further explanation and workarounds for the problem:
https://www.relishapp.com/rspec/rspec-mocks/docs/verifying-doubles/dynamic-classes
Related
I was trying to practice Rspec, but seems I was confused about some rails part.
Here is Zombie.rb
class Zombie < ActiveRecord::Base
validates :name, presence: true
has_many :tweets
def hungry?
true
end
end
It seems when I create a Zombie instance, it will check the name attribute.
So, I wrote these Rspec code.
it 'should not be hungry after eating' do
#zombie = Zombie.create
#zombie.should_not be_valid
#zombie.hungry?.should be_truthy
end
Why it will pass? If the #zombie is not valid, why #zombie.hungry? will still return true
hungry? will always return true, therefore your expectation always passes.
That your instance is invalid doesn't mean that the instance is not valid Ruby. It is still a fully functional instance. The valid? method returns false, because the values of this instance are not valid to you, since you defined that it is not valid to you without a name.
From Ruby's point of view it is still a valid Zombie instance.
Btw since you just started to learn RSpec. You use the old RSpec syntax, you might want to learn the new syntax instead which would look like this:
describe Zombie do
context 'without a name' do
subject(:zombie) { Zombie.new }
it 'is not valid' do
expect(zombie).to_not be_valid
end
it 'is hungry' do
expect(zombie).to be_hungry
end
end
end
because your hungry? method always returns true
be_truthy # passes if obj is truthy (not nil or false) even if your
object is nil it's returns true
be_truthy
z = Zombie.create - It will not be created because of validation
z.hungry? => true
so your test passed
Testing Rails model validations with RSpec, without testing AR itself
Lets as setup we have model User:
class User < ActiveRecord::Base
validate :name, presence: true, uniqueness: { case_sensitive: false }, on: :create
validate :password, presence: true, format: { with: /\A[a-zA-z]*\z/ }
end
A see several ways to test this:
it { expect(user).to validate_presence_of(:name).on(:create) }
or
it do
user = User.create(name: '')
expect(user.errors[:name]).to be_present
end
My main question is which of the approaches is better and why? Can suggest me different approach?
Additional questions:
How much should I test? As an example, I can write so many tests for the regex, but it will be hell for maintenance.
How much you think will be full test coverage in this example?
The functionalities of:
Rails being able to validate the presence of an arbitrary value on your model
errors being added to an object for an attribute that is missing when a validation for it is configured
are covered in the tests for Rails itself (specifically, in the ActiveModel tests).
That leaves needing to write the tests for the config that covers the business logic of your app eg validating the presence of the specific name attribute on your specific User class etc. In my opinion, the matchers from the shoulda-matchers gem should have you covered:
RSpec.describe User, type: :model do
subject(:user) { build(:user) } # assuming you're using FactoryGirl
describe 'validations' do
specify 'for name' do
expect(user).to validate_presence_of(:name).on(:create)
# NOTE: saving here is needed to test uniqueness amongst users in
# the database
user.save
expect(user).to validate_uniqueness_of(:name)
end
specify 'for password' do
expect(user).to validate_presence_of(:password)
expect(user).to allow_value('abcd').for(:password)
expect(user).to_not allow_value('1234').for(:password)
end
end
end
I think that unless you have specific custom error messages for your errors that you want to test for (ie you've overridden the default Rails ones), then tests like expect(user.errors[:name]).to be_present can be removed (even if you have custom errors, I still think they're of dubious value since those messages will become locale-dependent if you internationalise your app, so I'd test for the display of some kind of error on the page in a feature spec instead).
I can write so many tests for the regex, but it will be hell for maintenance.
I don't think you can really get around this when testing validations for format, so I'd suggest just write some representative test cases and then add/remove those cases as you discover any issues you may have missed, for example:
# use a `let` or extract out into a test helper method
let(:valid_passwords) do
['abcd', 'ABCD', 'AbCd'] # etc etc
end
describe 'validations' do
specify 'for password' do
valid_passwords.each do |password|
expect(user).to allow_value(password).for(:password)
end
end
end
How much you think will be full test coverage in this example?
I've gotten 100% code coverage from reports like SimpleCov when writing unit specs as described above.
These 2 of them should be used, because:
it { expect(user).to validate_presence_of(:name).on(:create) }
=> You are expecting the validate_presence_of should be run on create, this should be the test case for model
it do
user = User.create(name: '')
expect(user.errors[:name]).to be_present
end
=> You are expecting a side effect when creating user with your input, so this should be the test case for controller
Why you shouldn't remove 1 of them:
Remove the 1st test case: what happens if you do database validation level instead, you expect an active record level validation
Remove the 2nd test case: what happens on controller actually creates a new User, how do you expect the error returning!
Sorry for the vague title, there are a lot of moving parts to this problem so I think it will only be clear after seeing my code. I'm fairly sure I know what's going on here and am looking for feedback on how to do it differently:
I have a User model that sets a uuid attr via an ActiveRecord callback (this is actually in a "SetsUuid" concern, but the effect is this):
class User < ActiveRecord::Base
before_validation :set_uuid, on: :create
validates :uuid, presence: true, uniqueness: true
private
def set_uuid
self.uuid = SecureRandom.uuid
end
end
I am writing a functional rspec controller test for a "foo/add_user" endpoint. The controller code looks like this (there's some other stuff like error-handling and #foo and #params being set by filters, but you get the point. I know this is all working.)
class FoosController < ApplicationController
def add_user
#foo.users << User.find_by_uuid!(#params[:user_id])
render json: {
status: 'awesome controller great job'
}
end
end
I am writing a functional rspec controller test for the case "foo/add_user adds user to foo". My test looks roughly this (again, leaving stuff out here, but the point should be obvious, and I know it's all working as intended. Also, just to preempt the comments: no, I'm not actually 'hardcoding' the "user-uuid" string value in the test, this is just for the example)
RSpec.describe FoosController, type: :controller do
describe '#add_user' do
it_behaves_like 'has #foo' do
it_behaves_like 'has #params', {user_id: 'user-uuid'} do
context 'user with uuid exists' do
let(:user) { create(:user_with_uuid, uuid: params[:user_id]) } # params is set by the 'has #params' shared_context
it 'adds user with uuid to #foo' do
route.call() # route is defined by a previous let that I truncated from this example code
expect(foo.users).to include(user) # foo is set by the 'has #foo' shared_context
end
end
end
end
end
end
And here is my user factory (I've tried setting the uuid in several different ways, but my problem (that I go into below) is always the same. I think this way (with traits) is the most elegant, so that's what I'm putting here):
FactoryGirl.define do
factory :user do
email { |n| "user-#{n}#example.com" }
first_name 'john'
last_name 'naglick'
phone '718-555-1234'
trait :with_uuid do
after(:create) do |user, eval|
user.update!(uuid: eval.uuid)
end
end
factory :user_with_uuid, traits: [:with_uuid]
end
end
Finally, The problem:
This only works if I reference user.uuid before route.call() in the spec.
As in, if I simply add the line "user.uuid" before route.call(), everything works as intended.
If I don't have that line, the spec fails because the user's uuid doesn't actually get updated by the after(:create) callback in the trait in the factory, and thus the User.find_by_uuid! line in the controller does not find the user.
And just to preempt another comment: I'm NOT asking how to re-write this spec so that it works like I want. I already know a myriad of ways to do this (the easiest and most obvious being to manually update user.uuid in the spec itself and forget about setting the uuid in the factory altogether). The thing I'm asking here is why is factorygirl behaving like this?
I know it has something to do with lazy-attributes (obvious by the fact it magically works if I have a line evaluating user.uuid), but why? And, even better: is there some way I can do what I want here (setting the uuid in the factory) and have everything work like I intend? I think it's a rather elegant looking use of rspec/factorygirl, so I'd really like it to work like this.
Thanks for reading my long question! Very much appreciate any insight
Your issue has less to do with FactoryGirl and more to do with let being lazily evaluated.
From the docs:
Use let to define a memoized helper method. The value will be cached across
multiple calls in the same example but not across examples.
Note that let is lazy-evaluated: it is not evaluated until the first time
the method it defines is invoked. You can use let! to force the method's
invocation before each example.
Since your test doesn't invoke the user object until the expectation there is nothing created. To force rspec to load object, you can use let!.
Instead of using the before_validation callback you should be using after_initialize. That way the callback is fired even before .valid? is called in the model lifecycle.
class User < ActiveRecord::Base
before_initialization :set_uuid!, on: :create, if: :set_uuid?
validates :uuid, presence: true, uniqueness: true
private
def set_uuid!
# we should also check that the UUID
# does not actually previously exist in the DB
begin
self.uuid = SecureRandom.uuid
end while User.where(uuid: self.uuid).any?
end
def set_uuid?
self.uuid.nil?
end
end
Although the chance of generating the same hash twice with SecureRandom.uuid is extremely slim it is possible due to the pigeonhole principle. If you maxed out in the bad luck lottery this would simply generate a new UUID.
Since the callback fires before validation occurs the actual logic here should be completely self contained in the model. Therefore there is no need to setup a callback in FactoryGirl.
Instead you would setup your spec like so:
let!(:user) { create(:user) }
it 'adds user with uuid to #foo' do
post :add_user, user_id: user.uuid, { baz: 3 }
end
I'm using the gem Frozen Record in order to setup a series of invariable questions in my Rails app, which will be accessed and used by other models. Given I'm used to Test Driven Development, I'd already set up a series of tests and validations before adding the Frozen Record gem:
class Question < FrozenRecord::Base
validates :next_question_id_yes, :question_text, :answer_type, presence: true
end
And the tests:
require 'rails_helper'
RSpec.describe Question, :type => :model do
let(:question) {Question.new(id: "1", question_text: "who's the daddy?", answer_type: 'Boolean', next_question_id_yes: '7', next_question_id_no: "9")}
it 'is valid' do
expect(question).to be_valid
end
#questions can be valid without a next_question_id_no, because some questions just capture info
#questions must have a next_question_id_yes as that will route them to the subsequent question
#if it's the last question in a block, we will use the next number up to signify the questions are complete
it 'is invalid without a next_question_id_yes' do
question.next_question_id_yes = nil
expect(question).to_not be_valid
end
it 'is invalid without a question_text' do
question.question_text = nil
expect(question).to_not be_valid
end
it 'is invalid without an answer_type' do
question.answer_type = nil
expect(question).to_not be_valid
end
end
All of these passed when I had the class Question < ActiveRecord::Base but since becoming a FrozenRecord I get:
rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/frozen_record-0.4.0/lib/frozen_record/base.rb:78:in `method_missing': undefined method `validates' for Question:Class (NoMethodError)
I presume there's no method in Frozen Record to validate the presence of these columns. What I'm less sure of is:
Do I need to perform validations on static data loaded via YAML? My first feeling was that this would be an additional check on my YAML code.
If I do, can I write custom validations to check this data?
Has anyone else used this gem and overcome the issue using Rspec? (it's been downloaded thousands of times according to RubyGems)
Any help would be appreciated.
Sorry to waste everyone's time - I just looked through the source code of FrozenRecord and worked out I can do it like this:
class Question < FrozenRecord::Base
include ActiveModel::Validations
validates :next_question_id_yes, :question_text, :answer_type, presence: true
self.base_path = '/config/initializers/questions.yml'
end
Should think before I ask next time.
I have a Reminder model that needs to calculate an important piece of data upon creation. I'm using the before_create callback for this purpose:
class Reminder < ActiveRecord::Base
validates :next_send_time, presence: true
before_create :set_next_send_time
def set_next_send_time
self.next_send_time = calc_next_send_time
end
end
The problem is, the callback doesn't seem to be running in my controller spec. The attribute is vital to the model and throughout the rest of my application's tests I will expect it to be calculated upon create.
The reminders_controller#create method is
def create
respond_with Reminder.create(reminder_params)
end
and here's the reminders_controller_spec:
RSpec.describe Api::RemindersController do
describe 'create' do
it "should work" do
post :create,
reminder: {
action: 'Call mom',
frequency: 1,
type: 'WeeklyReminder',
day_of_week: 0,
start_date: Date.new,
time_of_day: '07:00:00',
user_id: 1
},
format: :json
reminders = Reminder.all
expect(reminders.length).to eq(1)
end
end
end
If I inspect the response, the error is that next_send_time is null.
How can I get the Reminder's callback to run in my Rspec tests?
Instead of before_create, try before_validation :set_next_time, on: :create.
If the record doesn't pass validation, the callback wouldn't fire.
EDIT: Corrected the method name to before_validation.
If you're using Rails 4.0 you can use the [TestAfterCommit][1] gem.
If you're using Rails 5.0 it is built in: https://github.com/rails/rails/pull/18458