Our new ebook “CI/CD with Docker & Kubernetes” is out. Download it here.

How to Test Rails Models with RSpec

Test Rails Models with RSpec

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.

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.

One thought on “How to Test Rails Models with RSpec

Leave a Reply

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

Sign up for a weekly Semaphore newsletter