Pratical guide to Test Driven Development in Rails

by Ícaro Falcão


Tdd


Test Driven Development (TDD) is a very important tool for any developer nowadays. The idea is fairly simple: build our tests first, see the test failing, build the feature to pass these tests, and refactor after all tests have passed. TDD has many upsides, like:

  • Helps the developer to think about the best design to implement the feature.
  • Offers feedback to every condition established by the developer on the tests, you can actually follow a step-by-step guide provided by the Test-Suite to understand what needs to be done to pass the test.
  • Could actually save time: when compared to manual testing a new complex feature, for example.

How to use TDD in your Rails application

For this article, I'll be creating a model following the Test Driven Development in a Ruby on Rails application using the gems RSpec, Database Cleaner, Factory Bot (previously called Factory Girl), and Shoulda Matchers.

RSpec

It extends Rails' built-in testing framework to support rspec examples for requests, controllers, models, views, helpers, mailers and routing.

Factory Bot

factory_bot is a fixtures replacement with a straightforward definition syntax, support for multiple build strategies (saved instances, unsaved instances, attribute hashes, and stubbed objects), and support for multiple factories for the same class (user, admin_user, and so on), including factory inheritance. Previously called *Factory Girl*

Database Cleaner

Database Cleaner is a set of strategies for cleaning your database in Ruby. The original use case was to ensure a clean state during tests. Each strategy is a small amount of code but is code that is usually needed in any ruby app that is testing with a database.

Shoulda Matchers

Shoulda Matchers provides RSpec- and Minitest-compatible one-liners that test common Rails functionality. These tests would otherwise be much longer, more complex, and error-prone.


Let's start by putting these gems in our Gemfile on Development and Test group and running bundle install

group :development, :test do
  gem 'rspec-rails', '~> 3.6'
  gem 'factory_girl_rails'
end

group :test do
  gem 'database_cleaner'
  gem 'shoulda-matchers', '~> 3.1'
end

After we run bundle install we can install the rspec, so run in your terminal the following line:

$ rails generate rspec:install

This will create a folder called 'spec' in the root of our project where our tests will be contained. This new folder has 2 files: rails_helper.rb and spec_helper.rb where we can change some configs from rpsec. We will make some changes in our rails_helper.rb file to configure the Database Cleaner on our tests by adding the following default configuration for RSpec that is recommended on the gem's documentation.

# Rest of the start of the filer above here...
require 'database_cleaner' # Must require the module

# This block is already created on the rails_helper file
# Must add database cleaner config inside it
RSpec.configure do |config| 

  config.before(:suite) do
     # Run every test as a database transaction
     DatabaseCleaner.strategy = :transaction
     # Clean the database before the test
     DatabaseCleaner.clean_with(:truncation)
   end

# Run transaction on each test
  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end

# Rest of the file under here ...


The Factory Bot will automatically replace the fixtures, so when we generate a model, for example, it will automatically generate a Factory Bot file instead of a fixture. The Factory Bot objects are way better than fixtures because they are created at the time we generate the entity with the default attributes making it way easier to modify.
To run the factory bot methods without having to write FactoryBot::method_name, we will add the following line to our rails_helper.rb file:

# Factory Bot configuration
# must be inside the RSpec.configure block
config.include FactoryGirl::Syntax::Methods

Now we are able to call the method only by their name.
The Shoulda Matchers gem will provide one line methods to test Rails core functionalities that would otherwise be big and repetitive methods. Let's configure it inside of rails_helper.rb

# This should be outside of the RSpec.configure block
Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

Creating the model Task

Let's generate our model and start testing it. I will assume you already have a User model or is about to create one so we can test the relations of our model as well. Our Task model will have the following fields:

  • title:string
  • description:text
  • deadline:datetime
  • done:boolean
  • user:references

To generate this model, run the following on your terminal:

rails g model Task title:string description:text deadline:datetime done:boolean admin:references
rails db:migrate
# Use rake db:migrate in case you are not using Rails 5+

This will generate a file task_spec.rb in your 'spec/models/' folder and a file tasks.rb inside your 'spec/factories/' folder. The file inside the factories folder will have every attribute already created for you, which saves a lot of time.
Now go ahead and open the task_spec file inside the 'spec/models' folder so we can start adding our tests. The first thing we should do is generate a local object to our tests using Factory Bot, to do that we have a method called let that allows creating variables to use in our tests. The factory bot provides us a couple of methods to create objects, we'll use the build method to generate the object without saving it to the database, but if you want to save it then use the create method.

require 'rails_helper'

RSpec.describe Task, type: :model do
  # Delete the pending line that comes with the file
  let(:task) { build :task }
end

Let's lay down the validation we need to set in our model so we can start creating the tests

  • When a task is new the done field must be false
  • Must validate the presence of the title and user
  • The task must have a reference to user
  • Must respond to all the created fields

