Goal

In this workshop, we’ll help you make your first steps with Test Driven Development in Rails, using RSpec. RSpec is a widely used testing framework that can be used un pure ruby and also have helpers for Rails and other web developement frameworks.

We will start with an already existing application just to get started and learn about how to write a test, the different types of tests, good practices. Then, once we added some different tests, we’ll develop a new feature using Test Driven Development.

Install rvm

\curl -sSL https://get.rvm.io | bash -s stable

RVM is a Ruby version manager. There are many others, like rbenv and asdf. It is useful when you have multiple Ruby applications on your computer, using different ruby versions. You don’t have to install RVM if you already have a version manager.

Clone one of the repos at:

git clone -b tdd-workshop https://github.com/bluciam/railsbridge-montreal-website.git
cd railsbridge-montreal-website
bundle install

Why testing?

Testing in Rails is both hard and easy. Hard because it takes as much time as coding does, but easy because there are many tools that make the most complicated things straightforward. There are so many tools though, that sometimes it is hard to choose. Neat paradox.

The next issue is what and when to test. Michael Hartl has an introduction to testing in his Ruby on Rails tutorial, which describes why testing and the kinds of tests he is likely to do. In summary,

The regression tests on bugs are also very important, and specially having tests in place before any refactoring.

RSpec testing infrastructure

Set up

When a new Rails project is created with default settings, a test directory is created, coupled to work with minitest. In this workshop we will be using RSpec, RSpec (http://rspec.info/), so we are changing the default as follows:

rails g controller StaticPages about --no-test-framework

The test files related to the StaticPages controller will not be created. To create the right files, RSpec must be installed. That is done by including its gem in the Gemfile. RSpec takes advantage of a series of helpers to run tests automatically. The gems are specified following:

group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end

group :test do
  gem 'selenium-webdriver'
  gem 'faker'
  gem 'capybara'
  gem 'launchy'
end

And then run bundle. One of the most interesting about Rails testing, is the interaction with databases and RSpec helpers makes this easy.

By default, every Rails application has three environments: development, test, and production. The database for each one of them is configured in config/database.yml. http://guides.rubyonrails.org/testing.html

That fact is simply brilliant, as the development database is NOT the same as the test database. One can create automatically hundreds of records to test for specific features in isolation. To run any testing, the databases need to be created, so run

rake db:create:all

which will create the databases which do not exist and inform you of the ones already created. The information on the databases description is taken from the file config/database.yml, which should include the information about test, development and production databases. Make sure that the three are name differently! Then run

rails generate rspec:install

which generates the following configuration files:

.rspec
spec/spec_helper.rb
spec/rails_helper.rb

All tests and helpers will reside in the spec directory. This is directory where RSpec searches for the tests to run.

NOTE: After making changes to any of the models in development, you have to migrate the changes to the test database as well by running

rake db:migrate RAILS_ENV=test

With this, the testing infrastructure is set up.

Syntax

RSpec uses mainly the words “describe” and “it” so we can express concepts like a conversation:

“Describe an order.”

“It sums the prices of its line items.”

The describe method creates an ExampleGroup. Within the block passed to describe you can declare examples using the it method. Under the hood, an example group is a class in which the block passed to describe is evaluated. The broad syntax of the test is as follows:

describe _Object_ do
  it "Descriptive message of the test" do
    _code with expectations_
  end
end

Each it line only expects one example. Best practice is to test one thing at a time to make it simple to find errors. Although the descriptive message is technically optional, omitting defeat the purpose of individual testing. Previous RSpec examples had the “should” beginning the message, however that just clutters the output. A direct verb suffices.

Actual testing

Now to writing the tests. But where to start? With how to run a test. To run a test, use the command

rspec

from the root directory of the app. If used alone, it will run all the tests found in the spec directory. You can also specify a directory or a filename including its path with respect to root. RSpec will run all tests found in that directory in the first case, or just the file specified in the second. The testing framework automatically creates directories to sort out the tests. An example spec directory of an app looks like this:

controllers/  factories/       models/         requests/       support/
helpers/      rails_helper.rb  spec_helper.rb  views/

Testing Models

Models are the building blocks of the application. They are also easier to test since their behaviour should be well defined in any application. They could be considered as the priority to test. Everyday Rails

The blog Everyday Rails considers the following to be essential model tests:

Our adaptation for this workshop are:

Factory test

The first test is to make sure that a valid record can be created safely and respecting all of the constraints. In other words, that is has a valid factory.

A factory is an object for creating other objects – formally a factory is simply an object that returns an object from some method call, which is assumed to be “new”. https://en.wikipedia.org/wiki/Factory_(object-oriented_programming)#cite_ref-1

This is probably not technically TTD, rather, it follows the database design. Therefore, the table might exist already. If the table exists, we need tools to find the table structure in the development database. There are two easy ways that I know of using the rails command.

The first is using the console

rails c # short for rails console

The command

rails c
Loading development environment (Rails 5.1.6)
irb(main):001:0> ActiveRecord::Base.connection.tables
=> ["checks", "children", "courses", "editions", "messages", "registrants", "schema_migrations", "ar_internal_metadata"]

will output an array including all of the current tables. And the command Registrant.column_names

irb(main):002:0> Registrant.column_names
=> ["id", "name", "email", "created_at", "updated_at", "bringing_laptop", "special_needs", "level", "language", "cancelled_at", "waitlisted", "course_id", "edition_id"]

will output an array including the column names of the table Registrant.

With this information and with the information in the model.rb file, the factory can be written for the model. I used the faker gem to create random names and descriptions (https://github.com/stympy/faker). Given that authors write articles, I also have a factory for articles. The factories look like this:

# spec/factories.rb
require 'faker'

FactoryBot.define do

  factory :author do
    name Faker::Name.name
    bio { Faker::Lorem.sentences.to_s }
    image { Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec',
      'support', 'images', 'image_2.jpg')) }
  end

  factory :article do
    author
    title "The life of Pepito Perez"
    summary { Faker::Lorem.sentence }
  end
