DEV Community

Cover image for Your Guide to testing in Ruby on Rails 5
Rob Race
Rob Race

Posted on

Your Guide to testing in Ruby on Rails 5

Note: This article is a sample chapter in my upcoming book Building a SaaS Ruby on Rails 5. The book will guide you from humble beginnings through deploying an app to production. If you find this type of content valuable, the book is on pre-sale right now!


Testing, while requiring a little bit more cognitive load up front, and even loathed by some developers, will often pay dividends down the road. It will almost always have you saying "I'm glad I wrote tests for this" at some point. TDD(Test Driven Development) is one approach that has grown on the Rails community. Although, not completely accepted by all in the community. This approach has you writing your test(s) first and then writing code to make your tests pass.

Why Test

Testing will provide a few main benefits when done correctly. First, it will help you develop the behaviors within your app. You may write a test that your model is validating the presence of an email. Then realizing you needed to include validations for the password or add a "retype password" input box.

Another great reason to test is to protect yourself against regressions within your application as your modifying code or adding new features. This will also give you confidence and assurance when your refactoring classes, collapsing code or removing extraneous code. If written tests pass both before and after changes you should be good to go! Another testing method is to write a test after you find an issue or bug in your application. Ensuring the fix works and does not plague your application in the future.

Testing Frameworks

Like many other parts of Rails, there are default gems and libraries. As well as other optional alternatives, which at times are more used by than the defaults.

RSpec vs MiniTest

As of the latest version of Rails, the default testing framework that is included is MiniTest. Which in itself is an improvement on the original testing framework for Rails Test::Unit. Personal preference can play a huge part in choosing many aspects of a Rails application. Testing is no exception.

The largest alternative to MiniTest is RSpec. Which focuses on readability and composability of tests. It uses words such as "describes" and "it" when naming tests and test groups. For example:

describe "the test" do
  it "is a success" do
    expect(1).to eq(1)
  end
end
Enter fullscreen mode Exit fullscreen mode

Whereas MiniTest will use the asset verbiage found in other programmatic testing frameworks. Personally, I like RSpec, find it easier to reason about, and has a little bit better of a command line interface(CLI).

Capybara Feature Tests vs Integration Tests

Another noteworthy alternative in the testing work is the addition of Capybara. While you can use built in Integration tests to test workflow. It leaves desiring more for real world browser workflows. This is where Capybara comes in. Allowing you to write feature tests taking you through pages of your choosing within your application. Performing behaviors such as clicking, filling in a form or verifying that everything is behaving as expected.

Capybara plugs right into RSpec, allowing you to use the same syntax and phrasing to write tests. Here is an example right from their docs!

describe "the signin process", :type => :feature do
  before :each do
    User.make(email: 'user@example.com', password: 'password')
  end

  it "signs me in" do
    visit '/sessions/new'
    within("#session") do
      fill_in 'Email', with: 'user@example.com'
      fill_in 'Password', with: 'password'
    end
    click_button 'Sign in'
    expect(page).to have_content 'Success'
  end
end
Enter fullscreen mode Exit fullscreen mode

FactoryGirl

The last alternative I'll mention is the gem FactoryGirl. The default method of test objects within Rails is fixtures. Fixtures files are in written the YAML format. Providing a basic static mock object representation. Fixtures will get tougher as the model objects get more complex. Including their associations proliferating. FactoryGirl describes itself as an API to create test objects. Meaning, you will programmatically use the API to create mock objects for your tests. You can provide a factory definition, in Ruby, then use the API much like normal object to .build or .create. FactoryGirl also makes it super simple to include associations through the factory definitions.

Testing Types

There are three main types of tests you will find in a typical Rails application. Model(Unit) test, Controller tests, and View tests. Additionally, you can also use Feature Tests. These tests will function as an end to end walkthrough test of your application. Each type of test has it's place in the testing world and may be equally important.

Model/Unit Tests

Model specs are most likely the simplest of the test types you will deal with in Rails. With these tests you are testing the normal CRUD(Create/Read/Update/Delete) operations on the model object. Additionally, they will test validations and/or associations. Model specs will normally run super quick and be fairly self-contained

FactoryGirl.define do
  factory :user do
    name "Same Name"
    email "test@user.com"
  end
end
Enter fullscreen mode Exit fullscreen mode
class User < ApplicationRecord
  validates :email, presence: true
  validates :email, uniqueness: true
  validates :name, presence: true
  
Enter fullscreen mode Exit fullscreen mode
describe User do
  it "has a valid factory" do
    expect(FactoryGirl.build(:user).save).to be_true
  end

  it "is invalid without a name" do
    expect(FactoryGirl.build(:user, name: nil).save).to be_false
  end

  it "is invalid without a unique name" do
    user = FactoryGirl.create(:user)
    expect(FactoryGirl.build(:user, name: "Same Name")).to be_false
  end

  it "is invalid without an email" do
    expect(FactoryGirl.build(:user, email: nil)).to be_false
  end
end
Enter fullscreen mode Exit fullscreen mode

For this example, there is an already valid user factory definition that is setting some default values. This particular scenario is testing that the User model can create a user. Then validates user creation fails on a few key Rails Model validators on this User model.

Controller Tests

Controller tests will be your bread and butter when it comes to testing all your conditional logic. With tests based on request type, user type, success/failure of Models and so on. Controller specs are a little bit slower than Model specs due to their use of the request/response cycle. Though are still considerably faster that feature specs. This makes it a great place to do the bulk of your testing.

