Last active
September 23, 2024 03:01
-
-
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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("[email protected]").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 |
This is cool !!!
Something similar but without shoulda-matchers
?
Amazing!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@segunadeleye to respond to that blog post:
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 isor when describing a method on the object:
This ensures meaningful spec output, since it flows in English, which is the goal of rspec.