Getting back to our file, let's start implementing the tests. First one is to set the done field to false when a new task is created. To do that, we can use the task object we created from Factory Bot.
First thing to be done when creating a test that belongs to a specific event is defining this context before the test. For example, the done field must be false only when the task is new. Let's see how to create this context and how to implement the test:

require 'rails_helper'

RSpec.describe Task, type: :model do
  let(:task) { build :task }

  context 'When is new' do
    # booleans fields have these be_<column_name> methods
    # to test if they are true
    it { expect(task).not_to be_done }
  end
end

Observe how self-explanatory the test is. Reading good tests must be like reading the software documentation.
Now we need to validate the presence of title and user_id. To do that, we have a method called validate_presence_of in Shoulda Matchers which is almost equal to the one we need to implement in the model to pass this test (Spoiler alert!).
Another thing we can realize from this next test is that we don't need a created object to test it (That sounded redundant), we only need to check if the validation is been applied to this model. To tests like this, RSpec provides a naked model object by calling is_expected which will make it even more readable and easier to understand. Let's check how to implement this test:

it { is_expected.to validate_presence_of :title }
it { is_expected.to validate_presence_of :user_id }

Simple, and very easy to read and understand! Let's take on the next test, which is validating the relation between Task and User. Since we are testing the Task model, let's only worry about this side of the relation testing for now (after this is finished you should implement the other side of this test in the spec/models/user_spec.rb).
After seeing the almost equal method to test the presence's validation, you should not be very surprised to see how the belongs_to is validated using Shoulda Matchers' methods.

it { is_expected.to belong_to :user }

Let's finish the first part of our TDD journey by testing if every field is present on the model. This is a bit of a controversy test because many people think this is not necessary and is very repetitive, but as I said above:

Reading good tests must be like reading the software documentation.

After we implement the respond_to test, a person that reads it, later on, can identify every field present on the model. Now you could ask me if it isn't the schema.rb file purpose and I will answer Yes, but the test file is a self-contained documentation of every feature this model possesses.
With all of that being said, it's your choice whether to implement this test or not. In case you are going through with this, to test if the model contains a column, Shoulda Matchers provides a method called respond_to:

it { is_expected.to respond_to :title }
it { is_expected.to respond_to :description }
it { is_expected.to respond_to :deadline }
it { is_expected.to respond_to :done }
it { is_expected.to respond_to :user_id }

All our tests have been implemented, let's see how our spec/models/task_spec.rb should look like now:

require 'rails_helper'

RSpec.describe Task, type: :model do
  let(:task) { build :task }

  context 'When is new' do
    it { expect(task).not_to be_done }
  end

  it { is_expected.to belong_to :user }

  it { is_expected.to validate_presence_of :title }
  it { is_expected.to validate_presence_of :user_id }

  it { is_expected.to respond_to :title }
  it { is_expected.to respond_to :description }
  it { is_expected.to respond_to :deadline }
  it { is_expected.to respond_to :done }
  it { is_expected.to respond_to :user_id }
end

Passing the Tests

Now comes the best part, we'll receive feedbacks from RSpec on what needs to be implemented in order to achieve the features our tests expect. Let's run our tests and get a bit sad by running on your terminal:

rspec

Yep, that's it. Now we'll see the 2 errors on the tests concerning presence validation. The error message is very explanatory and helps a lot on finding what we forgot and what needs to be done, this is one of the most important features on developing using TDD and RSpec helps a lot in this area.
Matching this requirement in Rails is fairly simple, let's open our app/models/task.rb file and add the following method to validate the presence of the required fields:

validates_presence_of :title, :user_id

Another thing worth mentioning is the presence of the line belongs_to :user in our model which passes the test we previously wrote (this line is created alongside the model when we define a column with user:references when generating the model). A good practice is to remove the conditions to pass a test and verify if we didn't write a fake test.
A fake test is basically a test that doesn't test anything (Redundance allowed in this case :) ). If we remove this line from our Task model file and run the test again we'll see another 2 failing tests, one regarding the belong_to User and another when trying to create the task object on Factory Bot.
The done field is also not ensured to be false when creating the Task object, to do that let's open the migration regarding the creation of the task model and edit the done field line:

class CreateTasks < ActiveRecord::Migration[5.1]
  def change
    create_table :tasks do |t|
      t.string :title
      t.text :description
      t.datetime :deadline
      t.boolean :done, default: false
      t.references :user, foreign_key: true

      t.timestamps
    end
  end
end

After running our tests with rspec we'll see that every example passed.

Refactoring

In this article example, we don't have this opportunity to refactor anything but in case you are writing functions and creating files and doing architecture design, this is the part where you make your algorithms better regarding performance and best practices.


Leave a Comment

(Other users will not see it)
* Required fields