Instantly share code, notes, and snippets.

Embed
What would you like to do?
Rails Rspec model testing skeleton & cheat sheet using rspec-rails, shoulda-matchers, shoulda-callbacks, and factory_girl_rails. Pretty much a brain dump of examples of what you can (should?) test in a model. Pick & choose what you like, and please let me know if there are any errors or new/changed features out there. Reddit comment thread: http…
# This is a skeleton for testing models including examples of validations, callbacks,
# scopes, instance & class methods, associations, and more.
# Pick and choose what you want, as all models don't NEED to be tested at this depth.
#
# I'm always eager to hear new tips & suggestions as I'm still new to testing,
# so if you have any, please share!
#
# @kyletcarlson
#
# This skeleton also assumes you're using the following gems:
#
# rspec-rails: https://github.com/rspec/rspec-rails
# Shoulda-matchers: https://github.com/thoughtbot/shoulda-matchers
# shoulda-callback-matchers: https://github.com/beatrichartz/shoulda-callback-matchers
# factory_girl_rails: https://github.com/thoughtbot/factory_girl_rails
require 'spec_helper'
describe Model do
it "has a valid factory" do
# Using the shortened version of FactoryGirl syntax.
# Add: "config.include FactoryGirl::Syntax::Methods" (no quotes) to your spec_helper.rb
expect(build(:factory_you_built)).to be_valid
end
# Lazily loaded to ensure it's only used when it's needed
# RSpec tip: Try to avoid @instance_variables if possible. They're slow.
let(:factory_instance) { build(:factory_you_built) }
describe "ActiveModel validations" do
# http://guides.rubyonrails.org/active_record_validations.html
# http://rubydoc.info/github/thoughtbot/shoulda-matchers/master/frames
# http://rubydoc.info/github/thoughtbot/shoulda-matchers/master/Shoulda/Matchers/ActiveModel
# Basic validations
it { expect(bodybuilder).to validate_presence_of(:food).with_message(/you can't get big without your protein!/) }
it { expect(developer).to validate_presence_of(:favorite_coffee) }
it { expect(meal).to validate_numericality_of(:price) }
it { expect(tumblog).to validate_numericality_of(:follower_count).only_integer }
it { expect(odd_number).to validate_numericality_of(:value).odd }
it { expect(even_number).to validate_numericality_of(:value).even }
it { expect(mercedes).to validate_numericality_of(:price).is_greater_than(30000) }
it { expect(junked_car).to validate_numericality_of(:price).is_less_than_or_equal_to(500) }
it { expect(blog_post).to validate_uniqueness_of(:title) }
it { expect(wishlist).to validate_uniqueness_of(:product).scoped_to(:user_id, :wishlist_id).with_message("You can only have an item on your wishlist once.") }
# Format validations
it { expect(user).to allow_value("JSON Vorhees").for(:name) }
it { expect(user).to_not allow_value("Java").for(:favorite_programming_language) }
it { expect(user).to allow_value("dhh@nonopinionated.com").for(:email) }
it { expect(user).to_not allow_value("base@example").for(:email) }
it { expect(user).to_not allow_value("blah").for(:email) }
it { expect(blog).to allow_blank(:connect_to_facebook) }
it { expect(blog).to allow_nil(:connect_to_facebook) }
# Inclusion/acceptance of values
it { expect(tumblog).to ensure_inclusion_of(:status).in_array(['draft', 'public', 'queue']) }
it { expect(tng_group).to ensure_inclusion_of(:age).in_range(18..35) }
it { expect(band).to ensure_length_of(:bio).is_at_least(25).is_at_most(1000) }
it { expect(tweet).to ensure_length_of(:content).is_at_most(140) }
it { expect(applicant).to ensure_length_of(:ssn).is_equal_to(9) }
it { expect(contract).to validate_acceptance_of(:terms) } # For boolean values
it { expect(user).to validate_confirmation_of(:password) } # Ensure two values match
end
describe "ActiveRecord associations" do
# http://guides.rubyonrails.org/association_basics.html
# http://rubydoc.info/github/thoughtbot/shoulda-matchers/master/frames
# http://rubydoc.info/github/thoughtbot/shoulda-matchers/master/Shoulda/Matchers/ActiveRecord
# Performance tip: stub out as many on create methods as you can when you're testing validations
# since the test suite will slow down due to having to run them all for each validation check.
#
# For example, assume a User has three methods that fire after one is created, stub them like this:
#
# before(:each) do
# User.any_instance.stub(:send_welcome_email)
# User.any_instance.stub(:track_new_user_signup)
# User.any_instance.stub(:method_that_takes_ten_seconds_to_complete)
# end
#
# If you performed 5-10 validation checks against a User, that would save a ton of time.
# Associations
it { expect(profile).to belong_to(:user) }
it { expect(wishlist_item).to belong_to(:wishlist).counter_cache }
it { expect(metric).to belong_to(:analytics_dashboard).touch }
it { expect(user).to have_one(:profile }
it { expect(classroom).to have_many(:students) }
it { expect(initech_corporation).to have_many(:employees).with_foreign_key(:worker_drone_id) }
it { expect(article).to have_many(:comments).order(:created_at) }
it { expect(user).to have_many(:wishlist_items).through(:wishlist) }
it { expect(todo_list).to have_many(:todos).dependent(:destroy) }
it { expect(account).to have_many(:billings).dependent(:nullify) }
it { expect(product).to have_and_belong_to_many(:descriptors) }
it { expect(gallery).to accept_nested_attributes_for(:paintings) }
# Read-only matcher
# http://rubydoc.info/github/thoughtbot/shoulda-matchers/master/Shoulda/Matchers/ActiveRecord/HaveReadonlyAttributeMatcher
it { expect(asset).to have_readonly_attribute(:uuid) }
# Databse columns/indexes
# http://rubydoc.info/github/thoughtbot/shoulda-matchers/master/Shoulda/Matchers/ActiveRecord/HaveDbColumnMatcher
it { expect(user).to have_db_column(:political_stance).of_type(:string).with_options(default: 'undecided', null: false)
# http://rubydoc.info/github/thoughtbot/shoulda-matchers/master/Shoulda/Matchers/ActiveRecord:have_db_index
it { expect(user).to have_db_index(:email).unique(:true)
end
context "callbacks" do
# http://guides.rubyonrails.org/active_record_callbacks.html
# https://github.com/beatrichartz/shoulda-callback-matchers/wiki
let(:user) { create(:user) }
it { expect(user).to callback(:send_welcome_email).after(:create) }
it { expect(user).to callback(:track_new_user_signup).after(:create) }
it { expect(user).to callback(:make_email_validation_ready!).before(:validation).on(:create) }
it { expect(user).to callback(:calculate_some_metrics).after(:save) }
it { expect(user).to callback(:update_user_count).before(:destroy) }
it { expect(user).to callback(:send_goodbye_email).before(:destroy) }
end
describe "scopes" do
# It's a good idea to create specs that test a failing result for each scope, but that's up to you
it ".loved returns all votes with a score > 0" do
product = create(:product)
love_vote = create(:vote, score: 1, product_id: product.id)
expect(Vote.loved.first).to eq(love_vote)
end
it "has another scope that works" do
expect(model.scope_name(conditions)).to eq(result_expected)
end
end
describe "public instance methods" do
context "responds to its methods" do
it { expect(factory_instance).to respond_to(:public_method_name) }
it { expect(factory_instance).to respond_to(:public_method_name) }
end
context "executes methods correctly" do
context "#method name" do
it "does what it's supposed to..."
expect(factory_instance.method_to_test).to eq(value_you_expect)
end
it "does what it's supposed to..."
expect(factory_instance.method_to_test).to eq(value_you_expect)
end
end
end
end
describe "public class methods" do
context "responds to its methods" do
it { expect(factory_instance).to respond_to(:public_method_name) }
it { expect(factory_instance).to respond_to(:public_method_name) }
end
context "executes methods correctly" do
context "self.method name" do
it "does what it's supposed to..."
expect(factory_instance.method_to_test).to eq(value_you_expect)
end
end
end
end
end
@sjaveed

This comment has been minimized.

sjaveed commented Nov 8, 2013

This is a pretty sweet template for how to organize your model tests. Thanks!

@kyletcarlson

This comment has been minimized.

Owner

kyletcarlson commented Nov 15, 2013

@sjaveed Thanks! I had to write several hundred tests for a project at work & made this to make my job easier. Remember: a lazy developer is a good developer!

@andrewmartin

This comment has been minimized.

andrewmartin commented Feb 25, 2014

This is awesome. I'm really green to testing and the expectations always through me off. Killer to see these in one place! Out of curiosity, do you have any open source projects you have up that use a good amount of these tests? Would love to kind of play with a real app. Either way, thank you for sharing this!

@kyletcarlson

This comment has been minimized.

Owner

kyletcarlson commented Mar 22, 2014

Thanks @andrewmartin! I wrote a ton of tests for my company and noticed i was doing the same thing over and over. This made it sooo much easier.

@egeersoz

This comment has been minimized.

egeersoz commented Jun 29, 2014

This is amazing. Do you have a similar template for testing controllers?

@kyletcarlson

This comment has been minimized.

Owner

kyletcarlson commented Sep 23, 2014

@egeersoz - Nope, sorry. Never test my controllers :)

@fantgeass

This comment has been minimized.

fantgeass commented Nov 5, 2014

For validating uniqueness we need data in database which we will test uniqueness against.
Example:

context 'when customer already has active order' do
  let(:active_order) { FactoryGirl.create(:order, active: true) }

  it 'disallows several active orders for same customer' do
    new_active_order = FactoryGirl.build(:order, active: true, customer: active_order.customer)

    expect(new_active_order).to validate_uniqueness_of(:customer).scoped_to(:active)
                   .with_message("one customer can't have two active orders")
  end
end
@udit7590

This comment has been minimized.

udit7590 commented Jan 8, 2015

Can you share same kind of cheatsheet for controllers... :)

@nicogaldamez

This comment has been minimized.

nicogaldamez commented Jan 14, 2015

This is great!
+1 for a cheatsheet for controllers, requests and views

@eliotsykes

This comment has been minimized.

eliotsykes commented Feb 2, 2015

As requested by @udit7590 and @nicogadamez rspec_cheatsheet_controller_spec.rb

@enriquesalceda

This comment has been minimized.

enriquesalceda commented Jul 9, 2015

Beautiful!

@dsklopp

This comment has been minimized.

dsklopp commented Aug 21, 2015

+1

I'm struggling through Rails testing with an ocean of articles. Of all I've seen, this gist is the most useful. Thanks!

@gurgelrenan

This comment has been minimized.

gurgelrenan commented Aug 23, 2015

Same felling @dsklopp

@avifoxi

This comment has been minimized.

avifoxi commented Oct 2, 2015

AWESOME

@ParinVachhani

This comment has been minimized.

ParinVachhani commented Oct 11, 2015

Thanks for sharing this! It's a very helpful template for newbies like me!

@tcaddy

This comment has been minimized.

tcaddy commented Oct 23, 2015

FYI: a few lines are missing do at the end. Lines: 145, 149, 164

@myf9000

This comment has been minimized.

myf9000 commented Jan 17, 2016

Thanks a lot! This approach for test RSpec is for me the best! Probably I'm leazy :)

@rastating

This comment has been minimized.

rastating commented Mar 9, 2016

Brilliant! Thanks a lot :)

@angelfan

This comment has been minimized.

angelfan commented Apr 21, 2016

👍

@takuma-saito

This comment has been minimized.

takuma-saito commented May 26, 2016

great 👍

@asad-ali-bhatti

This comment has been minimized.

asad-ali-bhatti commented Aug 16, 2016

You are an angel 👍

@mculp

This comment has been minimized.

mculp commented Nov 28, 2016

how do I send you money tips for this? this is really, really good.

@Arunk1390

This comment has been minimized.

Arunk1390 commented Jan 12, 2017

This is great, really helped me out 👍

@joshmn

This comment has been minimized.

joshmn commented Feb 3, 2017

i love you.

@msdundar

This comment has been minimized.

msdundar commented Feb 19, 2017

Now matchers, which starts with ensure (ensure_inclusion_of, ensure_length_of) are renamed to validate_inclusion_of, validate_length_of.

thoughtbot/shoulda-matchers@55c8d09

@joecairns

This comment has been minimized.

joecairns commented May 31, 2017

This is really good stuff, thanks for posting it up.

@dbarria

This comment has been minimized.

dbarria commented Aug 29, 2017

Awesome! Thank you!

@Yassir4

This comment has been minimized.

Yassir4 commented Apr 18, 2018

Awesome! Thanks for sharing
ensure_length_of are renamed to -> validate_length_of

@zerokol

This comment has been minimized.

zerokol commented Jun 7, 2018

Really nice!!! =)

@renlesterdg

This comment has been minimized.

renlesterdg commented Jun 12, 2018

this is awesome!

@frizbee

This comment has been minimized.

frizbee commented Jun 18, 2018

Please update!
ensure_length_of are renamed to -> validate_length_of

@johncorderox

This comment has been minimized.

johncorderox commented Jul 23, 2018

helped so much for my api. thanks m8

@windsn0w

This comment has been minimized.

windsn0w commented Aug 30, 2018

Hi! This is awesome! +1 for good job))
A little add: in block with public class methods we must test methods of class, not instance (factory_instance is an instance object, right?).

@BKSpurgeon

This comment has been minimized.

BKSpurgeon commented Oct 8, 2018

Please let me know if there are any errors or new/changed features out there

FactoryGirl gem is now called FactoryBot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment