In my Rails 3 application, I have a RSpec spec that checks behavior of a given field (role in the User model) to guarantee that the value is within a list of valid values.
Now I am going to have the exact same spec for another field, in another model with another set of valid values. I would like to extract the common code instead of merely copying and pasting it, changing the variables.
I am wondering if this would be the case to use a shared example or other RSpec reuse technique.
Here's the relevant RSpec code:
describe "validation" do
describe "#role" do
context "with a valid role value" do
it "is valid" do
User::ROLES.each do |role|
build(:user, :role => role).should be_valid
end
end
end
context "with an empty role" do
subject { build(:user, :role => nil) }
it "is invalid" do
subject.should_not be_valid
end
it "adds an error message for the role" do
subject.save.should be_false
subject.errors.messages[:role].first.should == "can't be blank"
end
end
context "with an invalid role value" do
subject { build(:user, :role => 'unknown') }
it "is invalid" do
subject.should_not be_valid
end
it "adds an error message for the role" do
subject.save.should be_false
subject.errors.messages[:role].first.should =~ /unknown isn't a valid role/
end
end
end
end
What would be the best case to reuse this code, but extracting role (the field being verified) and User::ROLES (the collection of valid values) into parameters being passed to this code?
I think this is a perfectly reasonable use case for shared examples. e.g. something like this:
shared_examples_for "attribute in collection" do |attr_name, valid_values|
context "with a valid role value" do
it "is valid" do
valid_values.each do |role|
build(:user, attr_name => role).should be_valid
end
end
end
context "with an empty #{attr_name}" do
subject { build(:user, attr_name => nil) }
it "is invalid" do
subject.should_not be_valid
end
it "adds an error message for the #{attr_name}" do
subject.save.should be_false
subject.errors.messages[attr_name].first.should == "can't be blank"
end
end
context "with an invalid #{attr_name} value" do
subject { build(:user, attr_name => 'unknown') }
it "is invalid" do
subject.should_not be_valid
end
it "adds an error message for the #{attr_name}" do
subject.save.should be_false
subject.errors.messages[attr_name].first.should =~ /unknown isn't a valid #{attr_name}/
end
end
end
Then you can call it in your specs like this:
describe "validation" do
describe "#role" do
behaves_like "attribute in collection", :role, User::ROLES
end
end
Haven't tested this but I think it should work.
You can DRY your spec with shared_examples technic this way:
shared_examples "no role" do
it "is invalid" do
subject.should_not be_valid
end
end
context "with an empty role" do
subject { Factory.build(:user, :name => nil) }
it_behaves_like "no role"
end
context "with an invalid role value" do
subject { Factory.build(:user, :name => '') }
it_behaves_like "no role"
end
But what about your idea to DRY few specs..I think it's too much. I'm convince that spec has to be readable firstly and only then DRY'ing. If you DRY few specs, it will be probably a headache for future reading/refactoring/changing this code.
Related
Here is my Spec file:
require 'spec_helper'
describe User, "references" do
it { should have_and_belong_to_many(:roles) }
it { should belong_to(:account_type) }
it { should belong_to(:primary_sport).class_name("Sport") }
it { should belong_to(:school) }
it { should belong_to(:city) }
end
describe User, "factory" do
before(:each) do
#user = FactoryGirl.create(:user)
end
it "is invalid with no email" do
#user.email = nil
#user.should_not be_valid
end
it "is valid with email" do
#user.should be_valid
end
end
Factory:
FactoryGirl.define do
factory :user do
email Faker::Internet.email
password "password"
password_confirmation "password"
agreed_to_age_requirements true
end
end
The part I am trying to "test" for and not sure how to 100% is checking to make sure when a User is created that the email address is not nil.
shoulda provides validation helpers to help you test the validations.
it { should validate_presence_of(:email) }
If you want to use rspec and write your own, then
describe User do
it "should be invalid without email" do
user = FactoryGirl.build(:user, :email => nil)
#user.should_not be_valid
#user.errors.on(:email).should == 'can't be blank' #not sure about the exact message. But you will know when you run the test
end
it "should be valid with email" do
user = FactoryGirl.build(:user, :email => "user#user.com")
#user.should be_valid
end
end
When you run the test, it would read as
User
should be invalid without email
should be valid with email
Giving a good description for your test case is very important, because it kind of acts like a documentation.
I am trying to verify that the validators are working correctly on my model, and for that I am using Rspec and Capybara. Here is my code.
describe "#when registering" do
before { visit new_record_path }
describe "#with invalid information" do
describe "#should not modify database" do
subject { -> { click_button submit } }
it { should_not change(Pet, :count) }
it { should_not change(Owner, :count) }
end
end
end
end
When I run the specs, i get an error: "undefined method 'model_name' for NilClass:Class"
What could be causing rspec to think my model is nil?
Thanks!
You should not test your validations with a feature/acceptance test, it should be with a model test. Then for each form you could test an error is raised if something is invalid instead of testing every error through acceptance tests. For each model it should be something like so:
describe Pet do
describe "validations" do
# These can echo any model validation
it "is invalid if attribute is not present" do
Pet.new(:attribute => "Invalid Item").should_not be_valid
end
end
end
or with Factory Girl:
describe Pet do
describe "validations" do
it "is invalid if attribute is not present" do
build(:pet, :attribute => "Invalid Item").should_not be_valid
end
end
end
Then in an acceptance test you can have something like:
it "displays an error if validation fails" do
visit new_pet_path
#Something to make the form submission fail, not everything
fill_in("Attribute", :with => "")
click_button("Create Pet")
page.should have_content("can't be blank")
current_path.should == pets_path
end
This will help to keep your acceptance tests light and test the validations in the model where it belongs. Hope this helps!
I have the following validator:
# Source: http://guides.rubyonrails.org/active_record_validations_callbacks.html#custom-validators
# app/validators/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
def validate_each(object, attribute, value)
unless value =~ /^([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
object.errors[attribute] << (options[:message] || "is not formatted properly")
end
end
end
I would like to be able to test this in RSpec inside of my lib directory. The problem so far is I am not sure how to initialize an EachValidator.
I am not a huge fan of the other approach because it ties the test too close to the implementation. Also, it's fairly hard to follow. This is the approach I ultimately use. Please keep in mind that this is a gross oversimplification of what my validator actually did... just wanted to demonstrate it more simply. There are definitely optimizations to be made
class OmniauthValidator < ActiveModel::Validator
def validate(record)
if !record.omniauth_provider.nil? && !%w(facebook github).include?(record.omniauth_provider)
record.errors[:omniauth_provider] << 'Invalid omniauth provider'
end
end
end
Associated Spec:
require 'spec_helper'
class Validatable
include ActiveModel::Validations
validates_with OmniauthValidator
attr_accessor :omniauth_provider
end
describe OmniauthValidator do
subject { Validatable.new }
context 'without provider' do
it 'is valid' do
expect(subject).to be_valid
end
end
context 'with valid provider' do
it 'is valid' do
subject.stubs(omniauth_provider: 'facebook')
expect(subject).to be_valid
end
end
context 'with unused provider' do
it 'is invalid' do
subject.stubs(omniauth_provider: 'twitter')
expect(subject).not_to be_valid
expect(subject).to have(1).error_on(:omniauth_provider)
end
end
end
Basically my approach is to create a fake object "Validatable" so that we can actually test the results on it rather than have expectations for each part of the implementation
Here's a quick spec I knocked up for that file and it works well. I think the stubbing could probably be cleaned up, but hopefully this will be enough to get you started.
require 'spec_helper'
describe 'EmailValidator' do
before(:each) do
#validator = EmailValidator.new({:attributes => {}})
#mock = mock('model')
#mock.stub('errors').and_return([])
#mock.errors.stub('[]').and_return({})
#mock.errors[].stub('<<')
end
it 'should validate valid address' do
#mock.should_not_receive('errors')
#validator.validate_each(#mock, 'email', 'test#test.com')
end
it 'should validate invalid address' do
#mock.errors[].should_receive('<<')
#validator.validate_each(#mock, 'email', 'notvalid')
end
end
I would recommend creating an anonymous class for testing purposes such as:
require 'spec_helper'
require 'active_model'
require 'email_validator'
RSpec.describe EmailValidator do
subject do
Class.new do
include ActiveModel::Validations
attr_accessor :email
validates :email, email: true
end.new
end
describe 'empty email addresses' do
['', nil].each do |email_address|
describe "when email address is #{email_address}" do
it "does not add an error" do
subject.email = email_address
subject.validate
expect(subject.errors[:email]).not_to include 'is not a valid email address'
end
end
end
end
describe 'invalid email addresses' do
['nope', '#', 'foo#bar.com.', '.', ' '].each do |email_address|
describe "when email address is #{email_address}" do
it "adds an error" do
subject.email = email_address
subject.validate
expect(subject.errors[:email]).to include 'is not a valid email address'
end
end
end
end
describe 'valid email addresses' do
['foo#bar.com', 'foo#bar.bar.co'].each do |email_address|
describe "when email address is #{email_address}" do
it "does not add an error" do
subject.email = email_address
subject.validate
expect(subject.errors[:email]).not_to include 'is not a valid email address'
end
end
end
end
end
This will prevent hardcoded classes such as Validatable, which could be referenced in multiple specs, resulting in unexpected and hard to debug behavior due to interactions between unrelated validations, which you are trying to test in isolation.
Inspired by #Gazler's answer I came up with the following; mocking the model, but using ActiveModel::Errors as errors object. This slims down the mocking quite a lot.
require 'spec_helper'
RSpec.describe EmailValidator, type: :validator do
subject { EmailValidator.new(attributes: { any: true }) }
describe '#validate_each' do
let(:errors) { ActiveModel::Errors.new(OpenStruct.new) }
let(:record) {
instance_double(ActiveModel::Validations, errors: errors)
}
context 'valid email' do
it 'does not increase error count' do
expect {
subject.validate_each(record, :email, 'test#example.com')
}.to_not change(errors, :count)
end
end
context 'invalid email' do
it 'increases the error count' do
expect {
subject.validate_each(record, :email, 'fakeemail')
}.to change(errors, :count)
end
it 'has the correct error message' do
expect {
subject.validate_each(record, :email, 'fakeemail')
}.to change { errors.first }.to [:email, 'is not an email']
end
end
end
end
One more example, with extending an object instead of creating new class in the spec. BitcoinAddressValidator is a custom validator here.
require 'rails_helper'
module BitcoinAddressTest
def self.extended(parent)
class << parent
include ActiveModel::Validations
attr_accessor :address
validates :address, bitcoin_address: true
end
end
end
describe BitcoinAddressValidator do
subject(:model) { Object.new.extend(BitcoinAddressTest) }
it 'has invalid bitcoin address' do
model.address = 'invalid-bitcoin-address'
expect(model.valid?).to be_falsey
expect(model.errors[:address].size).to eq(1)
end
# ...
end
Using Neals great example as a basis I came up with the following (for Rails and RSpec 3).
# /spec/lib/slug_validator_spec.rb
require 'rails_helper'
class Validatable
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :slug
validates :slug, slug: true
end
RSpec.describe SlugValidator do
subject { Validatable.new(slug: slug) }
context 'when the slug is valid' do
let(:slug) { 'valid' }
it { is_expected.to be_valid }
end
context 'when the slug is less than the minimum allowable length' do
let(:slug) { 'v' }
it { is_expected.to_not be_valid }
end
context 'when the slug is greater than the maximum allowable length' do
let(:slug) { 'v' * 64 }
it { is_expected.to_not be_valid }
end
context 'when the slug contains invalid characters' do
let(:slug) { '*' }
it { is_expected.to_not be_valid }
end
context 'when the slug is a reserved word' do
let(:slug) { 'blog' }
it { is_expected.to_not be_valid }
end
end
If it's possible to not use stubs I would prefer this way:
require "rails_helper"
describe EmailValidator do
let(:user) { build(:user, email: email) } # let's use any real model
let(:validator) { described_class.new(attributes: [:email]) } # validate email field
subject { validator.validate(user) }
context "valid email" do
let(:email) { "person#mail.com" }
it "should be valid" do
# with this expectation we isolate specific validator we test
# and avoid leaking of other validator errors rather than with `user.valid?`
expect { subject }.to_not change { user.errors.count }
expect(user.errors[:email]).to be_blank
end
end
context "ivalid email" do
let(:email) { "invalid.com" }
it "should be invalid" do
expect { subject }.to change { user.errors.count }
# Here we can check message
expect(user.errors[:email]).to be_present
expect(user.errors[:email].join(" ")).to include("Email is invalid")
end
end
end
I am using Ruby on Rails 3.0.9 and RSpect 2. I am trying to refactoring some spec file in the following way (in order to test with less code similar User class object attribute values):
describe User do
let(:user1) { Factory(:user, :users_attribute_a => 'invalid_value') }
let(:user2) { Factory(:user, :users_attribute_b => 'invalid_value') }
let(:user3) { Factory(:user, :users_attribute_c => 'invalid_value') }
it "foreach user" do
[ user1, user2, user3 ].each do |user|
subject { user }
it "should be whatever"
user.should_not be_valid
...
end
end
end
end
However, if I run the above test I get the following error:
Failure/Error: it "should be whatever" do
NoMethodError:
undefined method `it' for #<RSpec::Core::ExampleGroup::Nested_1::Nested_2::Nested_2:0x00000106ccee60>
What is the problem? How can I solve that?
UPDATE after the #Emily answer.
If in the above code I use context "foreach user" do ... instead of it "foreach user" do ... I get the following error:
undefined local variable or method `user1' for #<Class:0x00000105310758> (NameError)
The problem is having one spec nested within another. You need to replace it "foreach user" with context "foreach user".
Edited to add: After some investigation, it looks like helpers set with let are only available inside of the it "should ..." block, and not in the surrounding context. I'd recommend is trying to find a different structural solution. What the best solution is will depend on what you're actually trying to test. I'm guessing what you're trying to do is make sure the user is invalid when you remove any of the required attributes. In that case, what I've done is something like this:
describe User do
let(:user_attributes){ Factory.attributes_for(:user) }
# Testing missing values aren't valid
[:name, :email, :phone].each do |required_attribute|
it "should not be valid without #{required_attribute}" do
User.new(user_attributes.except(required_attribute)).should_not be_valid
end
end
# Testing invalid values aren't valid
[[:email, 'not_an_email'], [:phone, 'not a phone']].each do |(attribute, value)|
it "should not be valid with bad value for #{attribute}" do
User.new(user_attributes.update(attribute => value)).should_not be_valid
end
end
end
If you're doing something that requires more complex differences in the instance you're creating, there may not be a clean way to do it with iteration. I don't think DRY is quite as essential in testing as it is in other parts of your code. There's nothing wrong with having three different contexts for the three user types, and a validity test in each context.
describe User do
context "with user1" do
subject{ Factory(:user, :users_attribute_a => 'invalid_value') }
it{ should_not be_valid }
end
context "with user2" do
subject{ Factory(:user, :users_attribute_b => 'invalid_value') }
it{ should_not be_valid }
end
context "with user3" do
subject{ Factory(:user, :users_attribute_c => 'invalid_value') }
it{ should_not be_valid }
end
end
You're mixing and matching all sorts of rspec stuff. Here's your stuff, fixed:
describe User do
let(:user1) { Factory(:user, :users_attribute_a => 'invalid_value') }
let(:user2) { Factory(:user, :users_attribute_b => 'invalid_value') }
let(:user3) { Factory(:user, :users_attribute_c => 'invalid_value') }
it "should not be valid" do
[ user1, user2, user3 ].each do |user|
user.should_not be_valid
end
end
end
I would do it this way:
describe User do
subject{Factory.build(:user)}
it "should not be valid with invalid users_attribute_a" do
subject.users_attribute_a = "invalid_value"
subject.should_not be_valid
end
it "should not be valid with invalid users_attribute_b" do
subject.users_attribute_b = "invalid_value"
subject.should_not be_valid
end
end
If you want to have "context", then cool, but you can't have variables before your context inside of your context.
If you want to have a specification, then have one, but you can't net "it" statements
UPDATE WITH LEAST POSSIBLE CODE
describe User do
it "should not be valid with other attributes" do
{:users_attribute_a => 'invalid_value', :users_attribute_b => 'invalid_value', :users_attribute_c => 'invalid_value'}.each do |key, value|
Factory.build(:user, key => value).should_not be_valid
end
end
end
The problem is that the helpers that are set with "let" do not exist outside of a example context.
What you're trying to do could be achieved as:
it "does something with all users" do
[user1, user2, user3] do |user|
user.valid?.should be_true
end
end
Both contexts are different
Another way it might work (haven't tried it) it's like this:
context "for all users" do
[:user1, :user2, :user3].each do |user|
it "does something" do
send(user).valid?.should be_true
end
end
end
This should work. Note how the context is written, it will make the output of tests clearer. From writing it this way it implies (to me) that you should make a test for each attribute separately, but it's your choice:
describe User do
let!(:users) {
[:users_attribute_a, :users_attribute_b, :users_attribute_c].map do |a|
Factory(:user, => 'invalid_value')
end
}
context "Given a user" do
context "With an invalid value" do
subject { users }
it { subject.all?{|user| should_not be_valid }
end
end
end
i'm into rspec these days, trying to make my models more precise and accurate. Some things are still a bit weird to me about rspec and so i thought it would be nice if someone could clarify.
Let's say that i have a User model. This one has a :name. The name should be between 4..15 characters(that's a secondary objective, at first it must just exist). So now i'm thinking: What is the best way to test that in a manner that assures that this will happen. To test that a user must have a name, i wrote something like this :
describe User do
let(:user) { User.new(:name => 'lele') }
it "is not valid without a name" do
user.name.should == 'lele'
end
end
Now, i'm not quite sure that this accomplishes exactly what i want. It seems to me that i'm actually testing Rails with this one. Moreover, if i want to check that a name cannot be more than 15 chars and less than 4, how can this be integrated ?
EDIT:
Maybe this is better ?
describe User do
let(:user) { User.new(:name => 'lele') }
it "is not valid without a name" do
user.name.should_not be_empty
end
end
You're probably looking for the be_valid matcher:
describe User do
let(:user) { User.new(:name => 'lele') }
it "is valid with a name" do
user.should be_valid
end
it "is not valid without a name" do
user.name = nil
user.should_not be_valid
end
end
I use this way:
describe User do
it "should have name" do
lambda{User.create! :name => nil}.should raise_error
end
it "is not valid when the name is longer than 15 characters" do
lambda{User.create! :name => "im a very looooooooong name"}.should raise_error
end
it "is not valid when the name is shorter than 4 characters" do
lambda{User.create! :name => "Tom"}.should raise_error
end
end
I like to test the actual error messages for validations:
require 'spec_helper'
describe User do
let (:user) { User.new }
it "is invalid without a name" do
user.valid?
user.errors[:name].should include("can't be blank")
end
it "is invalid when less than 4 characters" do
user.name = "Foo"
user.valid?
user.errors[:name].should include("is too short (minimum is 4 characters)")
end
it "is invalid when greater than 15 characters" do
user.name = "A very, very, very long name"
user.valid?
user.errors[:name].should include("is too long (maximum is 15 characters)")
end
end
It's also helpful to use a factory that builds an object with valid attributes, which you can invalidate one at a time for testing.
I would use something similar to this
class User < ActiveRecord::Base
validates_presence_of :name
validates_length_of :name, :in => 4..15
end
describe User do
it "validates presence of name" do
user = User.new
user.valid?.should be_false
user.name = "valid name"
user.valid?.should be_true
end
it "validates length of name in 4..15" do
user = User.new
user.name = "123"
user.valid?.should be_false
user.name = "1234567890123456"
user.valid?.should be_false
user.name = "valid name"
user.valid?.should be_true
end
end
Most notable is that I'm using active record validations for both conditions. In my examples I don't rely on the error strings. In examples that test the behavior of validations there's no reason to touch the database so I don't. In each example I test the behavior of the object when it's both valid and invalid.