end

The image field has been created with the carrierwave gem and that is the code needed in the testing environment. The file image_2.jpg must exist in the spec/support/images/ directory.

In the previous example, there is only one file including all the factories. However, this works when there are only a couple of models but for best practices, there should be one factory per model: model_name.rb in the spec/factories directory.

The test to validate the factory is written in file spec/models/article_spec.rb:

require 'rails_helper'

describe Article do
  # Validation tests
  it "has a valid factory" do
    expect(FactoryGirl.create(:article)).to be_valid
  end
end

The RSpec matcher be_valid verifies that our factory does indeed return a valid object. When the models exist prior to writting the test, it is a good practive to make the test fail by changing the code, fix the code to have the test pass. This ensures that specific parts of code are indeed being tested.

Data validations

These tests are straight-forward, validating any of the constraints needed in each field. The model of the article has the constraint that the title is mandatory, as shown below:

# app/models/article.rb
class Article < ActiveRecord::Base
  validates :title, presence: true

The following lines, create an object in the test environment, and gives it an empty title, which is not allowed:

require 'rails_helper'

describe Article do

  it "is invalid without a title" do
    expect(FactoryBot.build(:article, title: nil)).not_to be_valid
  end

end

Notice in the article_spec.rb file there are two special methods: create and build. create builds and saves the object, while build only does that. This allows the modification of attributes before saving the object. The not_to verifies that an empty title should not be allowed in a valid object.

Associations between models

The following allows RSpec testing of the association of models. The gem shoulda found in github allows just that. First add it the Gemfile.

group :test do
  gem 'shoulda-matchers'
end

Run bundle. Then add to the spec/models/article_spec.rb, the following test:

require 'rails_helper'

describe Article do

  # Associations test
  it { is_expected.to belong_to(:author) }
end

Testing Controllers

(Shamelessly based on Relish docs.)

Controller specs are marked by :type => :controller or if you have set config.infer_spec_type_from_file_location! by placing them in spec/controllers. Regardless of the configuration, it is good practice to place these tests in that directory.

A controller spec is an RSpec wrapper for a Rails functional test (ActionController::TestCase::Behavior). It allows you to simulate a single http request in each example, and then specify expected outcomes such as:

To specify outcomes, you can use:

