Rspec unit tests failing due to previous tests - ruby-on-rails

I'm new to Rspec and I'm putting in tests for an old project. I'm having issues with setting variables. Bascially my User object is affected by actions in previous tests.
I have a simple user factory:
FactoryGirl.define do
factory :user do
first_name 'Bob'
last_name 'Shabbadoo'
office
login 'lurker48'
email 'test.user#test.com'
password 'TestWhatNots#21'
password_confirmation{|u| u.password}
end
end
Then I have my actual rspec tests.
require "spec_helper"
describe User do
it 'has a valid factory' do
FactoryGirl.build(:user).should be_valid
end
describe '#password' do
it "cannot contain the word 'password'" do
valid_user = FactoryGirl.build(:user)
valid_user.password << "password"
valid_user.password_confirmation = valid_user.password
valid_user.should_not be_valid
end
it "cannot contain the users last_name" do
valid_user = FactoryGirl.build(:user)
valid_user.password << valid_user.last_name
valid_user.password_confirmation = valid_user.password
valid_user.should_not be_valid
end
it "cannot contain the users first_name" do
valid_user = FactoryGirl.build(:user)
valid_user.password << valid_user.first_name
valid_user.password_confirmation = valid_user.password
valid_user.should be_valid
end
end
end
I purposefully made the "cannot contain the users first_name" test fail
and as I expected I got this:
User
has a valid factory
#password
cannot contain the word 'password'
cannot contain the users last_name
cannot contain the users first_name (FAILED - 1)
But when I took a closer look a the password it looked like this:
TestWhatNots#21passwordShabbadooBob
Why would actions in previous tests taint the information?

When you build new user object, factory girl assigns to its password given string (it is, to the instance of String class). Every time you do this, you user password is pointing to that object - if you build two users, their password are not only identical - the are the same object.
By using << method youre altering this object and hence you are altering password value for all objects which have been or will be created by this factory.
u1 = FactoryGirl.build(:user)
u2 = FactoryGirl.build(:user)
u1.password.object_id == u2.password.object_id #=> true!
Solutions:
Option 1. Do not modify the object but assign a new string object instead:
valid_user.password += valid_user.first_name
Option 2. Wrap all your values within a block. This will force FactoryGirl to run block each time you build new object and hence to create separate but identical String instances:
factory :user do
first_name { 'Bob' }
last_name { 'Shabbadoo' }
...

Related

Rails Rspec/Factory Bot isn't invoking model before_save callbacks

I have a user model with a number of before_save callbacks - for example, one that strips leading and trailing whitespace:
app/models/user.rb:
def strip_whitespace_in_user_names
self.first_name.strip!
self.first_name.gsub!(" ", "")
self.last_name.strip!
self.last_name.gsub!(" ", "")
end
I have a basic model spec, and I'd like to check that this actually works. For example, " Nathan " should return "Nathan"
spec/models/user_spec.rb:
RSpec.describe User, type: :model do
let(:user) { build :poorly_defined_user }
it "has no leading white space" do
expect(user.first_name).not_to end_with(" ")
end
end
Here is the factory definition of a poorly_defined_user:
require 'faker'
password = Faker::Internet.password
# Factory to define a user
FactoryBot.define do
factory :poorly_defined_user, class: User do
first_name " asd "
last_name "AS DF "
handle "BLASDF824"
email Faker::Internet.email
password password
password_confirmation password
end
end
However, when I run the tests, this expectation fails. I checked on postman (this is for an API), and the callbacks are correctly run, and the user's attributes are properly set.
Any help as to why this is happening, or, how to restructure my tests to reflect how Rspec/Factory Bot actually work.
Change build to create like so:
let(:user) { create :poorly_defined_user }
When calling build, the object is not actually saved to the db so the callbacks don't fire.

How to share a variable with many "it" examples in rspec

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

FactoryGirl.build not returning an object per definition

I have a User class with a save method which makes a change to one of the user instance attributes. Specifically, each user has an options hash that gets one of its values deleted during the save process.
I have an rspec test with 2 context groups. Each group creates a new #user object using FactoryGirl.build(:user). When I call #user.save in the first context group, the attribute change occurs as expected. However, the second time that FactoryGirl.build(:user) gets called, it doesn't return a User object according to the FactoryGirl definition. It returns a user object with an options hash that is missing the same value that gets deleted during the save process. This object is not valid, and as a result #user.save fails the second time.
UPDATE: I tried changing the variable names and I still have the same problem. The issue seems to be with the FactoryGirl :user factory being modified somehow during the first example, resulting in the second example failing.
Below is a simplified version of my code. Whichever context group is executed second ("with avatar" or "without avatar") when run randomly by Rspec is the one that fails. I have used puts in both cases to confirm that the second #user has a bad options hash, and causes the test to fail.
describe "save" do
context "with avatar" do
before(:context) do
#user = FactoryGirl.build(:user)
puts #user
#save_result = #user.save
end
after(:context) do
delete_user(#user)
end
it "should return true" do
expect(#save_result).to be true
end
end
context "without avatar" do
before(:context) do
#user = FactoryGirl.build(:user, avatar: nil)
puts #user
#save_result = #user.save
end
after(:context) do
delete_user(#user)
end
it "should return true" do
expect(#save_result).to be true
end
end
end
I suspect that the options hash gets reused.
According to the FactoryGirl readme, when you want to add a hash attribute to a FactoryGirl definition and that hash is dynamic (i.e. not the same among all created instances), you need to wrap it in a block:
Instead of:
factory :user do
options { option1: 1, option2: 2 }
end
You need to do:
factory :user do
options { { option1: 1, option2: 2 } }
end

RSpec test for not allowing two users with the same email address

First of all I should probably mention that I'm very new to Rails and this is my first "serious" project, so I apologise if this is a simple question but I can't find an answer for it.
I'm using TDD in my project and am using RSpec to write the model tests, FactoryGirl to create the models and Faker to create dummy data for the models. Everything has been going really well until I added a test to make sure no two users have the same email address. In my User model I validated it like so:
# /app/models/user.rb
validates :email, :password_reset_code, :auth_token, uniqueness: true
My factory creates a user model with Faker, like so:
# /spec/factories/users.rb
FactoryGirl.define do
factory :user do
email { Faker::Internet.email }
password { Faker::Internet.password }
password_reset_code { Faker::Lorem.word }
auth_token { Faker::Lorem.word }
end
end
and my user_spec.rb test for this is as follows:
# /spec/models/user_spec.rb
it "is invalid with a duplicate email" do
user = FactoryGirl.create(:user)
FactoryGirl.create(:user, email: user.email).should_not be_valid
end
Here I'm creating a new model with FactoryGirl using its dummy values from Faker, saving it to the database and then creating another one with the same email as the first one. I'd expect RSpec to tell me this test passed because of the should_not be_valid part. But instead I get this output when I run the test:
Failures:
1) User is invalid with a duplicate email
Failure/Error: FactoryGirl.create(:user, email: user.email).should_not be_valid
ActiveRecord::RecordInvalid:
Validation failed: Email has already been taken
# ./spec/models/user_spec.rb:19:in `block (2 levels) in <top (required)>'
So it seems that the model validation is raising an error which RSpec isn't catching and using to pass the test? I've managed to work around it by changing the test to this:
it "is invalid with a duplicate email" do
begin
user = FactoryGirl.create(:user)
FactoryGirl.create(:user, email: user.email).should_not be_valid
rescue
false
end
end
which seems to work, however I have a feeling this isn't the best way to do it.
What's the proper way to write this test?
I ran into this problem too. The error you're encountering is due to the fact that the create() method actually persists the model, which is then throwing an ActiveRecord::RecordInvalid at the DB layer (well, the persistence layer). If you want to assert if a model is valid or not you should use the build() method for your second object and then ask if it's valid. You can read up on it in this post.
Additionally, if you're just trying to test various validations on models and what not I wrote a quick and dirty gem that you can use to assert some of the more basic model validations. You can check it out here.
Hope that helps.
I would go with:
# /spec/models/user_spec.rb
describe 'validations' do
context 'with a duplicate email' do
let(:other_user) { FactoryGirl.create(:user) }
let(:attributes) { FactoryGirl.attributes_for(:user) }
subject(:user) { User.new(attributes.merge(email: user.email)) }
it 'is not valid' do
expect(user).to_not be_valid
end
end
end

FactoryGirl and Rspec

I've very green to this TDD business, so any help would be fantastic!
So, I've got a factory with the following:
FactoryGirl.define do
factory :account do
email "example#example.com"
url "teststore"
end
end
And an Rspec test with:
it "fails validation without unique email" do
account1 = FactoryGirl.create(:account)
account2 = FactoryGirl.create(:account)
account2.should have(1).error_on(:email)
end
I get a failure with the following message:
1) Account fails validation without unique email
Failure/Error: account2 = FactoryGirl.create(:account)
ActiveRecord::RecordInvalid:
Validation failed: Email taken, please choose another, Url taken, please choose another
# ./spec/models/account_spec.rb:11:in `block (2 levels) in <top (required)>'
Is this the correct way to create new factories? Any ideas what I'm doing wrong here (I have no doubt I'm doing something totally incorrect!)
EDIT: I'm thinking of instead of using 'create' on the second account, I may want to use .build and then .save instead?
Save yourself the database interactions and use the build method for situations like this.
it "fails validation without unique email" do
account1 = create(:account)
account2 = build(:account)
account2.should_not be_valid
account2.should have(1).error_on(:email)
end
You don't need to try and create an account for valid? to return false. You have access to the errors object on the account even when it's just built in memory. This will decrease database interactions and thus making your tests much faster.
Have you considered using sequences in your factories? I don't know how far along you are with your RSpec / FactoryGirl experience, but you will find that things like the following are very useful.
factories.rb
factory :account do
sequence(:email) { |n| "user#{n}#example.com" }
url "teststore"
end
Every time you call build or create on the account factory, you will get unique emails.
Remember that you can always specify values for the attributes on the factory using the options hash. So when testing your uniqueness validation on the account, you would do something like this.
it "fails validation without unique email" do
account1 = create(:account, :email => "foo#bar.com")
account2 = build(:account, :email => "foo#bar.com")
account2.should_not be_valid
account2.should have(1).error_on(:email)
end
Try this:
FactoryGirl.create(:account)
lambda {
FactoryGirl.create(:account)
}.should raise_error(ActiveRecord::RecordInvalid)
This will help - with similar syntax to what you're doing.
However, searching for "rspec validate_uniqueness_of" will find you some more elegant approaches instead of using factory girl like this!

Resources