A user has many comments, so I would like to have a factory user with a comment associated to it (user_with_comment):
factory :user, class: User do |t|
...
factory :user_with_comment do |t|
after(:create) do |user|
FactoryGirl.create(:comment, user_id: user.id)
end
end
It works fine... when I call FactoryGirl.create(:user_with_comment), it creates the user and the related comment in my test db.
However, I'm facing some issues in the controller_spec:
Using let I have to reload the user to see the comment:
let(:user) { FactoryGirl.create(:user_with_comment) }
user.comments.size #=>0
user.reload
user.comments.size #=>1
One solution would be using before(:each), but it will create venda and comment before each test:
before(:each) do
#user = FactoryGirl.create(:user_with_comment)
end
#user.comments.size #=>1
Or, I can reload the userbefore each test, but it will also hit the database:
let(:user) { FactoryGirl.create(:user_with_comment) }
before(:each) do
user.reload
end
What is the best approach in this situation?
This has to do with the fact that let is lazy-evaluated and the comment for your user factory is added after the user is created.
Note that let is lazy-evaluated: it is not evaluated until the first time
the method it defines is invoked.
See the RSpec documentation on let and let!
Using let means the user created when you first use it in an example and thus the comment is created after that. You can use let! to create the user before each example to avoid the reload. This shouldn't negatively impact your tests if you're using the factory in all/most of the examples in the spec.
How about this?
let(:user) { FactoryGirl.create(:user_with_comment).reload }
Related
I am using let to create a user record using factory girl. However i want to use exactly the same variable across 2 tests in the context as the user_id and email are important to the external API i am sending.
However i had no luck making a single variable for using across the examples. Here is my current code
context "User" do
let(:user) { FactoryGirl.create(:user) }
it "should create user and return 'nil'" do
expect(send_preferences(user, "new")).to eq nil
end
it "should not create user preferences again after sending two consecutive same requests" do
expect(send_preferences(user, "new")).to eq "User preferences already saved. No need to re-save them."
end
it "should update user preferences" do
expect(send_preferences(user, "update")).to eq nil
end
end
any clues?
You can use lets within lets:
context "User" do
let(:email_address) { 'test#test.com' }
let(:user) { FactoryGirl.create(:user, email_address: email_address) }
You will then also have access to the email_address variable within all your tests.
This works because previously the email address was being randomly generated by the factory every time the user was created, as we hadn't set a value for it anywhere. So, we called the code below in each test:
send_preferences(user, "new")
It called the 'user' let which created a new user with a completely random email address (as we hadn't give it a specific email value). Therefore during the backend API call it was sending a different email address every time.
let(:user) { FactoryGirl.create(:user) }
However, when we defined the email address 'let' as 'test#test.com', and passed that into the user factory as in the code I provided, we overrode the randomly generated email address with our own static value, So, every time we call the code again:
send_preferences(user, "new")
It now triggers the user factory create which is also taking our new 'email_address' let, which is always set to a specific value of test#test.com every time it is called.
let(:email_address) { 'test#test.com' }
let(:user) { FactoryGirl.create(:user, email_address: email_address) }
Therefore, when the backend API call is made the email address is always what we set it to.
Also, as it is a let we can use that variable in any of the tests themselves if we wish. For example:
it 'should set the email address' do
expect(user.email_address).to eq(email_address)
end
It's quite hard to explain in a few sentences but let me know if that's still not clear.
Having an instantiated variable shared among multiple tests is an anti-pattern 90% of the time in my opinion.
The problem with doing something like the below is you will be creating objects in your db without doing a cleanup.
before(:all) do
#user = FactoryGirl.create :user
end
Sure, you can do a before(:after) block or use DatabaseCleaner, but I think it is much better practice for tests to be as standalone as possible. In your case, make your setup of a send_preferences event before making an expectation on what happens the second time:
context "User" do
let(:user) { FactoryGirl.create(:user) }
# ...
it "should not create user preferences again after sending two consecutive same requests" do
send_preferences(user, "new") # Setup
expect(send_preferences(user, "new")).to eq "User preferences already saved. No need to re-save them."
end
it "should update user preferences" do
send_preferences(user, "new") # Setup
expect(send_preferences(user, "update")).to eq nil
end
end
I was able to make the test work with the following code, but it seems to be weird and I don't totally understand it.
Can somebody tell me if creating the objects this way is the optimal one?
Why do I have to only use let! for the 2nd post_comment_reply creation and why don't I for the rest of the objects?
post_comment.rb
belongs_to :post, touch: true
belongs_to :user
has_many :post_comment_replies, dependent: :destroy
has_many :users, through: :post_comment_replies
def send_post_comment_reply_creation_notification(reply)
post_repliers = ([user] + [post.user] + users).uniq - [ reply.user ]
post_repliers.each do |replier|
Notification.create(recipient_id: replier.id, sender_id: reply.user_id, notifiable: self.post, action: "commented")
end
end
post_comment_spec.rb
describe "instance methods" do
let(:post_user) { create(:user) }
let(:comment_user) { create(:user) }
let(:reply_user) { create(:user) }
let(:reply_user_2) { create(:user) }
let(:post_reader) { create(:user) }
let(:post) { create(:post, user: post_user) }
let(:post_comment) { create(:post_comment, user: comment_user) }
let(:post_comment_reply) { create(:post_comment_reply, post_comment: post_comment, user: reply_user) }
let!(:post_comment_reply_2) { create(:post_comment_reply, post_comment: post_comment, user: reply_user_2) }
it "send_post_comment_reply_creation_notification" do
expect{
post_comment.send_post_comment_reply_creation_notification(post_comment_reply)
}.to change{Notification.count}.by(3)
end
end
let is lazy. If you don't reference it, it doesn't get evaluated and, in your case, side effects don't happen (side effect being the creation of database entry).
let!, on the other hand, is always evaluated.
Why you need a let!: let is lazy (it runs only when referred to); let! is eager (it runs before the test whether referred to or not). Your test needs to create :post_comment_reply twice; the let one works because the test refers to it, but the let! one isn't referred to so it has to be a let!, not a let.
Is it optimal? Your test setup works, but as we discovered it's not as clear as it could be. It also sets a trap for anyone adding more tests to the describe block that contains the let!: that object will be created before every test whether it's needed or not, slowing down all tests and possibly affecting the results.
Instead, I'd delete the let! and write this (lets not shown):
describe '#send_post_comment_reply_creation_notification' do
it "notifies each user who replies to the post_comment" do
create(:post_comment_reply, post_comment: post_comment, user: reply_user_2)
expect { post_comment.send_post_comment_reply_creation_notification(post_comment_reply) }.
to change { Notification.count }.by(3)
end
end
In general, prefer creating factory objects in examples (it blocks) rather than in let! blocks. In fact, prefer creation in examples to let as well, unless you're actually using the let variable in more than one example. (You only showed one example, but I suspect there are really more in the same describe block.) If you're only using a factory object in one test there is no reason to make the reader hunt around your test file for where it's defined, or to define a name available in other tests whether it's used there or not.
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
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.
The following spec ensures that a Project has a User:
it "requires a user" do
expect(FactoryGirl.build_stubbed(:project, user_id: nil)).to_not be_valid
end
But for some reason I feel compelled to do the following too:
context "user identity" do
let(:temp) { FactoryGirl.build_stubbed(:user) }
subject(:project) { FactoryGirl.build_stubbed(:project, user: temp) }
its(:user){ should == temp }
end
I know I need the first test, but I'm beginning to wonder if the second one is a waste of time, especially since the association is handled by the controller:
#project = current_user.projects.build
Is the second test pointless? Seems like it's just testing my factory more than anything.
Is the second test pointless? Seems like it's just testing my factory more than anything.
I think it is not necessary to test. You test has_many and belongs_to relations from core of Rails.