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
Related
In Rails models we usually have attributes and relations tests, like:
describe 'attributes' do
it { is_expected.to have_db_column(:identifier).of_type(:uuid) }
it { is_expected.to have_db_column(:content).of_type(:jsonb) }
it { is_expected.to have_db_column(:created_at).of_type(:datetime) }
end
describe 'relations' do
it { is_expected.to belong_to(:user).class_name('User') }
end
And using a TDD style it seems to be some useful tests, however I have been dwelling if these are really necessary tests, and I would like to know if there is some common knowledge about it, is it good practice to create these tests? or are we just testing rails?
Amongst the purposes of a unit test are...
Does it work?
Does it still work?
If it's a promise, if other things rely on it, you should test it to ensure you keep that promise. This is regression testing.
But don't test more than you promise. You'll be stuck with it, or your code will break when you make an internal change.
For example...
it { is_expected.to have_db_column(:identifier).of_type(:uuid) }
This promises that it has a column called identifier which is a UUID. Usually you don't promise all that detail; it is glass-box testing and it makes your test brittle.
Instead, promise as little as you can. Its ID is a UUID. This is black-box testing.
require "rspec/uuid"
describe '#id' do
subject { thing.id }
let(:thing) { create(:thing) }
it 'has a uuid ID' do
expect(thing.id).to be_a_uuid
end
end
It's possible there is an even higher level way to express this without holding yourself specifically to a UUID.
it { is_expected.to have_db_column(:content).of_type(:jsonb) }
Similarly, don't promise it has a jsonb column. That is blackbox testing. Promise that you can store complex data structures.
describe '#content' do
subject { create(:thing) }
it 'can round trip complex data' do
data = [1, { two: 3, four: [5] }]
thing.update!(content: data)
# Force it to re-load content from the database.
thing.reload
expect(thing.content).to eq data
end
end
it { is_expected.to belong_to(:user).class_name('User') }
Instead of promising what it belongs to, promise the relationship.
describe '#user' do
let(:thing) { create(:thing) }
let(:user) { create(:user) }
before {
user.things << thing
}
it 'belongs to a user' do
expect(thing.user).to eq user
expect(user.things).to contain(thing)
end
end
I have answered a nearly identical question here: https://stackoverflow.com/a/74195850/14837782
In summary: If it is end-developer code, I believe it should be tested. If it can be fat-fingered, I believe it should be tested. If you're going to remove it deliberately, I also believe you should have to remove a test deliberately as well. If it can fail, there should be a specific test for that failure mode.
This is not to be confused with testing the Rails framework. You obviously want to design your tests so that you're not testing Rails itself or Rails implementation, only your own code.
Attributes should be tested. Here is how I do it in minitest:
test/models/car_test.rb
class CarTest < ActiveSupport::TestCase
###################################################################
#
# Attributes
#
###################################################################
test 'describe some attr_reader fields' do
expected = [:year, :make, :model, :vin]
assert_has_attr_readers(Car, expected)
end
###############################################
test 'describe some attr_writer fields' do
expected = [:infotainment_fimrware_version]
assert_has_attr_writers(Car, expected)
end
###############################################
test 'describe some attr_accessor fields' do
expected = [:owner, :color, :mileage]
assert_has_attr_readers(Car, expected)
assert_has_attr_writers(Car, expected)
end
end
test/test_helpers/attributes_helper.rb
# frozen_string_literal: true
module AttributesHelper
###################################################################
#
# Assertions
#
###################################################################
#
# Performs an assertion that the given class contains reader/getter methods for the given attribute names.
# This helper checks for the existence of `attribute_name` methods on the class, and does not concern itself
# with how those methods are declared: directly defined, attr_reader, attr_accessor, etc.
#
def assert_has_attr_readers(klass, attribute_names)
# Get public and protected method names, passing `false` to exclude methods from super classes.
actual_method_names = klass.instance_methods(false).map(&:to_s)
attribute_names.each do |attribute|
message = "Expected class #{klass.name} to contain a reader for attribute #{attribute}"
assert_includes(actual_method_names, attribute.to_s, message)
end
end
#
# Performs an assertion that the given class contains writer/setter methods for the given attribute names.
# This helper checks for the existence of `attribute_name=` methods on the class, and does not concern itself
# with how those methods are declared: directly defined, attr_writer, attr_accessor, etc.
#
def assert_has_attr_writers(klass, attribute_names)
# Get public and protected method names, passing `false` to exclude methods from super classes.
actual_method_names = klass.instance_methods(false).map(&:to_s)
attribute_names.each do |attribute|
message = "Expected class #{klass.name} to contain a writer for attribute #{attribute}"
assert_includes(actual_method_names, "#{attribute}=", message)
end
end
#
# Performs an assertion that the given class implements attr_encrypted for the given attribute names.
# This helper is tied to the implementation details of the attr_encrypted gem. Changes to how attributes
# are encrypted will need to be accounted for here.
#
def assert_has_encrypted_attrs(klass, attribute_names)
message = "Expected class #{klass.name} to encrypt specific attributes"
actual_attributes = klass.encrypted_attributes.keys
assert_equal(attribute_names.map(&:to_s).sort, actual_attributes.map(&:to_s).sort, message)
end
end
Your example tests seem to be testing the existence of DB fields, not getter/setter model attributes. Database fields are impossible to fat-finger (they require a migration to modify) so if that's what you're talking about, I do not believe it makes sense to test them. (And I personally believe it is useful to test nearly everything.)
Although I guess in the case where the DB is accessible by other applications and could potentially be modified outside of a single application then it could make sense to test for the existence of those fields as well, as pointed out by Dave Newton in a comment below.
Ultimately it is up to you, and if your one application is the only one with access to the DB but you still want to test DB field existence and settings, maybe a 3rd option is some sort of migration test that you're looking for to make sure the migration is written properly. I've not written anything like that yet, but it might be feasible. I would hate to try to write one, and it does seem to go too far, but it's an idea...
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.
How can you test the presence of a callback in your model, specifically one that's triggered by creating a record, such as after_create or after_commit on: :create?
Here's an example callback with the (empty) method that it calls.
# app/models/inbound_email.rb
class InboundEmail < ActiveRecord::Base
after_commit :notify_if_spam, on: :create
def notify_if_spam; end
end
Here's the pending spec, using RSpec 3.
# spec/models/inbound_email_spec.rb
describe InboundEmail do
describe "#notify_if_spam" do
it "is called after new record is created"
end
end
Using a message expectation to test that the method is called seems like the way to go.
For example:
expect(FactoryGirl.create(:inbound_email)).to receive(:notify_if_spam)
But that doesn't work. Another way is to test that when a record is created, something inside the called method happens (e.g. email sent, message logged). That implies that the method did get called and therefore the callback is present. However, I find that a sloppy solution since you're really testing something else (e.g. email sent, message logged) so I'm not looking for solutions like that.
I think Frederick Cheung is right. This should work. The problem with your example is that the callback has already been called before the expectation has been set.
describe InboundEmail do
describe "#notify_if_spam" do
it "is called after new record is created" do
ie = FactoryGirl.build(:inbound_email)
expect(ie).to receive(:notify_if_spam)
ie.save!
end
end
end
I'm newbie with rspec and I'm facing some problems with it. Could someone help me?
I have a controller action responsible for deactivate an user. I'm trying to cover it with rspec tests, but the result is not what I'm waiting for.
Controller:
def deactivate
#user = User.find(params[:id])
if !#user.nil?
#user.update_attribute(:active, false)
redirect_to users_url
end
end
Controller Spec
describe "PUT #deactivate" do
describe "with valid parameters" do
before (:each) do
#user = mock_model(User, :id => 100, :login => "login", :password => "password123",
:email => "email#gmail.com", :active => true)
User.should_receive(:find).with("100").and_return(#user)
end
it "should deactivate an user" do
#user.stub!(:update_attribute).with(:active, false).and_return(true)
put :deactivate, :id => "100"
#user.active.should eq false
end
end
end
The test result:
1) UsersController PUT #deactivate with valid parameters should deactivate an user
Failure/Error: #user.active.should eq false
expected: false
got: true
(compared using ==)
So, I don't understand why the active attribute stills true when it should be false. Any ideas ?
Thanks!
You appear to be stubbing the update_attribute method unnecessarily. Try removing that line and see what happens.
I look for this for a long time, update_column can always work no matter you use let or build
Your expectation is "wrong".
Let's see what happens when your spec it "should deactivate an user" is executed:
#user.stub!(:update_attribute).with(:active, false).and_return(true) modifies the existing mock model, so it has an update_attribute which, when called with arguments :active and false
will return true
will keep track that this call has happened (that's what mocks do)
(and, unlike a real User object, will do nothing else)
put :deactivate, :id => "100" calls the real deactivate in your Controller
Your Controller calls User.find. But you've mocked that class method, which will return the mock object #user instead of searching for the actual user with that id.
Your Controller calls #user.update_attribute. But because of step 3 above, #user here is the mock object, too. Its update_attributes method is the one from step 1. As we've seen above, it will return true, keep track that this call happened and do nothing else. Which means it will not change #user's active attribute, so that stays true.
Changing active when update_attribute is called is functionality of objects of the actual User class, but no such object came into play while running your spec. Because this functionality is inherited from ActiveRecord, you don't have to test it. Instead just test that the update_attribute has been received by the mock object:
it "should deactivate an user" do
#user.stub!(:update_attribute).with(:active, false).and_return(true)
put :deactivate, :id => "100"
#user.should have_received(:update_attribute).with(:active, false)
end
(I'm guessing about the old should syntax here, based on how it's done with the newer expect syntax.)
To mock or not?
If you do want to test the combined functionality of your controller with the actual User implementation, do not mock User or its objects. Instead test from the browser perspective with a request spec. (It might make sense to do that additionally, even if you want the isolated tests for only controller (with model mocked) and for only model (which probably won't require doubles, except maybe for other models).
Can you try this:
describe "should deactivate an user" do
before do
#user.stub!(:update_attribute).with(:active, false).and_return(true)
put :deactivate, :id => "100"
end
it { #user.active.should eq false }
end
when you are mocking the call to update_attribute, how is the model going to change?
if you are a beginner: DONT use stubs and mocks!
first get a general knowledge in testing, THEN expand your knowledge to mocks and stubs.
I am finding it very hard to stub certain attributes of a model on a controller test. I want to make sure to stub as little as possible.
EDIT: I have been demoved of using stubs for such integration. I understood that the stubs won't reach the action call. The correct question would now be:
How can one use mocks and stubs to simulate a certain state in a Rails controller test?
So I've reached something like the following:
Spec
require 'spec_helper'
describe TeamsController do
let(:team) { FactoryGirl.create :team }
context "having questions" do
let(:competition) { FactoryGirl.create :competition }
it "allows a team to enter a competition" do
post(:enter_competition, id: team.id, competition_id: competition.id)
assigns(:enroll).team.should == team
assigns(:enroll).competition.should == competition
end
end
# ...
end
Factories
FactoryGirl.define do
factory :team do
name "Ruby team"
end
factory :competition, class: Competition do
name "Competition with questions"
after_create do |competition|
competition.
stub(:questions).
and_return([
"something"
])
end
end
factory :empty_competition, class: Competition do
name "Competition without questions"
questions []
after_create do |competition|
competition.stub(:questions).and_return []
end
end
end
Production code
class TeamsController < ApplicationController
def enter_competition
#team = Team.find params[:id]
#competition = Competition.find params[:competition_id]
#enroll = #team.enter_competition #competition
render :nothing => true
end
end
class Team < ActiveRecord::Base
def enter_competition competition
raise Competition::Closed if competition.questions.empty?
enroll = Enroll.new team: self, competition: competition
enroll.save
enroll
end
end
When I run the test, the questions attribute comes as being nil and so the test fails in the model when checking for nil.empty?.
Why isn't the stub being used so that the state of that message is correctly used? I expected that #competition.questions would be [ "question" ] but instead I get nil.
The problem you're running into is that stub works on an instance of a Ruby object; it doesn't affect all ActiveRecord objects that represent the same row.
The quickest way to fix your test would be to add this to your test, before the post:
Competition.stub(:find).and_return(competition)
The reason that's necessary is that Competition.find will return a fresh Competition object that doesn't have questions stubbed out, even though it represents the same database row. Stubbing find as well means that it will return the same instance of Competition, which means the controller will see the stubbed questions.
I'd advise against having that stub in your factory, though, because it won't be obvious what's stubbed as a developer using the factory, and because it means you'll never be able to test the real questions method, which you'll want to do in the Competition unit test as well as any integration tests.
Long story short: if you stub out a method on an instance of your model, you also need to stub out find for that model (or whatever class method you're using to find it), but it's not a good idea to have such stubs in a factory definition.
When you call create on FactoryGirl, it creates database records which you then retrieve back in your controller code. So the instances you get (#team, #competition) are pure ActiveRecord, without any methods stubbed out.
Personally I would write you test like this (not touching database at all):
let(:team) { mock_model(Team) }
let(:competition) { mock_model(Competition) }
before do
Team.stub(:find) { team }
Competition.stub(:find) { competition }
end
and then in your test something like this:
it "should call enter_competition on #team with #competition" do
team.should_receive(:enter_competition).with(competition)
post :enter_competition, id: 7, competition_id: 10
I don't really understand what your controller is supposed to do or what are you testing for that matter, sorry :(