Rails 3.1, RSpec: testing model validations - ruby-on-rails

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

Related

rspec model spec let vs factory

I'm starting out with testing and it's not exactly clear when I should use let.
Should I use let in the following model testing for lazy loading or since the data is bit different in every test I can keep it as it is? As I've seen in some examples it's way more important for controller testing as the :task would be the same for each action test.
model spec
require 'rails_helper'
RSpec.describe Task, type: :model do
describe "model validations" do
it "has a valid factory" do
expect(build(:task)).to be_valid
end
it "is invalid without executor" do
expect(build(:task, executor_id: nil)).not_to be_valid
end
it "is invalid without assigner" do
expect(build(:task, assigner_id: nil)).not_to be_valid
end
it "is invalid without content" do
expect(build(:task, content: nil)).not_to be_valid
end
it "is invalid without deadline" do
expect(build(:task, deadline: nil)).not_to be_valid
end
it "is invalid with deadline in the past" do
expect(build(:task, deadline: Faker::Time.between(DateTime.now - 1, DateTime.now - 2))).not_to be_valid
end
end
end
factories
FactoryGirl.define do
factory :task do
content { Faker::Lorem.sentence }
deadline { Faker::Time.between(DateTime.now + 2, DateTime.now + 3) }
association :executor, factory: :user
association :assigner, factory: :user
end
end
The benefits of let come from tests that aren't of the form you use above. Imagine this group:
context "completing tasks" do
let(:completing_a_task){ task.complete }
context "that aren't due yet" do
let(:task){ create(:not_due_task) }
it "should not send an email" do
expect( TaskMailer ).not_to receive(:deliver_message)
expect{ completing_a_task }.not_to raise_error
end
end
context "overdue" do
let(:task){ create(:overdue_task) }
it "should send an email" do
expect( TaskMailer ).to receive(:deliver_message)
expect{ completing_a_task }.not_to raise_error
end
end
end
By allowing for late binding, you can make minimal changes but provide maximal coverage. The more collaborators you need in order to set up the appropriate behavior needed for your tests, the more likely you are to benefit from let. While it's try that you don't particularly need to drive for DRY in your test suite, huge setup blocks for your tests are a smell, and the let technique is a great tool to help fight for clarity and simplicity even when your domain presents complexity. My own example includes no collaborators still, but hopefully the concept is still clear enough.
I'd suggest keeping it how you have it and not using a let. Don't worry about DRYing up your tests. They don't interact with each other, and so you don't run into the issues you would with code duplication in application logic.
For what it's worth, you can use the shoulda-matchers gem to accomplish what you've got there: https://github.com/thoughtbot/shoulda-matchers
describe Task do
describe "validations" do
it { is_expected.to validate_presence_of(:content) }
it { is_expected.to validate_presence_of(:deadline) }
end
end

Shoulda Matcher with custom validation causing all shoulda validations to fail

I am running into a problem where a custom validation on my model is causing all of the shoulda validations to fail.
Essentially:
class User < ActiveRecord::Base
validates_presence_of :name
validate :some_date_validation
private
def some_date_validation
if date_given > birthday
errors.add(:birthday, "Some sort of error message")
end
end
end
And then in the spec:
require 'rails_helper'
RSpec.describe User, type: :model do
describe "shoulda validations" do
it { should validate_presence_of(:name) }
end
end
This will cause my test to fail because the other validation won't pass. Why is this?
You need to test using an instance of an object which is valid by default.
When you use the implicit subject in your Rspec test, Rspec will create a new instance of the object under test for you using the default initializer. In this case, User.new. This instance will be invalid because neither name is present nor is the custom validation going to pass.
If you are using factories (e.g. factory_girl) then you should create a User factory which sets all the attributes which make the validations pass.
FactoryGirl.define do
factory :user do
name "John Doe"
date_given Time.now
birthday 25.years.ago
end
end
Then use it in your tests
require 'rails_helper'
RSpec.describe User, type: :model do
describe "shoulda validations" do
subject { build(:user) }
it { should validate_presence_of(:name) }
end
end
You've now explicitly set the subject of your tests to be a new instance of User created by your factory. The attributes will be pre-set which means your instance is valid by default, and the tests should now be able to test each individual validation properly.

Rspec and FactoryGirl.attributes_for params refactor

