Learn the First Best Practices for Rails and RSpec


Learn the First Best Practices for Rails and RSpec

January 11, 2016

This article was peer reviewed by Thom Parkin. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!


Writing code without testing can be a deceptively smooth ride; one that, unfortunately, comes to a screeching halt when you start working with other developers. This tutorial is aimed at helping Ruby on Rails developers (especially the new folks) in setting up an RSpec test suite using best practices, in the Behaviour Driven Development way. I know that testing can be pain when you are just getting into it. There are lots of resources online to help you on this journey. I am hopeful this will be of benefit to new Ruby on Rails developers since I just started getting into testing myself.

In this tutorial we will learn the following:
* How to setup – RSpec, Capybara, Shoulda-Matchers, Database Cleaner
* How to create a factory using Factory Girl Rails and Faker
* How to write Model Specs
* How to write Controller Specs
* How to write Feature Specs
* How to check spec coverage using SimpleCov

Rails Application

rails new myapp

Installing RSpec

Open up your Gemfile and add the rspec-rails gem to the development and test group:

group :development, :test do
  gem 'byebug'
  gem 'rspec-rails', '~> 3.4'
end

Install the gems:

bundle install

Now run:

rails generate rspec:install

This adds the spec directory and some skeleton files, including a .rspec file. Before going further, go ahead and remove the test directory that was generated with our Rails application.

Shoulda-Matchers and Database Cleaner

Open up your Gemfile and add a test group containing the shoulda-matcher and database_cleaner gems:

Gemfile

group :test do
  gem 'shoulda-matchers', '~> 3.0', require: false
  gem 'database_cleaner', '~> 1.5'
end

Run bundle install.

Shoulda-Matchers Configuration

Shoulda-Matchers provides one-line matchers to RSpec used in testing Rails functionality which we will see briefly. Open your spec/rails_helper.rb file and configure shoulda-matchers to work with RSpec by pasting in the following:

require 'shoulda/matchers'

Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

Database Cleaner Configuration

Database Cleaner is a set of strategies for cleaning your database in Ruby between test runs. It helps to ensure a clean state for testing.

To integrate database_cleaner, make the following adjustment to spec/rails_helper.rb:

config.use_transactional_fixtures = false

Create a new directory called support inside of your spec directory:

mkdir spec/support

Inside it, create a new file, database_cleaner.rb and paste in the following:

RSpec.configure do |config|

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, :js => true) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end

With that, database_cleaner is set up and will clean the database between each unit test and test suite.

Capybara Setup

Capybara is an automation framework used for creating functional tests that simulates how users will interact with your application. Add the capybara gem to the :development, :test group in your Gemfile, the same group where you added rspec-rails:

gem 'capybara', '~> 2.5'

and bundle install.

Open up your spec_helper.rb and require the capybara gem:

***spec/spec_helper.rb***
...
require 'capybara/rspec'

Faker and Factory Girl Setup

Faker is useful in generating random data for your test. You will see how to use it with factory_girl_rails in making factory. Add the faker gem to the :test group in your Gemfile:

gem 'faker', '~> 1.6.1'

Factory Girl allows you create objects that you need in your tests which can include default values. With faker, you will be able to create random objects for your test instead of using just one default value.

Add factory_girl_rails gem to :development, :test group:

gem 'factory_girl_rails', '~> 4.5.0'

Then bundle install.

At this point, your Gemfile should look like this:

source 'https://rubygems.org'


gem 'rails', '4.2.4'
gem 'sqlite3'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-rails'
gem 'turbolinks'
gem 'jbuilder', '~> 2.0'
gem 'sdoc', '~> 0.4.0', group: :doc

group :development, :test do
  gem 'byebug'
  gem 'rspec-rails', '~> 3.4'
  gem 'factory_girl_rails', '~> 4.5'
  gem 'capybara', '~> 2.5'
end

group :development do
  gem 'web-console', '~> 2.0'
  gem 'spring'
end

group :test do
  gem 'shoulda-matchers', '~> 3.0', require: false
  gem 'database_cleaner', '~> 1.5'
  gem 'faker', '~> 1.6.1'
