No More Seat Costs: Semaphore Plans Just Got Better!

    15 Jul 2022 · Greatest Hits

    How to Test Rails Models with RSpec

    25 min read
    Contents

    Testing is where we spend most of our time as developers. Good testing raises the quality of software, reduces bugs and, in the long run, makes our work easier.

    In this article, we’ll discuss the basics of testing with Ruby on Rails:

    • What is BDD?
    • How to test models in Rails?
    • How to test business logic with Rspec?
    • How to use Continuous Integration to automate testing?

    What is Behaviour-driven Development?

    Behaviour-driven Development (BDD) as a software development process is composed of multiple sub techniques. Under the hood, it combines Test Driven Development, which allows short feedback loops, with domain-driven design and object-oriented design and analysis. As a development process, it allows the stakeholders and the developers to enjoy the benefits of short feedback loops, writing just the right amount of code and design to make the software work. You can read more about BDD in our tutorial on Behavior-driven Development.

    Introducing our Model

    When we think of Rails models, we usually think of reflecting the problem domain for which we are providing a solution. Models can sometimes be full-blown objects with rich behavior. Other times, they can be just simple data containers or data maps.

    For the purpose of this tutorial, we will be creating a model called Auction. We will keep things simple—each auction will have only one item for sale. The Auction model will have an end date, item title, description of the item on sale and seller.

    Let’s create a new Rails project:

    $ gem install rails rspec
    $ rails new --skip-bundle auctions
    $ cd auctions

    Add the rspec-rails helper to the Gemfile:

    # Gemfile
    
    . . .
    
    group :development, :test do
      gem 'rspec-rails', ">= 3.9.0"
    end

    Install the Gems and complete the setup:

    $ bundle install --path vendor/bundle
    $ bin/rails generate rspec:install
    $ bin/rails webpacker:install 

    Let’s generate this model using Rails generator:

    $ bin/rails generate model Auction \
                         start_date:datetime \
                         end_date:datetime \
                         title:string \
                         description:text
    
          invoke  active_record
          create    db/migrate/20160406205337_create_auctions.rb
          create    app/models/auction.rb
          invoke    rspec
          create      spec/models/auction_spec.rb

    After that, let’s run the migration and prepare the database:

    $ rake db:migrate db:test:prepare
    
    == 20160406205337 CreateAuctions: migrating ===================================
    -- create_table(:auctions)
       -> 0.0021s
    == 20160406205337 CreateAuctions: migrated (0.0023s) ==========================

    For the time being, we’ll omit the bids and the sellers and focus on the title, description, start_date and end_date.

    Since we’ve laid the foundations, let’s get to work. The first thing we’ll tackle is the validations.

    Specifying Validations

    Since we are currently not sure which validations we should have in the model, let’s create some pending examples to get the ball rolling.

    Edit the spec file called spec/models/auction_spec.rb:

    # spec/models/auction_spec.rb
    
    require 'rails_helper'
    
    RSpec.describe Auction, :type => :model do
      it "is valid with valid attributes"
      it "is not valid without a title"
      it "is not valid without a description"
      it "is not valid without a start_date"
      it "is not valid without a end_date"
    end

    Here we have the basic validations specced out. The purpose of the first spec is to make it obvious what is needed to make a valid object of the Auction class. If we run this spec, we will see five pending specs:

    $ rspec spec/models/auction_spec.rb
    
    *****
    
    Pending: (Failures listed here are expected and do not affect your suite's status)
    
      1) Auction is valid with valid attributes
         # Not yet implemented
         # ./spec/models/auction_spec.rb:6
    
      2) Auction is not valid without a title
         # Not yet implemented
         # ./spec/models/auction_spec.rb:7
    
      3) Auction is not valid without a description
         # Not yet implemented
         # ./spec/models/auction_spec.rb:8
    
      4) Auction is not valid without a start_date
         # Not yet implemented
         # ./spec/models/auction_spec.rb:9
    
      5) Auction is not valid without a end_date
         # Not yet implemented
         # ./spec/models/auction_spec.rb:10
    
    
    Finished in 0.00216 seconds (files took 1.18 seconds to load)
    5 examples, 0 failures, 5 pending

    Since all of the specs are pending, let’s implement the first one.

    # spec/models/auction_spec.rb
    
    require 'rails_helper'
    
    RSpec.describe Auction, :type => :model do
      it "is valid with valid attributes" do
        expect(Auction.new).to be_valid
      end
    
      it "is not valid without a title"
      it "is not valid without a description"
      it "is not valid without a start_date"
      it "is not valid without a end_date"
    end

    Since we have not added any constraints to the model, our model object will be valid without specifying any attributes. Let’s run the spec and see it in effect:

    $ rspec spec/models/auction_spec.rb
    
    .****
    
    Pending: (Failures listed here are expected and do not affect your suite's status)
    
      1) Auction is not valid without a title
         # Not yet implemented
         # ./spec/models/auction_spec.rb:8
    
      2) Auction is not valid without a description
         # Not yet implemented
         # ./spec/models/auction_spec.rb:9
    
      3) Auction is not valid without a start_date
         # Not yet implemented
         # ./spec/models/auction_spec.rb:10
    
      4) Auction is not valid without a end_date
         # Not yet implemented
         # ./spec/models/auction_spec.rb:11
    
    
    Finished in 0.01073 seconds (files took 1.17 seconds to load)
    5 examples, 0 failures, 4 pending

    Next, let’s implement the second example. For this example, we will need to create an Auction object without a title.

    # spec/models/aution_spec.rb
    
    . . .
    
    it "is not valid without a title" do
      auction = Auction.new(title: nil)
      expect(auction).to_not be_valid
    end
    
    . . .

    If we run this spec now, we should see it failing.

    $ rspec spec/models/auction_spec.rb
    
    .F***
    
    Pending: (Failures listed here are expected and do not affect your suite's status)
    
      1) Auction is not valid without a description
         # Not yet implemented
         # ./spec/models/auction_spec.rb:13
    
      2) Auction is not valid without a start_date
         # Not yet implemented
         # ./spec/models/auction_spec.rb:14
    
      3) Auction is not valid without a end_date
         # Not yet implemented
         # ./spec/models/auction_spec.rb:15
    
    
    Failures:
    
      1) Auction is not valid without a title
         Failure/Error: expect(auction).to_not be_valid
           expected #<Auction id: nil, start_date: nil, end_date: nil, title: nil, description: nil, created_at: nil, updated_at: nil> not to be valid
         # ./spec/models/auction_spec.rb:10:in `block (2 levels) in <top (required)>'
    
    Finished in 0.03033 seconds (files took 1.39 seconds to load)
    5 examples, 1 failure, 3 pending
    
    Failed examples:
    
    rspec ./spec/models/auction_spec.rb:8 # Auction is not valid without a title

    As you can see, the example failed because our validation functionality needs to be added to the model. Since we’ve completed the red step (from the red-green-refactor loop), let’s get our test passing. To get this step working, we need to add the validation on the title attribute to app/models/auction.rb :

    # app/models/auction.rb 
    
    class Auction < ActiveRecord::Base
      validates_presence_of :title
      validates_presence_of :description
      validates_presence_of :start_date
      validates_presence_of :end_date
    end

    With the validation in place, we can run the tests again:

    $ rspec spec/models/auction_spec.rb
    
    F.***
    
    Pending: (Failures listed here are expected and do not affect your suite's status)
    
      1) Auction is not valid without a description
         # Not yet implemented
         # ./spec/models/auction_spec.rb:13
    
      2) Auction is not valid without a start_date
         # Not yet implemented
         # ./spec/models/auction_spec.rb:14
    
      3) Auction is not valid without a end_date
         # Not yet implemented
         # ./spec/models/auction_spec.rb:15
    
    
    Failures:
    
      1) Auction is valid with valid attributes
         Failure/Error: expect(Auction.new).to be_valid
           expected #<Auction id: nil, start_date: nil, end_date: nil, title: nil, description: nil, created_at: nil, updated_at: nil> to be valid, but got errors: Title can't be blank
         # ./spec/models/auction_spec.rb:5:in `block (2 levels) in <top (required)>'
    
    Finished in 0.02039 seconds (files took 1.18 seconds to load)
    5 examples, 1 failure, 3 pending
    
    Failed examples:
    
    rspec ./spec/models/auction_spec.rb:4 # Auction is valid with valid attributes

    Our example is passing now, but our first example fails. This happened due to the newly introduced validation. Let’s fix our first spec now:

    # spec/models/auction_spec.rb
    
    . . .
    
      subject { described_class.new }
    
      it "is valid with valid attributes" do
        subject.title = "Anything"
        subject.description = "Anything"
        subject.start_date = DateTime.now
        subject.end_date = DateTime.now + 1.week
        expect(subject).to be_valid
      end
    
    . . .

    To get the first example passing, we need to specify the title to the Auction object, which is the subject of our example. Let’s run the specs again now:

    $ rspec spec/models/auction_spec.rb
    
    .***
    
    Pending: (Failures listed here are expected and do not affect your suite's status)
    
      1) Auction is not valid without a description
         # Not yet implemented
         # ./spec/models/auction_spec.rb:18
    
      2) Auction is not valid without a start_date
         # Not yet implemented
         # ./spec/models/auction_spec.rb:19
    
      3) Auction is not valid without a end_date
         # Not yet implemented
         # ./spec/models/auction_spec.rb:20
    
    
    Finished in 0.01422 seconds (files took 1.17 seconds to load)
    5 examples, 0 failures, 3 pending

    Since our specs are passing, we’ve reached the green step of the red-green-refactor flow. It’s time to refactor our examples. This refactoring step is quite simple. We’ll add a subject to our specs, which will be the main object under test in this spec file. After that, we will appropriately set the object attributes in the examples. This is what our refactored spec will look like:

    # spec/models/auction_spec.rb
    
    . . .
    
      subject {
        described_class.new(title: "Anything",
                            description: "Lorem ipsum",
                            start_date: DateTime.now,
                            end_date: DateTime.now + 1.week,
                            user_id: 1)
      }
    
    . . .

    If we run our specs now, they will pass. Let’s add the rest of the validations by writing the specs first, and the implementation after. To save up space, we will write all of the specs at once, and get them passing after that.

    # spec/models/auction_spec.rb
    
    require 'rails_helper'
    
    RSpec.describe Auction, :type => :model do
      subject {
        described_class.new(title: "Anything",
                            description: "Lorem ipsum",
                            start_date: DateTime.now,
                            end_date: DateTime.now + 1.week,
                            user_id: 1)
      }
    
      it "is valid with valid attributes" do
        expect(subject).to be_valid
      end
    
      it "is not valid without a title" do
        subject.title = nil
        expect(subject).to_not be_valid
      end
    
      it "is not valid without a description" do
        subject.description = nil
        expect(subject).to_not be_valid
      end
    
      it "is not valid without a start_date" do
        subject.start_date = nil
        expect(subject).to_not be_valid
      end
    
      it "is not valid without a end_date" do
        subject.end_date = nil
        expect(subject).to_not be_valid
      end
    end

    Our specs are done, so let’s run them:

    $ rspec spec/models/auction_spec.rb
    
    ....
    
    Finished in 0.01586 seconds (files took 1.19 seconds to load)
    5 examples, 0 failures

    Although the specs are working, as you can notice, we have quite a lot of duplication. Let’s see how we can refactor these examples to make them more concise.

    Refactoring and Getting back to Red

    The RSpec testing framework provides some quite useful utilities. We have seen one of them already—the subject method. We should see the subject as the testing subject—meaning that it will be the object upon which our examples will set expectations.

    # spec/models/auction_spec.rb
    
    require 'rails_helper'
    
    RSpec.describe Auction, :type => :model do
      subject {
        described_class.new(title: "Anything",
                            description: "Lorem ipsum",
                            start_date: DateTime.now,
                            end_date: DateTime.now + 1.week)
      }
    
      it "is valid with valid attributes" do
        expect(subject).to be_valid
      end
    
      it "is not valid without a title" do
        subject.title = nil
        expect(subject).to_not be_valid
      end
    
      it "is not valid without a description" do
        subject.description = nil
        expect(subject).to_not be_valid
      end
    
      it "is not valid without a start_date" do
        subject.start_date = nil
        expect(subject).to_not be_valid
      end
    
      it "is not valid without a end_date" do
        subject.end_date = nil
        expect(subject).to_not be_valid
      end
    end

    By making our subject valid, our examples will get much simpler. For every example, we can remove the attribute that we would like to assert upon. If we run the tests now, we’ll see that they are still passing:

    $ rspec spec/models/auction_spec.rb
    
    .....
    
    Finished in 0.01468 seconds (files took 1.34 seconds to load)
    5 examples, 0 failures

    Now, let’s go back and remind ourselves of what attributes and relationships an Auction object should have. Since an auction, in fact, means a sale, we need a person that will sell the item. In other words, an Auction should have a seller.

    Since both of these associations represent a person, we’ll need to introduce a User model, and add the appropriate associations to the Auction. Instead of going in-depth with the User model and its specs, we’ll go over them quickly, and continue with the implementation of the Auction model:

    $ bin/rails generate model User password:string email:string
    $ rake db:migrate db:test:prepare

    Here is our model:

    # app/models/user.rb
    
    class User < ApplicationRecord
      validates_presence_of :password, :email
    end

    Next, let’s take a look at the spec for the User model, spec/models/user_spec.rb:

    # spec/models/user_spec.rb
    
    require 'rails_helper'
    
    RSpec.describe User, :type => :model do
      subject { 
             described_class.new(password: "some_password", 
                                 email: "john@doe.com"
             )  
      }
    
      describe "Validations" do
        it "is valid with valid attributes" do
          expect(subject).to be_valid
        end
    
        it "is not valid without a password" do
          subject.password = nil
          expect(subject).to_not be_valid
        end
    
        it "is not valid without an email" do
          subject.email = nil
          expect(subject).to_not be_valid
        end
      end
    end

    To make testing simpler, we’ll use the shoulda matchers gem. By adding this gem to our bundle, we can easily test our associations using:

    # spec/models/auction_spec.rb
    
    . . .
    
    describe "Associations" do
      it { should belong_to(:user).without_validating_presence }
    end
    
    . . .

    To add the shoulda gem, edit Gemfile and add the package inside the development or test group:

    group :development, :test do
    
      . . .
    
      gem 'shoulda-matchers'
    end

    Add the following setup lines in spec/rails_helper.rb:

    # spec/rails_helper.rb
    
    . . .
    
    Shoulda::Matchers.configure do |config|
      config.integrate do |with|
        with.test_framework :rspec
        with.library :rails
      end
    end
    

    Then call bundle to install it:

    $ bundle install --path vendor/bundle

    If we run our specs at this point, they will fail. Let’s make them pass by adding the appropriate associations in our Auction model:

    # app/models/auction.rb
    
    class Auction < ActiveRecord::Base
      belongs_to :user, optional: true
      validates_presence_of :title, :description, :start_date, :end_date
    end

    Generate a migration for the database. We need to add a user_id column in the auction table:

    $ rails generate migration AddUserToAuctions user_id:integer 
    $ rake db:migrate db:test:prepare

    If we run our tests now, they will pass:

    $ .....
    
    Finished in 0.02151 seconds (files took 1.25 seconds to load)
    6 examples, 0 failures

    Having the user association in place, we can think about bidding. An auction can have multiple bids. While auctions are a very interesting and difficult problem to solve, for the purpose of this tutorial, we will add another model which will be called Bid.

    Adding Business Behaviour

    If you think about real-life auctions, you will immediately think about the auctioneer chants. The integral part of these chants is the bids made by the bidders. An Auction can have multiple bids, which, when put into code, translates to a new association. Also, a Bid can have only one bidder, which will be an association by itself.

    Let’s introduce the Bid model:

    $ rails g model Bid \
                    bidder_id:integer \
                    auction_id:integer \
                    amount:integer
    $ rake db:migrate db:test:prepare

    The model should have only one validation—it should validate the presence of a bidder:

    # spec/models/bid_spec.rb 
    
    require 'rails_helper'
    
    RSpec.describe Bid, :type => :model do
      describe "Associations" do
        it { should belong_to(:bidder) }
        it { should belong_to(:auction) }
      end
    
      describe "Validations" do
        it { should validate_presence_of(:bidder) }
      end
    end

    If we run this spec now, it will fail:

    $ rspec spec/models/bid_spec.rb
    
    FFF
    
    Failures:
    
      1) Bid Associations is expected to belong to bidder required: true
         Failure/Error: it { should belong_to(:bidder) }
           Expected Bid to have a belongs_to association called bidder (no association called bidder)
         # ./spec/models/bid_spec.rb:7:in `block (3 levels) in <top (required)>'
    
      2) Bid Associations is expected to belong to auction required: true
         Failure/Error: it { should belong_to(:auction) }
           Expected Bid to have a belongs_to association called auction (no association called auction)
         # ./spec/models/bid_spec.rb:8:in `block (3 levels) in <top (required)>'
    
      3) Bid Validations is expected to validate that :bidder cannot be empty/falsy
         Failure/Error: it { should validate_presence_of(:bidder) }
         
         Shoulda::Matchers::ActiveModel::AllowValueMatcher::AttributeDoesNotExistError:
           The matcher attempted to set :bidder on the Bid to nil, but that
           attribute does not exist.
         # ./spec/models/bid_spec.rb:12:in `block (3 levels) in <top (required)>'
    
    Finished in 0.01407 seconds (files took 1.22 seconds to load)
    3 examples, 3 failures
    
    Failed examples:
    
    rspec ./spec/models/bid_spec.rb:7 # Bid Associations is expected to belong to bidder required: true
    rspec ./spec/models/bid_spec.rb:8 # Bid Associations is expected to belong to auction required: true
    rspec ./spec/models/bid_spec.rb:12 # Bid Validations is expected to validate that :bidder cannot be empty/falsy

    To make this test pass, we only need to add the association and the validation to the model:

    # app/models/bid.rb
    
    class Bid < ApplicationRecord
      belongs_to :bidder, class_name: "User"
      belongs_to :auction, class_name: "Auction"
    
      validates_presence_of :bidder
    end

    This will make our tests pass. Now, to add the necessary business logic, we first need to think about the bidding process. The core business logic around bidding requires that each bid must be bigger than the previous bid. This means that every Auction will have multiple Bids. Let’s add a test for that association and introduce it to the model.

    We will again use the shoulda matcher for the test:

    # spec/models/auction_spec.rb
    
    . . .
    
    describe "Associations" do
      it { should belong_to(:user).without_validating_presence }
      it { should have_many(:bids) }
    end

    If we run the spec now, the tests will fail. We need to introduce the association:

    # app/models/auction.rb
    
    class Auction < ActiveRecord::Base
      belongs_to :user, optional: true
      has_many :bids
      validates_presence_of :title, :description, :start_date, :end_date
    end

    Our specs will carry on failing because we will need to have a user for our test subject. We also need to add the a user_id to the Auction. The foreign keys can be easily added using a migration, so we will skip that part here. To get our specs back to a green state, we need to update our subject. This means that we need to create one User object — one seller and assign it to the subject Auction:

    # spec/models/auction_spec.rb
    
    require 'rails_helper'
    
    RSpec.describe Auction, :type => :model do
    
      let(:seller) {
        User.new(:email => "jane@doe.com", :password => "pw1234")
      }
      subject {
        described_class.new(title: "Anything",
                            description: "Lorem ipsum",
                            start_date: DateTime.now,
                            end_date: DateTime.now + 1.week,
                            user_id: 1)
      }
    
    . . .

    To add business logic, we need validation. After adding a new Bid to the Auction, we need to check if the bid is bigger than the previous one. Because of the intersection between the Auction and the Bid objects, we will create an additional class, which will contain all of the bidding logic required for an Auction. We’ll call it BiddingEngine. First, let’s add the spec for it:

    # specs/models/bidding_engine_spec.rb
    
    require 'rails_helper'
    
    RSpec.describe BiddingEngine do
      describe ".bid!" do
        it "create a new bid on an auction" do
          seller = User.create(:email => 'jane@doe.com', :password => 'pw1234' )
          bidder = User.create(:email => 'john@doe.com', :password => 'pw1234' )
    
          auction = Auction.create(title: 'Anything', description: 'Lorem ipsum',
                       start_date: DateTime.now, end_date: DateTime.now + 1.week,
                       user_id: seller.id)
    
          described_class.bid!(auction, 100, bidder)
          expect(auction.errors).to be_empty
    
          described_class.bid!(auction, 90, bidder)
          expect(auction.errors[:bid].first).to eq "must be bigger than the last bid on the auction"
        end
      end
    end
    

    The BiddingEngine class will have the bid! method, which will contain the required logic to set a new bid on the Auction. Using this class, we will have only a single entry point towards the, often fragile, bidding process. By having this in place, we’ll have an overview of how the bidding process is done.

    Let’s add the implementation of this class:

    # app/models/bidding_engine.rb
    
    class BiddingEngine
      def self.bid!(auction, amount, bidder)
        new(auction, amount, bidder).bid!
      end
    
      def initialize(auction, amount, bidder)
        @auction = auction
        @bid = Bid.new(bidder: bidder, auction: @auction, amount: amount)
      end
    
      def bid!
        if @bid.valid? && is_bigger?
          @bid.save
        else
          @auction.errors.add(:bid, "must be bigger than the last bid on the auction")
        end
      end
    
      private
    
      def is_bigger?
        return true unless @auction.bids.last
        @auction.bids.last.amount < @bid.amount
      end
    end

    As you can see, the implementation of the class is quite simple. It will only have the bid! method, and it will perform only one validation, which is the business behavior that we wanted to achieve.

    Some of you might go down the road of enabling accepts_nested_attributes_of for Bid in the Auction class, but it’s easier to have this logic separated into a class. It will add more separation and better isolation. Also, using the BiddingEngine class in the controllers will be easier.

    Covering Edge Cases

    When a User creates a new the Bid on the Auction, we always check if it’s bigger than the last bid in the auction. However, we also need to take care of the edge case where the user tries to add a bid with the same amount as the last bid.

    This is an edge case, but we still have to think about it and solve it. Otherwise, we will have conflicting winners in the auctions. Let’s write a spec about this behavior, and then fix the functionality. In the test for the BiddingEngine, we will need to add another expectation stating that a Bid cannot be equal with the previous Bid on the Auction.

    # spec/models/bidding_engine.rb
    
    . . .
    
    it "cannot create a bid if its an equal amount as the last bid" do
      seller = User.create(:email => 'jane@doe.com', :password => 'pw1234')
      bidder = User.create(:email => 'john@doe.com', :password => 'pw1234')
    
      auction = Auction.create(title: 'Anything', description: 'Lorem ipsum',
               start_date: DateTime.now, end_date: DateTime.now + 1.week,
               user_id: seller.id)
    
      described_class.bid!(auction, 100, bidder)
      expect(auction.errors).to be_empty
    
      described_class.bid!(auction, 100, bidder)
      expect(auction.errors[:bid].first).to eq "must be bigger than the last bid on the auction"
    end
    

    As you can see, our spec is almost the same as the previous one. It will check for the validation errors on the Auction, stating that the Bid must be a higher amount. Now, we can easily refactor our specs by using RSpec’s let statements:

    # spec/models/biddingengine.rb
    
    require 'rails_helper'
    
    RSpec.describe BiddingEngine do
      describe ".bid!" do
        let(:seller) { User.create(:email => 'jane@doe.com', :password => 'pw1234') }
        let(:bidder) { User.create(:email => 'john@doe.com', :password => 'pw1234') }
        let(:auction) { Auction.create(title: 'Anything', description: 'Lorem ipsum', start_date: DateTime.now, end_date: DateTime.now + 1.week, user_id: seller.id) }
    
        it "create a new bid on an auction if bid is bigger than last bid on auction" do
          described_class.bid!(auction, 100, bidder)
          expect(auction.errors).to be_empty
    
          described_class.bid!(auction, 90, bidder)
          expect(auction.errors[:bid].first).to eq "must be bigger than the last bid on the auction"
        end
    
        it "cannot create a bid if its an equal amount as the last bid" do
          described_class.bid!(auction, 100, bidder)
          expect(auction.errors).to be_empty
    
          described_class.bid!(auction, 100, bidder)
          expect(auction.errors[:bid].first).to eq "must be bigger than the last bid on the auction"
        end
      end
    end

    As you can notice, the specs now look much tidier and easier to understand. We also covered an edge case, and these specs help us to prevent breaking the bidding flow and the validation of the auctions.

    Continuous Integration

    Continuous Integration (CI) is a software development practice in which the code is continually tested on an automated CI Pipeline. Teams using CI enjoy the benefits of having the code continually tested, they can merge changes more often, usually many times a day. A good CI setup raises the bar of software quality considerably.

    You can add your project to Semaphore CI for free in a few minutes. Semaphore has full support for Ruby and Ruby on Rails.

    Prerequisites

    Before you can use Semaphore to drive your CI pipelines, you’ll need:

    • GitHub: A GitHub account.
    • Repository: A GitHub repository (follow the create a repo instructions).
    • Git: install Git to handle the code.
    • Semaphore: use the Sign up with GitHub button on the top-right corner to get a free account.

    Now push the code to the GitHub repository:

    • Use the Clone or download button to get your repository push address:
    • Push the code with:
    $ git remote add origin YOUR_REPO_ADDRESS
    $ git pull origin master
    $ git add -A 
    $ git commit -m "initial commit"
    $ git push origin master

    Add Your Project to Semaphore

    Once your code is securely stored on GitHub, it’s time to add CI/CD to it:

    • Head to your Semaphore account.
    • Use the + (plus sign) next to Projects on the left navigation menu:
    • Find your project and click on Choose:
    • Select the Ruby on Rails starter workflow:
    • Click on Customize it first.

    Semaphore will show you the Workflow Builder:

    It’s main elements are:

    • Pipeline: a pipeline is made of blocks that are executed from left to right. Pipelines usually have a specific goal such as testing or building.
    • Agent: The agent is the virtual machine that powers the pipeline. We have three machine types to choose from. The machine runs an optimized Ubuntu 18.04 image with build tools for many languages.
    • Block: blocks group jobs that can be executed in parallel. Jobs in a block usually have similar commands and configurations. Once all jobs in a block complete, the next block begins.
    • Job: jobs define the commands that do the work. They inherit their configuration from the parent block.

    The starter workflow sets up a PostgreSQL database that we don’t need. We can remove it easily:

    • On the job command box, delete the sem-service start postgres line.
    • Open the Environment Variables section and remote the DATABASE_URL variable:
    • Click on Run the Workflow and then Start:

    Semaphore is now building and testing the application on every update:

    Optimizing the Pipeline

    Our pipeline does a lot in one job:

    • Downloads the Gems.
    • Prepares the database.
    • Runs the tests.

    For a simple example like ours, having everything in one job is not the end of the world. But, if we were to add more tests, more steps, or even a deployment pipeline, having it all in one place doesn’t scale well.

    We can improve the pipeline by separating the build and the testing process in two:

    • Click on Edit Workflow to open the Workflow Editor.
    • Click on the block, we’ll replace its name with “Bundle”
    • Change the title of the job to “Install”
    • Type the following commands in the box:
    checkout
    sem-version ruby 2.6.5
    cache restore
    bundle install --deployment --path vendor/bundle
    cache store

    We’ve replaced the Test job with the Install job. This job only responsibility is to download the Gems and store the in the cache:

    • sem-version: a Semaphore built-in command to manage programming language versions. Semaphore supports most Ruby versions.
    • checkout: another built-in command, checkout clones the repository and changes the current directory.
    • cache: the cache commands provides read and write access to Semaphore’s cache, a project-wide storage for the jobs. cache store saves the vendor directory in the cache and cache restore retrieves it.

    Let’s re-add the test job:

    • Use the +Add Block dotted line button to add a new block.
    • Call the block “Test”.
    • Call the job “Rspec”.
    • Open the Prologue. The prologue is executed before all jobs in the block and is normally used for shared setup commands:
    checkout
    sem-version ruby 2.6.5
    cache restore
    bundle install --deployment --path vendor/bundle
    bundle exec rake db:setup
    • Open the Environment Variables section. Set the RAILS_ENV = test variable.
    • Type the following commands in the job box:
    bundle exec rspec
    • Click on Run the Workflow and Start.

    Great! Our pipeline is shaping up nicely. Now you can add more tests in the test block without increasing the overall execution time, as all tests can run in parallel.

    NB: Semaphore also has a Test Reports feature that allows you to see which tests have failed, find the slowest tests in your test suite, and find skipped tests. Read more about the feature and how it can help your team.

    Next Steps

    Once you have your Rails application ready for prime time, you’ll want to deploy it.

    One great alternative for deployment is Docker. Docker creates portable containers that make it easy to run your application anywhere with minimal setup.

    Semaphore can build and test the Docker images in seconds. To learn more, head over to the Docker overview page and be sure to not miss the Dockerizing Ruby tutorial.

    Conclusion

    In this tutorial, we saw how we can approach testing models in Ruby on Rails. As you can see, model specs are very different from the kind of specs we would write for a regular Ruby object. Aside from the DSL that Rails provide for the model, RSpec also provides us with model specs, which allow us to properly unit test our models. We saw how we can tackle issues in the BDD way, by using specs and test-driven development.

    What is your preferred way to test models? Do you use BDD in your everyday work? When testing, do you use the red-green-refactor cycle? Feel free to leave a comment below.

    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.

    2 thoughts on “How to Test Rails Models with RSpec

    Leave a Reply

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

    Avatar
    Writen by:
    Full-stack Ruby on Rails developer. Blogs regularly at eftimov.net.