I am current getting my tests to pass, but I wanted to see if I could refactor the tests with a let(:message) or some kind of variable
Mailer tests
describe "Contact Form" do
context "when a valid message" do
it "sends an email" do
post contact_create_path, message: FactoryGirl.attributes_for(:message)
expect(ActionMailer::Base.deliveries.last.to).to eq(["#{ENV["MVP_USERNAME"]}"])
end
end
The part I want to refactor is message: FactoryGirl.attributes_for(:message).
I tried to doing something like
context "when a valid message" do
let(:message) { FactoryGirl.attributes_for(:message) }
it "sends an email" do
post contact_create_path, message
expect(ActionMailer::Base.deliveries.last.to).to eq(["#{ENV["MVP_USERNAME"]}"])
end
but that outputs
ActionController::ParameterMissing:
param not found: message
other attempts
#message = FactoryGirl.attributes_for(:message)
message = FactoryGirl.attributes_for(:message)
I could leave it like this, but I feel that I it should be changed for some reason. Suggestions?
It doesn't look to be in need of refactoring to me, but the trouble is the version you have in your let statement is simply a hash of attributes. It's missing the :message key.
let(:message) { message: FactoryGirl.attributes_for(:message) }
That should work, but as I said I don't think you need to refactor it. I'd actually just make sure you're mixing in the FactoryGirl syntax methods into your specs so you can drop the FactoryGirl and just use attributes_for directly.
In your spec helper:
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
end

How to test Mass Assignment in Rails 4 using RSpec

Given a simple User model, in Rails 4 with name, email, and an admin boolean, what's the best approach to testing mass assignment using RSpec?
Here's the UsersController:
def create
#user = User.new user_params
...snip
end
private
def user_params
params.require(:user).permit(:name, :email)
end
and two different tries at testing this:
in user_spec.rb
describe "accessible attributes" do
describe "should not allow access to admin" do
before do
#user.admin = "1"
#user.save
end
it { should_not be_admin }
end
end
or in users_controller_spec.rb
it 'should only allow name and email to be set' do
#controller.user_params.keys.should eq(['name', 'email')
end
Neither work - the former just creates a user with admin set to true (failing the test) - presumably this bypasses strong_parameters. The latter works, but only if the user_params method is not private. (The official docs recommend setting it to private. Note - watching for a MassAssignment error in the user_spec doesn't work either (no error is raised).
Note - actually setting the user to admin in a view correctly works - the admin attribute is filtered out and all is happy, but would really like to see this working properly in a test.
An alternative suggest is to use the shoulda-matchers gem with the user_spec.rb:
describe User do
...
it { should_not allow_mass_assignment_of(:admin) }
...
end
(this doesn't work either), giving:
Failure/Error: it { should_not allow_mass_assignment_of(:admin) }
NoMethodError:
undefined method `active_authorizer' for #<Class:0x007f93c9840648>
(I assume this error is due to the fact shoulda-matchers isn't Rails 4 compatible yet).
Thanks in advance!
it "should not allow mass assignment" do
raw_parameters = { :admin => 1 }
parameters = ActionController::Parameters.new(raw_parameters)
expect {#user.update_attributes(parameters)}.should raise_error
end
In order to test mass assignment you should simulate passing parameters from controller.
https://github.com/rails/strong_parameters#use-outside-of-controllers

Devise/Rspec - Tested a user creation with (maybe) missing attributes (got true..)

I am testing Devise with Rspec using Micheal Hartl source code (railstutorial)
Whereas the confirmable module is enabled, I don't understand why this test pass:
spec/models/user_spec.rb
require 'spec_helper'
describe User do
before(:each) do
#attr = { :username => "ExampleUser",
:email => "user#example.com",
:password => 'test1234',
}
end
it "should create a new instance given valid attributes" do
User.create!(#attr)
end
end
Basically, I want to be sure of this code does, it tests the creation on the user, not this validation (cause the user has not confirmed yet and the test returns true) ? This is right?
Moreover, I didn't provide attribute for password confirmation, and the user is still created!
Is this mean that in the :validatable module there is not (?):
validates :password, :confirmation => true
Thanks to get you view on this!
one problem is the trailing comma at the end of your each block. second, you are not asserting anything in your test to pass or fail the test, though you are probably erroring out at this point, which is why you are saying it didnt pass.
you can try assigning the user object to a variable:
it "should create a new instance given valid attributes" do
#user = User.new(#attr)
#user.should be_valid #=> new will let you know if its valid or not
#user.save.should be_true #=> another possible assertion to pass/fail the test
# debug message to give you back why it failed
puts #user.errors.full_messages.to_sentence
end

Resources