I'm currently developing a RoR 4 application.
In my application, I'm trying to implement Users tests similar to Mickael Hartl's tutorial. I'd like to understand a few things about the eMail uniqueness test :
1 - Uniqueness is tested when actually writing to the database, right ?
When writing my test the same way as in the tutorial, it is not efficient.
The #user = User.new(...) actually instantiates the user in memory only. The following test:
describe "when email address is already used" do
before do
user_with_same_email = #user.dup
user_with_same_email.save
end
it { should_not be_valid }
end
works only, in my case, when the #user = user.new(...) instruction is followed by a user.save instruction. Am I right ?
Additional note: when email uniqueness is implemented in the user model, it always fails !?!
Can you tell me what is wrong with this test ?
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, uniqueness: true, length: { maximum: 100 }, format: { with: VALID_EMAIL_REGEX }
2 - Does RSPEC really write to the TEST database ?
After running the above test (user.new(...) - user.save - user.dup - user.save), I check the 'modified date' of the TEST SQLite3 database file. It has not been touched when running the test. The spec_hepler.rb files starts with:
ENV["RAILS_ENV"] ||= 'test'
Did I miss something ?
Thank you for your help,
Best regards,
Fred
About your first point, the validation for uniqueness indeed occurs when you call save. However, you can also invoke all the validations by calling valid? - which is what be_valid does. So it doesn't actually need to save the #user model for this test to work.
About your second point, RSpec absolutely writes to the database. However, it runs each test within a transaction scope (by default). At the end of each test, instead of committing the transaction, it rolls it back. This leaves the database in the same state it was before the test started, effectively allowing each test to be isolated from the others while making use of the database just as your code would do in production (almost). Regarding your point about the file modification time, SQLite doesn't necessarily write out the data to the file as you make calls until they are actually committed.
Related
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!
Working my way through Michael Hartl's excellent tutorial for Ruby on Rails. I'm at the point where he creates a test that checks for duplicate email addresses, and I'm a little confused about his use of upcase, downcase, and case-insensitive checks.
The test (Listing 6.17) looks like this:
describe User do
before do
#user = User.new(name: "Example User", email: "user#example.com")
end
.
.
.
describe "when email address is already taken" do
before do
user_with_same_email = #user.dup
user_with_same_email.email = #user.email.upcase
user_with_same_email.save
end
it { should_not be_valid }
end
end
Note the call to upcase. All fine. But in his validity check (6.18), he sets case sensitivity off.
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
What? Why did he convert the copy to uppercase if he was going to do a case-insensitive validation?
Finally, in 6.20, he sets up a before_save block that converts a new user's email to lowercase.
before_save { self.email = email.downcase }
That makes perfect sense, because you want lowercase in your database. But I'm confused as to why he used uppercase in the test, given that the save is going to convert the email address to lowercase anyway. Am I missing something obvious?
seems to me that by converting to upper case, and storing the data as lower case, you make sure they're not equivalent, so the "case_sensitive:false" part of the validator will really be tested.
Checking that #user.email.upcase is invalid ensures the uniqueness of this value no matter what the case is.
When you write uniqueness: { case_sensitive: false } you force uniqueness, no matter what the case is: "foo" is equivalent to "fOO".
As for the before_save it might be a bit overkill to set it and also a validation case insensitive, but at least it show you the goal:
Email has to be unique, no matter the case, that is the validation. On the other hand you store everything lowercase, this is the data part.
The test is (a bit silently, it's not in the description) asserting that case is ignored when checking uniqueness. Which is a sensible behaviour for email addresses in general.
The lower-case "normalisation" for stored data, plus removing case-sensitivity from the validation, seems like overkill, but may be required due to sequence of events when validating as opposed to saving. Either way, it is self-consistent to have a uniqueness constraint that ignores case, whilst canonicalising the email addresses to lower case for storage.
I want to keep on using the same session and by that I mean Rails' session between various Test::Unit integration tests that use Capybara. The Capybara::Session object is the same in all the tests as it is re-used, but when I access another page in another test, I'm immediately logged out.
Digging in I found that capybara_session.driver.browser.manage.all_cookies is cleared between one test and the next.
Any ideas how? or why? or how to avoid it?
Trying to work-around that, I saved the cookie in a class variable and re-added later by running:
capybara_session.driver.browser.manage.add_cookie(##cookie)
and it seems to work, the cookie is there, but when there's a request, the cookie gets replaced for another one, so it had no effect.
Is there any other way of achieving this?
Add the following after your capybara code that interacts with the page:
Capybara.current_session.instance_variable_set(:#touched, false)
or
page.instance_variable_set(:#touched, false)
If that doesn't work, these might help:
https://github.com/railsware/rack_session_access
http://collectiveidea.com/blog/archives/2012/01/05/capybara-cucumber-and-how-the-cookie-crumbles/
If what you are doing is trying to string together individual examples into a story (cucumber style, but without cucumber), you can use a gem called rspec-steps to accomplish this. For example, normally this won't work:
describe "logging in" do
it "when I visit the sign-in page" do
visit "/login"
end
it "and I fill in my registration info and click submit" do
fill_in :username, :with => 'Foo'
fill_in :password, :with => 'foobar'
click_on "Submit"
end
it "should show a successful login" do
page.should have_content("Successfully logged in")
end
end
Because rspec rolls back all of its instance variables, sessions, cookies, etc.
If you install rspec-steps (note: currently not compatible with rspec newer than 2.9), you can replace 'describe' with 'steps' and Rspec and capybara will preserve state between the examples, allowing you to build a longer story, e.g.:
steps "logging in" do
it "when I visit the sign-in page" #... etc.
it "and I fill in" # ... etc.
it "should show a successful" # ... etc.
end
You can prevent the call to #browser.manage.delete_all_cookies that happens between tests by monkey patching the Capybara::Selenium::Driver#reset! method. It's not a clean way of doing it, but it should work...
Add the following code to your project so that it is executed after you require 'capybara':
class Capybara::Selenium::Driver < Capybara::Driver::Base
def reset!
# Use instance variable directly so we avoid starting the browser just to reset the session
if #browser
begin
##browser.manage.delete_all_cookies <= cookie deletion is commented out!
rescue Selenium::WebDriver::Error::UnhandledError => e
# delete_all_cookies fails when we've previously gone
# to about:blank, so we rescue this error and do nothing
# instead.
end
#browser.navigate.to('about:blank')
end
end
end
For interest's sake, the offending line can be seen in Capybara's codebase here: https://github.com/jnicklas/capybara/blob/master/lib/capybara/selenium/driver.rb#L71
It may be worth posting the reason why you need this kind of behaviour. Usually, having the need to monkey patch Capybara, is an indication that you are attempting to use it for something it was not intended for. It is often possible to restructure the tests, so that you don't need the cookies persisted across integration tests.
I'm using FactoryGirl for my fixtures and am finding that it's not really producing useful validation errors.
I always get the message for activerecord.errors.models.messages.record_invalid.
Not sure what further details are needed to help diagnose this. This makes it an excruciatingly slow process to track each error down.
Example factory:
Factory.define :partner do |partner|
partner.sequence(:username){ |n| "amcconnon#{n}" }
partner.first_name "Bobby Joe"
partner.last_name "Smiley"
partner.sequence(:email){ |n| "bob{n}#partners.com" }
partner.phone_number "5557 5554"
partner.country_id 75
partner.password "password"
partner.password_confirmation "password"
end
Then Factory(:partner) => "ActiveRecord::RecordInvalid Exception: Looks like something went wrong with these changes"
The actual problem is of course the email sequence doesn't use n properly and there is a unique validation on email. But that's for illustrative purposes.
rails => 3.2.2
factory_girl 2.6.1
Any other deets needed to help diagnose this?
(Note: edited this just to add an easier to read factory)
EDIT:
As per bijan's comment: "What exactly am I trying to do."
Trying to run "rspec spec". I would like when I use a factory like Factory(:partner) in this case for the error message when that fails to contain the same error I would get from Partner.new({blah...}).valid? then looked at the validation failures.
I'm not sure if this is exactly the same problem you were having, but it's a problem I was having with validation error messages not being very useful and I thought it could be useful to others searching for this problem. Below is what I came up with. This could be modified to give different info too.
include FactoryGirl::Syntax::Methods
# Right after you include Factory Girl's syntax methods, do this:
def create_with_info(*args, &block)
create_without_info(*args, &block)
rescue => e
raise unless e.is_a? ActiveRecord::RecordInvalid
raise $!, "#{e.message} (Class #{e.record.class.name})", $!.backtrace
end
alias_method_chain :create, :info
Then, when you use create :model_name, it will always include the model name in the error message. We had some rather deep dependencies (yes, another problem), and had a validation error like, "name is invalid"... and name was an attribute on a few different models. With this method added, it saves significant debugging time.
I think the key point here is that when you're setting up a test you need to make sure that the code you're testing fails at the point that you set the expectation (i.e. when you say "should" in Rspec). The specific problem with testing validations is of course that the validation fails as soon as you try to save the ActiveRecord object; thus the test setup mustn't invoke save.
Specific suggesions:
1) IMO Factory definitions should contain the minimum information required to create a valid object and should change as little as possible. When you want to test validations you can override the specific attribute being tested when you instantiate the new test object.
2) When testing validations, use Factory.build. Factory.build creates the ActiveRecord instance with the specified attributes but doesn't try to save it; this means you get to hold off triggering the validation until you set the expectation in the test.
How about something like this?
it "should fail to validate a crap password" do
partner_with_crap_password = Factory.build(:partner, :password => "crap password")
partner_with_crap_password.should_not be_valid
end
Testing in Rails has always been something of a mystery that I avoid if possible but I'm putting a production application together that people will pay for so I really need to test. This problem is driving me mad because the test fails but when I perform the same commands in the console (in test and development mode) it works fine.
user_test.rb
test "should update holidays booked after create"
user = users(:robin)
assert_equal user.holidays_booked_this_year, 4 # this passes
absence = user.absences.create(:from => "2011-12-02", :to => "2011-12-03", :category_id => 1, :employee_notes => "Secret") # this works
assert_equal user.holidays_booked_this_year, 5 # fails
end
absence.rb
after_create :update_holidays_booked
def update_holidays_booked
user = self.user
user.holidays_booked_this_year += self.days_used # the days used attribute is calculated using a before_create callback on the absence
user.save
end
My only thoughts are that it's something to do with updating the User model through a callback on the Absence model but, as I say, this works in the console.
Any advice would be appreciated.
Thanks
Robin
What are you using for your factory?
If you are using a database backed test then you need to reload the user in the test (because the user instance is not updated, the absence's user is updated and saved to the database), reloading the user would look like:
assert_equal user.reload.holidays_booked_this_year, 5
I would also guess that an absence needs to have a user, so you should use build instead of create so the foreign key for user is part of the "created" instance:
user.absences.build
First guess would be that in the console you are operating on a real user in the database whereas the test is a fixture. Have you tried this is in test?:
raise user.inspect
Look at the output and determine which user you are actually working with and what the holidays_booked_this_year attributes is.
(your test block also needs a "do" after the description)