The One Where We DRY Up Mongoid and Rspec Using Shared Examples and Modules
The Situation: You’re adding Addresses to seemingly every document you have. Doctors, Patients, Insurance Agents. Even the Delivery Man – everybody has a contact.
The Problem: You’re tempted to copy/paste the field and validations, but your spidy-senses tell you not to.
Let’s DRY this up using modules and rspec shared examples. We’ll end up with a way you could enable fast rails tests on your Mongoid models.
Some Assumptions Before We Begin
- You have a project setup with Mongoid (2.2+, Rspec 2.8+, and rspec-mongoid)
- You are awesome
OK, so we would normally have a model for the Doctor like so:
class Doctor include Mongoid::Document include Mongoid::Timestamps field :first_name, type: String field :last_name, type: String field :phone_number, type: String field :address_line_1, type: String field :city, type: String field :state, type: String field :postal_code, type: string field :email, type :String validates_presence_of :last_name validates_presence_of :first_name end
Step 1: Write those tests first
require 'spec_helper' describe Doctor do it { should have_fields(:first_name, :last_name) } it { should have_fields(:phone_number, :email, :address_line_1, :city, :state, :postal_code) } it { should validate_presence_of(:first_name) } it { should validate_presence_of(:last_name) } end
Note: If this doesn’t pass, you may need to add configuration.include Mongoid::Matchers in your rspec. See the mongoid-rspec gem for use.
Step The Second: Extract the mongoid definitions
require 'spec_helper' module Contactable def self.included(receiver) receiver.class_eval do field :first_name, type: String field :last_name, type: String field :phone_number, type: String field :address_line_1, type: String field :city, type: String field :state, type: String field :postal_code, type: String field :email, type: String validates_presence_of :first_name validates_presence_of :last_name end end end class Doctor include Mongoid::Document include Mongoid::Timestamps include Contactable end
And run those tests!
....
Finished in 0.11461 seconds
4 examples, 0 failures
Step 3: Extract the Tests!
We’ll extract the tests out into a shared_examples file in the support folder, and then tell the Doctor class that it should behave like a Contact.
#spec/support/shared_examples.rb shared_examples_for Contactable do it { should have_fields(:first_name, :last_name) } it { should have_fields(:phone_number, :email, :address_line_1, :city, :state, :postal_code) } it { should validate_presence_of(:first_name) } it { should validate_presence_of(:last_name) } end #spec/models/doctor_spec.rb require 'spec_helper' describe Doctor do it_behaves_like Contactable end
Run those tests! Could be faster
....
Finished in 0.13347 seconds
4 examples, 0 failures
All is going awesome! So we can now easily add contact information (and validations), to say users:
class User include Mongoid::Document include Mongoid::Timestamps include Contactable end describe User do it_behaves_like Contactable end
Now your Users have first_name, last_name, address, and more!
Let’s Take this to 11
Now we can very, very easily isolate the Doctor out for very fast tests.
module Mongoid module Document; end module Timestamps; end end class Pharmacy; end require_relative "../../app/models/doctor" require_relative "support/shared_examples" describe Doctor do it_behaves_like Contactable it "will tell the pharmacy to release the rx's" do Pharmacy.should_receive(:release_the_rx!) subject.gimme! end end
The above would fail since #gimme! doesn’t exist, but it would be:
- In total isolation from Rails
- Total isolation from Mongo DB
- fast!
I created the following to easily add re-usable data-bags to documents in Mongoid, rather than copying and pasting. The same shared-example technique works well in ActiveRecord.