end

Creating a Factory

Create a directory named factories in your spec folder, also adding a file named after your model. E.g contacts.rb

***spec/factories/contacts.rb***

FactoryGirl.define do
  factory :contact do
    full_name     { Faker::Name.name }
    email         { Faker::Internet.email }
    phone_number  { Faker::PhoneNumber.phone_number }
    address       { Faker::Address.street_address }
  end
end

In the above, I just showed you how to create fixtures using factory_girl_rails and faker. You do not have to explicitly enter objects. Factory Girl uses the random (fake) values generated by faker to create factories that will be used whenever you run your test.

Model Specs

We want to ensure that the factory we created above is valid for the model. Create a file in your model/spec folder for your model:

***spec/models/contact_spec.rb***

require 'rails_helper'

RSpec.describe Contact, type: :model do
  it "has a valid factory" do
    expect(contact).to be_valid
  end
end

From your terminal run:

rspec spec/models/contact_spec.rb

This will show an error, because you do not have Contact model. Go to the terminal, create the model, and migrate your database:

rails g model Contact full_name:string email:string phone_number:integer address:text
rake db:migrate

Run the spec again and viola! It should pass :)

We’ll wrap up the model test using shoulda-matchers to validate the presence of full name, email, phone number and address. In your contact model spec, create a new describe block:

***spec/models/contact_spec.rb***

  describe Contact do
    it { is_expected.to validate_presence_of(:full_name) }
    it { is_expected.to validate_presence_of(:email) }
    it { is_expected.to validate_presence_of(:phone_number) }
    it { is_expected.to validate_presence_of(:address) }
  end

Run your spec and watch it fail. To have it pass, go to the model and implement the validation code:

***app/model/contact.rb***

class Contact < ActiveRecord::Base
  validates_presence_of :full_name, :email, :phone_number, :address
end

Run the spec again and it will pass. Before you move to controller spec, I want to explain some of the terms we have used above.

RSpec DSL Pieces

RSpec is a DSL for creating executable examples of how code is expected to behave, organized in groups.

describe creates an example group. It takes an argument that tells what the spec is about. The argument can be a class, module, method or a string description.

it creates an example and takes a description of the example. (Example; it has status code 400). It is a best practice to limit the spec description to 40 characters or less. If it needs to be longer, you probably should consider using a context to create “sub-contexts” of a describe block.

expect lets you express expected outcomes on an object in an example. It takes an object or block and is used with either to or not_to alongside a matcher (e.g eq(), be_valid). While there are many built-in matchers, gems like Shoulda Matchers add even more to make your specs expressive and concise.

Controller Spec

The first controller spec will test if a new contact gets created with valid attributes. Open up your terminal and generate the controller:

rails g controller Contacts

This will automatically generate the necessary folders for your controller spec. You can see them at spec/controllers.

It’s time to write the spec to test the create action:

***spec/controllers/contacts_controller_spec.rb***

require 'rails_helper'

RSpec.describe ContactsController, type: :controller do

  describe "POST #create" do
    context "with valid attributes" do
      it "create new contact" do
        post :create, contact: attributes_for(:contact)
        expect(Contact.count).to eq(1)
      end
    end
  end
end

Run the spec:

rspec spec/controllers/contacts_controller_spec.rb

This will fail because you do not have a route that matches your controller. Open up the routes file and drop this in:

***config/routes.rb***
...
resources :contacts

Run your spec again and it should still fail with the error The action 'create' could not be found for ContactsController.

Open up your controller and drop in the necessary code to create a new contact.

***app/controllers/contacts_controller.rb***

class ContactsController < ApplicationController
  def new
    @contact = Contact.new
  end

  def create
    @contact = Contact.new(contact_params)

    respond_to do |format|
      if @contact.save
        format.html { redirect_to @contact }
        format.json { render :show, status: :created, location: @contact }
      else
        format.html { render :new }
        format.json { render json: @contact.errors, status: :unprocessable_entity}
      end
    end
  end

  private

  def contact_params
    params.require(:contact).permit(:full_name, :email, :phone_number, :address)
  end
end

Save and run the spec one more time, and it should pass.

Following the spec above, write a spec that uses invalid attributes to create a new contact. This spec should check that the contact is not created:

***spec/controllers/contacts_controller_spec.rb***

context "with invalid attributes" do
  it "does not create a new contact" do
    post :create, contact: attributes_for(:invalid_contact)
      expect(Contact.count).to eq(0)
    end
  end
end

Using the spec you wrote above as an example, you can churn out specs for other controller actions.

Feature Spec

Feature Specs are high-level tests that work through your application ensuring that every component works. They are usually written from the perspective of a user.

For the purpose of this tutorial, you will write a spec to test the creation of a new contact. Using the capybara gem, the specs will fill in a form with valid attributes and test that the show page displays the expected text when a contact is created.

Open your terminal or text editor and create folders and a file following the path below:

spec/features/contacts/create_spec.rb

Paste the following code into that file:

***spec/features/contacts/create_spec.rb***
require 'rails_helper'

RSpec.feature "Contact", :type => :feature do
  scenario "Create a new contact" do
    visit "/contacts/new"

    fill_in "Full name", :with => "My Name"
    fill_in "Email", :with => "my@email.com"
    fill_in "Phone number", :with => "123456789"
    fill_in "Address", :with => "34, Allen Way, OA"

    click_button "Create Contact"

    expect(page).to have_text("My Name")
  end
end

The spec above tests if the show page has the text My Name which is the value written into the full name input. Run this spec and it should fail.

First add a show action to your controller:

***app/controllers/contacts_controller.rb

def show
  @contact = Contact.find(params[:id])
end

Create the following files in your contacts view: new.html.erb, _form.html.erb, show.html.erb and fill in with the following code:

***app/views/contacts/_form.html.erb***

<%= form_for(@contact) do |f| %>
  <% if @contact.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@contact.errors.count, "error") %> prohibited this contact from being saved:</h2>

      <ul>
      <% @contact.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :full_name %><br>
    <%= f.text_field :full_name %>
  </div>
  <div class="field">
    <%= f.label :email %><br>
    <%= f.text_area :email %>
  </div>
  <div class="field">
    <%= f.label :phone_number %><br>
    <%= f.text_area :phone_number %>
  </div>
  <div class="field">
    <%= f.label :address %><br>
    <%= f.text_area :address %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

***app/views/contacts/new.html.erb***

<h2>Create new contact</h2>

<%= render 'form' %>

***app/views/contacts/show.html.erb***

<p id="notice"><%= notice %></p>

<p>
  <strong>Full Name:</strong>
  <%= @contact.full_name %>
</p>

<p>
  <strong>Email:</strong>
  <%= @contact.email %>
</p>

<p>
  <strong>Phone Number:</strong>
  <%= @contact.phone_number %>
</p>

<p>
  <strong>Address:</strong>
  <%= @contact.address %>
</p>

Run the spec and it should pass this time.

Coverage using SimpleCov

The SimpleCov gem is a code coverage analysis tool for Ruby. You will use it in your application to see how much of your code is tested. To install, open up the Gemfile and add to the test group:

gem 'simplecov', :require => false

Run bundle install.

Next open your spec helper are require simplecov

require 'simplecov'
SimpleCov.start

The next time you run the specs, a new folder with the name coverage will be generated. Open up your browser and point to: your-app-directory/coverage/index.html to see the coverage statistics for your application. You’ll want to add the coverage folder to .gitignore file so it does not get added to your remote repository.

Conclusion

This article aims to give you a foundation for setting up RSpec for your Rails application. These are the basic steps and, from here, you’ll be able to explore the vast syntax of RSpec and start refining your specs. At this point, you should be able to:

  • Set up a test suite for your application.
  • Write model, controller, and feature specs.
Here are some more resources to help you:

With more practice, in no time you will be fluent in testing your Rails application.

阅读量: 808
发布于:
修改于: