No More Seat Costs: Semaphore Plans Just Got Better!

    12 Aug 2015 · Software Engineering

    Writing One-Liner RSpec Tests in Rails with Shoulda-Matchers

    4 min read
    Contents

    Introduction

    Shoulda Matchers provides Test::Unit and RSpec-compatible one-liners that test common Rails functionality. It helps you write tests that would otherwise be much longer, more complex, and error-prone.

    Using shoulda-matchers for testing simplifies the entire process.

    Let’s take a look at shoulda-matchers in action, compared to its RSpec-only equivalent.

    One-Liner ActiveModel Tests

    Below is an RSpec unit test for an ActiveModel. Each named test goes through four distinct phases: setup, exercise, verify, and teardown. The first named test ensures that you can’t create a valid Contact record without a first_name attribute; the second test makes sure each Contact record has a unique email address, and the third enforces a minimum password length requirement.

    describe Contact do
      it "is invalid without a firstname" do
        contact = Contact.create(first_name: nil, email: 'ted@example.com', password: 'empirestatebuilding')
    
        contact.valid?
    
        expect(contact.errors[:firstname]).to include("can't be blank")
      end
    
      it "is invalid with a duplicate email address" do
        Contact.create(first_name: 'Ted', email: 'ted@example.com', password: 'empirestatebuilding')
        contact = Contact.new(first_name: 'Robin', email: 'ted@example.com', password: 'montrealcanadiens')
    
        contact.valid?
    
        expect(contact.errors[:emal]).to include("has already been taken")
      end
    
      it "is invalid without a minimum password length" do
        contact_one = Contact.create(first_name: 'Ted', email: 'ted@example.com', password: 'empire')
    
        contact_one.valid?
    
        expect(Contact.errors[:password]).to include("password is too short")
      end
    end

    At every phase of the RSpec-only test cases, you need to recall or reference too much information to correctly implement each one. You need to remember that Rails stores error messages as a Hash, and you also need to remember the exact text for each error message.

    That alone can discourage you from testing your Rails app because it slows you down.

    In this example, shoulda-matchers reduces about 20 lines of test code to just three, but gives you the same assurance that you can change your code at any time.

    describe Contact do
      it { is_expected.to validate_presence_of(:firstname) }
      it { is_expected.to validate_uniqueness_of(:email) }
      it { is_expected.to validate_length_of(:password).is_at_least(10) }
    end

    One-Liner ActionController Tests

    In Rails, it’s essential for each controller action to have tests that assert the core HTTP response properties.

    For a basic Rails controller, you’d test the following:

    • What was the response status code?
    • Did the controller render the expected template?
    • Did the controller render the expected Rails layout?
    • Did the controller set any flash messages?
    • Was any information inserted or deleted from the session?
    • Did the controller redirect the user to a new URL?

    Here’s what such a controller test might look like using shoulda-matchers:

    describe ContactsController do
      describe 'GET #index' do
        context 'when user is logged in' do
          with :user
          before do
            sign_in user
            get :index
          end
          it { is_expected.to respond_with :ok }
          it { is_expected.to render_with_layout :application }
          it { is_expected.to render_template :index }
        end
        context 'when user is logged out' do
          before do
            get :index
          end
          it { is_expected.to redirect_to new_session_path }
          it { is_expected.to set_the_flash(:warning).to('Please log in.') }
          it { is_expected.to set_session(:return_to).to(contacts_path) }
        end
      end
    end

    Controllers are rarely tested in Rails apps because in behavior-driven development a feature spec usually covers the work of multiple controller specs.

    Still, shoulda-matchers makes it a lot easier to test a controller method.

    One-Liner ActiveRecord Tests

    Since ActiveRecord associations are well-tested by the Rails test suite, most people just trust that they’ll work, and don’t feel the need to test them.

    However, if you want to make sure that your models are using any associations you’ve declared, shoulda-matchers can help you accomplish that easily.

    Here’s how you’d write a has_many association for a Contact record, assuming it has many phone numbers using just RSpec:

    describe Contact do
      it "can have many phone numbers" do
        contact = Contact.reflect_on_association(:phones)
        expect(contact.macro).to eql(:has_many)
      end
    end

    The shoulda-matchers version of the has_many association is easier to read, and clearly states the intent of your test. Adding a qualifier is just a matter of chaining the methods available to you.

    describe Contact do
      it { is_expected.to have_many(:phones).dependent(:destroy) }
    end

    As shown in the examples above, shoulda-matchers reduces the amount of test code you need to write for your Rails application — you can write robust tests that aren’t lengthy or complicated. The official documentation for shoulda-matchers shows you more one-liners, along with information about how to use them.

    P.S. Would you like to learn how to build sustainable Rails apps and ship more often? We’ve recently published an ebook covering just that — “Rails Testing Handbook”. Learn more and download a free copy.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Raymond works with SaaS developers, showing them how to improve their metrics through product integrations, micro-targeting and partnerships. He can make your micro SaaS product eat competitor's launch too.