There is a lot of discussions around the good practices of testing controllers. As they do provide some kind of value, they look pretty much like integration tests. And having a good integration test suite makes the controller tests obsolete. We will look at them anyways because they are a pretty good starting point anyways.

Controller test example

If we create a scaffold through the Rails generators, we’ll see that a test file with some tests already:

rails g scaffold article
      [...]
      invoke  scaffold_controller
      create    app/controllers/articles_controller.rb
      [...]
      invoke    rspec
      create      spec/controllers/articles_controller_spec.rb
      create      spec/views/articles/edit.html.erb_spec.rb
      create      spec/views/articles/index.html.erb_spec.rb
      create      spec/views/articles/new.html.erb_spec.rb
      create      spec/views/articles/show.html.erb_spec.rb
      create      spec/routing/articles_routing_spec.rb
      [...]
      
require 'rails_helper'

RSpec.describe ArticlesController, type: :controller do

  # This should return the minimal set of attributes required to create a valid
  # Article. As you add validations to Article, be sure to
  # adjust the attributes here as well.
  let(:valid_attributes) {
    skip("Add a hash of attributes valid for your model")
  }

  let(:invalid_attributes) {
    skip("Add a hash of attributes invalid for your model")
  }

  # This should return the minimal set of values that should be in the session
  # in order to pass any filters (e.g. authentication) defined in
  # ArticlesController. Be sure to keep this updated too.
  let(:valid_session) { {} }

  describe "GET #index" do
    it "returns a success response" do
      article = Article.create! valid_attributes
      get :index, params: {}, session: valid_session
      expect(response).to be_success
    end
  end

  describe "GET #show" do
    it "returns a success response" do
      article = Article.create! valid_attributes
      get :show, params: {id: article.to_param}, session: valid_session
      expect(response).to be_success
    end
  end

  describe "GET #new" do
    it "returns a success response" do
      get :new, params: {}, session: valid_session
      expect(response).to be_success
    end
  end

  describe "GET #edit" do
    it "returns a success response" do
      article = Article.create! valid_attributes
      get :edit, params: {id: article.to_param}, session: valid_session
      expect(response).to be_success
    end
  end

  describe "POST #create" do
    context "with valid params" do
      it "creates a new Article" do
        expect {
          post :create, params: {article: valid_attributes}, session: valid_session
        }.to change(Article, :count).by(1)
      end

      it "redirects to the created article" do
        post :create, params: {article: valid_attributes}, session: valid_session
        expect(response).to redirect_to(Article.last)
      end
    end

    context "with invalid params" do
      it "returns a success response (i.e. to display the 'new' template)" do
        post :create, params: {article: invalid_attributes}, session: valid_session
        expect(response).to be_success
      end
    end
  end

  describe "PUT #update" do
    context "with valid params" do
      let(:new_attributes) {
        skip("Add a hash of attributes valid for your model")
      }

      it "updates the requested article" do
        article = Article.create! valid_attributes
        put :update, params: {id: article.to_param, article: new_attributes}, session: valid_session
        article.reload
        skip("Add assertions for updated state")
      end

      it "redirects to the article" do
        article = Article.create! valid_attributes
        put :update, params: {id: article.to_param, article: valid_attributes}, session: valid_session
        expect(response).to redirect_to(article)
      end
    end

    context "with invalid params" do
      it "returns a success response (i.e. to display the 'edit' template)" do
        article = Article.create! valid_attributes
        put :update, params: {id: article.to_param, article: invalid_attributes}, session: valid_session
        expect(response).to be_success
      end
    end
  end

  describe "DELETE #destroy" do
    it "destroys the requested article" do
      article = Article.create! valid_attributes
      expect {
        delete :destroy, params: {id: article.to_param}, session: valid_session
      }.to change(Article, :count).by(-1)
    end

    it "redirects to the articles list" do
      article = Article.create! valid_attributes
      delete :destroy, params: {id: article.to_param}, session: valid_session
      expect(response).to redirect_to(articles_url)
    end
  end

end

This is a structure Rails developers recognise easily. Those tests will not pass though because we need params to pass in the let calls. It can easily be done with FactoryBot using the attributes_for method. It will return a hash containing the attributes a factory can define.