Skip to content

Instantly share code, notes, and snippets.

@SabretWoW
Last active September 23, 2024 03:01
Show Gist options
  • Save SabretWoW/6234923 to your computer and use it in GitHub Desktop.
Save SabretWoW/6234923 to your computer and use it in GitHub Desktop.
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
@ParinVachhani
Copy link

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

@tcaddy
Copy link

tcaddy commented Oct 23, 2015

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

@myf9000
Copy link

myf9000 commented Jan 17, 2016

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

@rastating
Copy link

Brilliant! Thanks a lot :)

@angelfan
Copy link

👍

@tsaito-cyber
Copy link

great 👍

@asad-ali-bhatti
Copy link

You are an angel 👍

@mculp
Copy link

mculp commented Nov 28, 2016

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

Copy link

ghost commented Jan 12, 2017

This is great, really helped me out 👍

@joshmn
Copy link

joshmn commented Feb 3, 2017

i love you.

@msdundar
Copy link

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
Copy link

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

@dbarria
Copy link

dbarria commented Aug 29, 2017

Awesome! Thank you!

@Yassir4
Copy link

Yassir4 commented Apr 18, 2018

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

@alvesoaj
Copy link

alvesoaj commented Jun 7, 2018

Really nice!!! =)

@rendegosling
Copy link

this is awesome!

@frizbee
Copy link

frizbee commented Jun 18, 2018

Please update!
ensure_length_of are renamed to -> validate_length_of

@johncorderox
Copy link

helped so much for my api. thanks m8

@nrpx
Copy link

nrpx 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?).

@benkoshy
Copy link

benkoshy 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.

@dfurutani
Copy link

Awesome job... on line 89 you need to close parenthesis on :profile
it { expect(user).to have_one(:profile) }

@LaSkilzs
Copy link

LaSkilzs commented Apr 5, 2019

Sweet, just learning and this is a great reference..... Thanks!

@jluczak
Copy link

jluczak commented Apr 10, 2019

Thank you!!! <3

@ivanmatas
Copy link

Great stuff, thanks!

@segunadeleye
Copy link

Some interesting conversations going on here about this gist. You might want to check it out.

https://www.codewithjason.com/examples-pointless-rspec-tests/

@tahsin352
Copy link

Now matchers, which starts with ensure (ensure_inclusion_in, ensure_length_of) are renamed to validate_inclusion_in, validate_length_of (55c8d09).

@RobinDaugherty
Copy link

RobinDaugherty commented Mar 6, 2021

@segunadeleye to respond to that blog post:

Unless I’m crazy, these sorts of tests don’t actually do anything.

You may not be crazy, but you're wrong. Shoulda checks a lot of things beyond whether the association exists. It ensures that the columns that support it exist, that the destination model class exists, and that all of the types match. This type of model test is extremely valuable for testing models. There are also chainable matchers that can be added to these matchers.

But perhaps importantly, we're writing specifications, so these also describe the behavior of the model.

Unfortunately the specs in this example gist are not correctly phrased. Since they're covering multiple objects, each of the it blocks refers to a different subject. In those cases, it should not be used with an implicit example name. The correct phrasing is

describe ModelObject do
  it { is_expected.to belong_to(:other_model_object).optional(true) }
end

or when describing a method on the object:

described ModelObject do
  described '#other_model_object' do
    subject(:other_model_object) { described_class.new.other_model_object }
    it { is_expected.to eq(other_object) }
  end
end

This ensures meaningful spec output, since it flows in English, which is the goal of rspec.

@asyraffff
Copy link

This is cool !!!

@alexventuraio
Copy link

Something similar but without shoulda-matchers?

@evertonlopesc
Copy link

Amazing!

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