describe UsersController do
  describe "GET index" do
    it "assigns @users" do
      user = FactoryGirl.create(:user)
      get :index
      expect(assigns(:users)).to eq([user])
    end

    it "renders the index template" do
      get :index
      expect(response).to render_template("index")
    end
  end

  "GET #show" do
    it "renders the #show view" do
      get :show, {id:user.id}
      response.should render_template :show
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

In this test file, you will find that we are testing a page that would list the users. First, testing the action assigns the right output. After creating a user, and getting the :index action the controller will have an instance variable that equals an array of the one user from the test. The second test is simply testing that getting the :index action, renders the index template.

Obviously, these tests are not as complex and are abbreviated to give you a taste of how easy controller testing can be. In a real world controller file, you would be testing every action. Testing its behavior and rendering. As well as each branch of the action such as success or failure of a database transaction.

View Tests

View specs are often overlooked by developers who test all aspects of the visual representation in feature tests. Feature tests are meant to give you the high-level behavior testing. However, they definitely should not be used to test every last detail of a server rendered template. Many times you will need to test different markup based on some sort of if statement or logic, that can be done in a view spec.

FactoryGirl.define do
  factory :user do
    name "Same Name"
    email "test@user.com"
    admin false
  end
end
Enter fullscreen mode Exit fullscreen mode
<%- if @user.admin? >
  <h1>Welcome back admin</h1>
<%- else %>
  <h1>Welcome back pal</h1><
<%- end %>
Enter fullscreen mode Exit fullscreen mode
describe "rendering homepage" do
  it "displays admin message" do
    assign(:user, FactoryGirl.create(:user, admin: true))
    render :template => "home/index.html.erb"
    expect(rendered).to match /admin/
  end

  it "displays regular user message" do
    assign(:user, FactoryGirl.create(:user))
    render :template => "home/index.html.erb"
    expect(rendered).to match /pal/
  end
end
Enter fullscreen mode Exit fullscreen mode

In this example, we have a simple view template that renders a message in the markup. The noun is based on whether the user referenced in the user variable is an admin or not. Here, the view spec renders out the message with the user object created in each test(one admin, one not) and tests for the text with regular expressions.

Feature Tests

Finally, feature specs(tests) are a great way to test your entire application at once(or large parts of behavior). Using RSpec and Capybara, you will be able to write plain simple tests that will walk through your app as if you were an end user.

describe "Signing Up", :type => :feature do
  it "allows a user to sign up" do
    visit root_path
    expect(page).to have_content 'Sign Up'
    within("form#user_new") do
      fill_in 'Name', with: 'John'
      fill_in 'Email', with: 'user@example.com'
      fill_in 'Password', with: 'password'
      fill_in 'Retype Password', with: 'password'
    end
    click_button 'Sign Up'
    expect(page).to have_content 'Welcome to Sample App'<
    expect(page).to have_content 'John'
  end
end
Enter fullscreen mode Exit fullscreen mode

This test simulates a user visiting your application for the first time. Then seeing a sign-up page, filling it out and successfully accessing the application after signup.

As you can see, Feature tests provide an amazing way to test at a high level, just as a user would be interacting with your application. However, with that comes the cost of the fact feature tests will run slower than all other types of tests. For that reason, it would be smart to not test every branch or possibility in your application. Using controller or other specs to test edge cases.

Testing Tools

Additionally, there a few extra tools I would like to go over and will be included in your application's codebase. These tools will help with issues such as test cleanup. Another, being a test coverage completeness tool which includes a local web tool to see analysis file by file. Lastly, is a tool to easily mock external resources, such as third-party APIs.

Database Cleaner: Database Cleaner is a testing tool, that allows you to use different strategies to wipe the database to a clean state after every test. The importance here is that while Rails attempts to wrap their tests in transactions. Which prevent one tests database changes from affecting another's tests. Yet, when using javascript drivers, those tests are performed in another thread. Meaning you will need another means to return the database to a pristine state, using truncation. Database Cleaner, in particular, will allow you to set the "cleaning" method based on test type.

Simplecov: We will use this gem to calculate, display and investigate code coverage. This tool will use built in Ruby coverage information, along with merging the results of different tools' and test types coverage. Afterward, it will generate the information, save it to a report and automatically create an HTML site for you to visit to look through the code coverage analysis.

VCR: VCR allows you to "record" HTTP transactions and save their responses locally. This allows you to "playback" the response from a file, making it easy and fast to run tests that interact with external resources. Along with the speed increases, it allows for offline testing and not worrying about a third party change affecting your tests. However, I recommend you still perform some sort of testing on the actual external resource's response.

CI and You

The last thing to discuss in this chapter is Continuous Integration. Which means you will be continuously merging changes into a master branch, where various pipelines make work on the newly merged code. In most cases, this is a way to merge changes into one central branch. Then run the tests on an external system and using the test results to chose another action such as deploying or requesting a fix to the codebase.

There are a few different tools one could use for CI. The first that may be recommended in Jenkins. The great thing about Jenkins is that it is an open-source project and free. However, there is no hosted solution available through their site. Meaning, you will either have to build and maintain your Jenkins server or seek a service that would do so for you.

There is good news for those who do not want to maintain their own server for running continuous integration. There are quite a few services that run a CI service. Having integrations wall of the major repository services(GitHub, Bitbucket, etc). This means a simple push to your repository could trigger your test to run automatically on their service. Followed by a report with the results in any response medium you would like. Such as email, chat services, and others.

Without any affiliation, the server I usually use for my applications is CircleCI. I find their ease of use as well as their pricing(free for most of my purposes) to for my needs well. Other choices would be TravisCI and SemaphoreCI. I would recommend checking these products out and choosing the one that fits you best.

Top comments (0)