I am trying to refactor some RSpec/Rails tests so that they persist as few objects to the database as possible, but am having trouble trying to figure out how to re-write tests like the following:
describe User do
context "record creation" do
before(:each) { #user = User.new(user_atts) }
it "should generate a confirmation_token" do
# Generated as the result of a callback
#user.save!
expect(#user.confirmation_token).to be_present
end
it "should set the confirmed_at attribute to nil" do
# Cleared as the result of a callback
#user.save!
expect(#user.confirmed_at).to be_nil
end
it "should call the send_confirmation_instructions method" do
#user.should_receive(:send_confirmation_instructions) {}
#user.save!
end
end
def user_atts
# return attributes hash
end
end
This is a pretty simple example, but there are plenty of similar instances in my specs, and, for the most part, they all persist records to the database. I would love to take advantage of RSpec's let and subject helpers, but am not fully sure that those would even help here.
I have been using FactoryGirl a lot and thought that maybe its build_stubbed strategy would speed up my specs a bit, but I couldn't find many instances where it would help limit actual record creation (or maybe I don't know how to use).
I assume there are some cases where a test requires record creation, but the above example hardly seems like one of them. Should I even be trying to refactor this or is there a better to write these tests? Any help would be greatly appreciated.
My tests would probably look something like this.
describe User do
let(:user) { FactoryGirl.build_stubbed(:user) }
context "record creation" do
it "should generate a confirmation_token" do
user.save!
expect(user.confirmation_token).to be_present
end
it "should set the confirmed_at attribute to nil" do
user.save!
expect(user.confirmed_at).to be_nil
end
it "should call the send_confirmation_instructions method" do
expect(user).to receive(:send_confirmation_instructions).once
user.save!
end
end
end
That's using Factory Girl to create the user models. Also, I'd have DatabaseCleaner to clear the database after each test as stated by #RahulGarg
All you'd have to do is configure in your spec_helper something like this
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
This means after each test the Database would be cleared.
Related
I have a model called "availabilities" in my Rails application that allows vendors to set their availability (i.e. their work hours). Thus, availabilities belong_to vendors and belong_to users, and a user has_many vendors and a vendor has_many availabilities.
I've been trying to create Rspec tests for my availability#destroy action. The specific test I refer to is:
#spec/controllers/availabilities_controller_spec.rb
require 'rails_helper'
RSpec.describe AvailabilitiesController, type: :controller do
describe "availabilities#destroy action" do
it "should allow a user who created the availability to destroy it"
availability = FactoryBot.create(:availability)
sign_in availability.user
delete :destroy, params: { id: availability.id, vendor_id: availability.vendor_id}
availability = Availability.find_by_id(availability.id)
expect(availability).to eq nil
end
end
end
When I run this test however, I receive the following error:
"An error occurred while loading ./spec/controllers/availabilities_controller_spec.rb.
Failure/Error: user = FactoryBot.create(:user)
ActiveRecord::RecordInvalid:
Validation failed: Email has already been taken"
However, I use factory bot for my factories, and I have my user factory to run as a sequence (see below):
FactoryBot.define do
factory :user do
sequence :email do |n|
"dummyEmail#{n}#gmail.com"
end
password "secretPassword"
password_confirmation "secretPassword"
confirmed_at Time.now
end
end
How can the email already be taken? What could explain this error?
I recommend you to use Faker along with FactoryBot. It will give your more flexibility and eliminate the need to do this sequence trick. Faker generates fake data with ease.
In any way, use database_cleaner to clean your test environment database after each test. You'll only need to set this is up:
# ./spec/rails_helper.rb
# start by truncating all the tables but then use the faster transaction strategy the rest of the time.
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
DatabaseCleaner.strategy = :transaction
end
# start the transaction strategy as examples are run
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
I have a feature/integration spec that runs through an entire user flow. By the end of that user flow, a page in the app will display a few records from my Postgres database. When I run the test by itself, it passes. I can see it saving correctly through the various steps since the selenium driver opens up firefox: Capybara.current_driver = :selenium. However, this spec fails regularly, almost predictably, when it is run after a bunch of controller specs. In those controller specs the only interesting thing that I am doing is running this login function:
module DeviseMacros
def login_user
before(:each) do
#request.env['devise.mapping'] = Devise.mappings[:user]
user = create(:user_with_active_account)
sign_in user
end
end
end
So I call it like this:
describe AwesomeController do
login_user
it 'responds with 200' do
get :new
expect(response.status).to eq 200
end
end
When run after a controller spec I can immediately see that the test will fail since certain UI elements should appear depending on what's in the DB and clearly they are not present.
My DatabaseCleaner strategy is as follows:
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, js: true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
Through trial and error I changed
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
to
config.before(:each) do
DatabaseCleaner.strategy = :truncation
end
And walla, it passes. Of course now the test suite takes over 2x as long.
I have tagged all my :selenium tests with js: true in order to ensure :truncation is used for them but that really doesn't matter since :selenium is already driving those. However, most importantly, this feature spec still fails after those controller specs.
Where else should I be looking? How do I proceed with debugging?
The only other unique thing I have in here that may be related is:
# In spec/support/shared_db_connection.rb
# https://gist.github.com/josevalim/470808
class ActiveRecord::Base
mattr_accessor :shared_connection
##shared_connection = nil
def self.connection
##shared_connection || retrieve_connection
end
end
# Forces all threads to share the same connection. This works on
# Capybara because it starts the web server in a thread.
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
Any advice or ideas on how to proceed with debugging would be greatly appreciated.
If you have any additional questions please ask. Thank you
Update: June 1, 2016
The exact line of code that causes the failure is:
module DeviseMacros
def login_user
before(:each) do
#request.env['devise.mapping'] = Devise.mappings[:user]
user = create(:user_with_active_account)
sign_in user # <----- THIS GUY RIGHT HERE! When removed, the ControllerSpec fails but the integration test passes.
end
end
end
So for some reason it appears that hitting the DB with this controller spec (which uses :transaction strategy) effects the feature spec (which uses :truncation strategy).
I'm debating just not hitting the DB at all in the controller specs when trying to authenticate a devise user, which is cool with me, but I feel like it shouldn't have to be that way. Any ideas on how to proceed if I do indeed want to be able to use the sign_in method?
Thank you
Remove the shared connection hack, since it causes more issues than it's worth, and update your Database Cleaner config to the recommended one from the database cleaner README - https://github.com/DatabaseCleaner/database_cleaner#rspec-with-capybara-example
here is my rspec code:
describe User do
before{(#user=User.new(username:"abcdefg",email:"123456#123.com",password:"123456")}
subject(#user)
#user.save
end
and I got such an error : undefined method 'save' for nil:NilClass(NoMethodError)
I try to write the same code in the rails console,it just worked. But when it comes to Rspec,it failed and I'm not able to find any reason...
Could any one help me with it?
here is the Rspec way:
describe User do
let(:valid_user) { User.new(username:"abcdefg",email:"123456#123.com",password:"123456") }
it "can be saved" do
expect(valid_user.save).to be_true
end
end
Note that you should avoid database operations in your specs, it's what make them slow.
Another point, consider using factories to clean up your specs.
You need to wrap the code in an example block (i.e., call the it method with a block), because in the context of the describe block, #user is not defined. For example:
describe User do
before{(#user=User.new(username:"abcdefg",email:"123456#123.com",password:"123456")}
subject(#user)
it "can be saved" do
#user.should respond_to(:save)
#user.save.should_not be_false
end
end
Edit: I noticed also that you have subject(#user) but that may need to be a block in order to set it properly. The following is cleaner overall:
describe User do
let(:user) { User.new(username:"abcdefg",email:"123456#123.com",password:"123456") }
it "can be saved" do
user.should respond_to(:save)
user.save.should_not be_false
end
end
I'm an RSpec newb, but am really loving how easy it is to write the tests and I'm continually refactoring them to be cleaner as I learn new features of RSpec. So, originally, I had the following:
describe Account do
context "when new" do
let(:account) { Account.new }
subject { account }
it "should have account attributes" do
subject.account_attributes.should_not be_nil
end
end
end
I then learned about the its method, so I tried to rewrite it as such:
describe Account do
context "when new" do
let(:account) { Account.new }
subject { account }
its(:account_attributes, "should not be nil") do
should_not be_nil
end
end
end
This fails due to its not accepting 2 arguments, but removing the message works just fine. The issue is that if the test fails, the message under the Failed examples section just says
rspec ./spec/models/account_spec.rb:23 # Account when new account_attributes
which isn't overly helpful.
So, is there a way to pass a message to its, or better yet, have it output a sane message automatically?
You could define an RSpec custom matcher:
RSpec::Matchers.define :have_account_attributes do
match do |actual|
actual.account_attributes.should_not be_nil
end
failure_message_for_should do
"expected account_attributes to be present, got nil"
end
end
describe Account do
it { should have_account_attributes }
end
You can also write: its(:account_attributes) { should_not be_nil }
See https://www.relishapp.com/rspec/rspec-core/v/2-14/docs/subject/attribute-of-subject
Take note that "its" will be extracted from rspec-core to a gem with the release of rspec 3, though.
Looks like a relatively simple monkey-patch will enable what you seek.
Look at the source of the rspec-core gem version you're using. I'm on 2.10.1. In the file lib/rspec/core/subject.rb I see the its method defined.
Here's my patched version - I changed the def line and the line after that.
Caution - this is very likely to be version specific! Copy the method from your version and modify it just like I did. Note that if the rspec-core developers do a major restructuring of the code, the patch may need to be very different.
module RSpec
module Core
module Subject
module ExampleGroupMethods
# accept an optional description to append
def its(attribute, desc=nil, &block)
describe(desc ? attribute.inspect + " #{desc}" : attribute) do
example do
self.class.class_eval do
define_method(:subject) do
if defined?(#_subject)
#_subject
else
#_subject = Array === attribute ? super()[*attribute] : _nested_attribute(super(), attribute)
end
end
end
instance_eval(&block)
end
end
end
end
end
end
end
That patch can probably be put in your spec_helper.rb.
Now the usage:
its("foo", "is not nil") do
should_not be_nil
end
Output on failure:
rspec ./attrib_example_spec.rb:10 # attr example "foo" is not nil
If you omit the second arg, the behavior will be just like the unpatched method.
I have started my journey with TDD in Rails and have run into a small issue regarding tests for model validations that I can't seem to find a solution to. Let's say I have a User model,
class User < ActiveRecord::Base
validates :username, :presence => true
end
and a simple test
it "should require a username" do
User.new(:username => "").should_not be_valid
end
This correctly tests the presence validation, but what if I want to be more specific? For example, testing full_messages on the errors object..
it "should require a username" do
user = User.create(:username => "")
user.errors[:username].should ~= /can't be blank/
end
My concern about the initial attempt (using should_not be_valid) is that RSpec won't produce a descriptive error message. It simply says "expected valid? to return false, got true." However, the second test example has a minor drawback: it uses the create method instead of the new method in order to get at the errors object.
I would like my tests to be more specific about what they're testing, but at the same time not have to touch a database.
Anyone have any input?
CONGRATULATIONS on you endeavor into TDD with ROR I promise once you get going you will not look back.
The simplest quick and dirty solution will be to generate a new valid model before each of your tests like this:
before(:each) do
#user = User.new
#user.username = "a valid username"
end
BUT what I suggest is you set up factories for all your models that will generate a valid model for you automatically and then you can muddle with individual attributes and see if your validation. I like to use FactoryGirl for this:
Basically once you get set up your test would look something like this:
it "should have valid factory" do
FactoryGirl.build(:user).should be_valid
end
it "should require a username" do
FactoryGirl.build(:user, :username => "").should_not be_valid
end
Here is a good railscast that explains it all better than me:
UPDATE: As of version 3.0 the syntax for factory girl has changed. I have amended my sample code to reflect this.
An easier way to test model validations (and a lot more of active-record) is to use a gem like shoulda or remarkable.
They will allow to the test as follows:
describe User
it { should validate_presence_of :name }
end
Try this:
it "should require a username" do
user = User.create(:username => "")
user.valid?
user.errors.should have_key(:username)
end
in new version rspec, you should use expect instead should, otherwise you'll get warning:
it "should have valid factory" do
expect(FactoryGirl.build(:user)).to be_valid
end
it "should require a username" do
expect(FactoryGirl.build(:user, :username => "")).not_to be_valid
end
I have traditionally handled error content specs in feature or request specs. So, for instance, I have a similar spec which I'll condense below:
Feature Spec Example
before(:each) { visit_order_path }
scenario 'with invalid (empty) description' , :js => :true do
add_empty_task #this line is defined in my spec_helper
expect(page).to have_content("can't be blank")
So then, I have my model spec testing whether something is valid, but then my feature spec which tests the exact output of the error message. FYI, these feature specs require Capybara which can be found here.
Like #nathanvda said, I would take advantage of Thoughtbot's Shoulda Matchers gem. With that rocking, you can write your test in the following manner as to test for presence, as well as any custom error message.
RSpec.describe User do
describe 'User validations' do
let(:message) { "I pitty da foo who dont enter a name" }
it 'validates presence and message' do
is_expected.to validate_presence_of(:name).
with_message message
end
# shorthand syntax:
it { is_expected.to validate_presence_of(:name).with_message message }
end
end
A little late to the party here, but if you don't want to add shoulda matchers, this should work with rspec-rails and factorybot:
# ./spec/factories/user.rb
FactoryBot.define do
factory :user do
sequence(:username) { |n| "user_#{n}" }
end
end
# ./spec/models/user_spec.rb
describe User, type: :model do
context 'without a username' do
let(:user) { create :user, username: nil }
it "should NOT be valid with a username error" do
expect(user).not_to be_valid
expect(user.errors).to have_key(:username)
end
end
end