Category Archives: Ruby on Rails

How to clear up obscure Rails tests using Page Objects

The challenge of keeping test code clean

The hardest part of a programmer’s job isn’t usually figuring out super hard technical problems. The biggest challenge for most developers, in my experience, is to write code that can stand up over time without collapsing under the weight of its own complexity.

Just as it’s challenging to keep a clean and understandable codebase, it’s also challenging to keep a clean and understandable test suite, and for the same exact reasons.

If I look at a test file for the first time and I’m immediately able to grasp what the test is doing and why it’s doing it, then I have a clear test. The test has a high signal-to-noise ratio. That’s good.

The opposite scenario is when the test is full of noise. Perhaps the test contains so many low-level details that the high-level meaning of the test is obscured. The term for this condition, this “test smell”, is Obscure Test. That’s bad. If the test code is obscure when it could have been clear, that creates extra head-scratching time.

One tool that can be used to help make Obscure Tests more understandable is the concept of a Page Object.

What a Page Object is and when it’s helpful

As a preface: Page Objects are only relevant when dealing with integration tests—that is, tests that interact with the browser. Page Objects don’t come into the picture for model tests or anything else that doesn’t interact with a browser.

A Page Object is an abstraction of a component of a web page. For example, I might create a Page Object that represents the sign-in form for a web application. (In fact, virtually all my Page Objects represent forms, since that where I find them to be most helpful.)

The idea is that instead of using low-level, “computerey” commands to interact with a part of a page, a Page Object allows me to use higher-level language that’s more easily understandable by humans. Just as Capybara is an abstraction on top of Selenium that’s clearer for a programmer to read and write than using Selenium directly, Page Objects can be an abstraction layer on top of Capybara commands that’s clearer for a programmer to read and write than using Capybara directly (at least that’s arguably the case, and only in certain situations, and when done intelligently).

Last note on what a Page Object is and isn’t: I personally started with the misconception that a Page Object is an abstraction of a page. According to Martin Fowler, that’s not the idea. A Page Object is an abstraction of a certain part of a page. There’s of course no law saying you couldn’t create an abstraction that represents a whole page instead of a component on a page, but having done it both ways, I’ve found it more useful to create Page Objects as abstractions of components rather than of entire pages. I find that my Page Objects come together more neatly with the rest of my test code when that’s the case.

A Page Object example

I’m going to show you an example of a Page Object by showing you the following:

  1. A somewhat obscure test
  2. A second, cleaner version of the same test, using a Page Object
  3. The Page Object code that enables the second version of the test

Here’s the obscure test example. What exactly it does is not particularly important. Just observe how hard the test code is to understand.

The obscure test

RSpec.describe "Grocery list", type: :system do
  let!(:banana) { create(:grocery_item) }

  describe "deleting a grocery item" do
    it "removes the item" do
      within ".grocery-item-#{banana.id}" do
        click_on "Delete"
      end

      expect(page).not_to have_content(banana.name)
    end
  end
end

This test is okay but it’s a little bit noisy. We shouldn’t have to care about the DOM details of how the Delete button for this grocery item is located.

Let’s see if we can improve this code with the help of a Page Object.

RSpec.describe "Grocery list", type: :system do
  let!(:banana) { create(:grocery_item) }
  let!(:grocery_list) { PageObjects::GroceryList.new }

  describe "deleting a grocery item" do
    it "removes the item" do
      grocery_list.click_delete(banana)
      expect(grocery_list).not_to have_item(banana)
    end
  end
end

Here’s the Page Object code that made this cleanup possible:

module PageObjects
  class GroceryList
    include Capybara::DSL

    def click_delete(item)
      within ".grocery-item-#{item.id}" do
        click_on "Delete"
      end
    end

    def has_item?(item)
      page.has_content?(item.name)
    end
  end
end

I find that Page Objects only start to become valuable beyond a certain threshold of complexity. I tend to see how far I can get with a test before I consider bringing a Page Object into the picture. It might be several weeks or months between the time I first write a test and the time I modify it to use a Page Object. But in the cases where a Page Object is useful, I find them to be a huge help in adding clarity to my tests.

Refactoring to POROs (using tests)

For a good chunk of my Rails career I was under the impression that in Rails projects, we have one model per database table. If all I have in my application are three tables, `users`, `products`, and `orders`, then I’ll have exactly three corresponding ActiveRecord classes, `User`, `Product`, and `Order`.

Then I read a post by Steve Klabnik (which I can’t find now) pointing out that the classes in `app/models` don’t have to inherit from ActiveRecord. It was a real aha moment for me and a turning point in the way I write code.

These ActiveRecord-less classes are often called Plain old Ruby Objects or POROs.

Why POROs are useful

Small methods are easier to understand than large methods. Small classes are easier to understand than large classes. Code that’s easy to understand tends to me less expensive to maintain and more enjoyable to maintain.

Rails projects that only use the classes that inherit from ActiveRecord tend to eventually end up with ActiveRecord classes that are way too big and do way too much unrelated stuff. If a project contains a `Product` class, the `Product` class becomes sort of a dumping ground for anything that could be tangentially related to a product. Inside of the huge `Product` class there might be three or four abstractions “hiding” in the code, yearning to be separated into their own neat and tidy classes where they don’t have to live scrambled together with unrelated code.

I’ve also found that when I have a method that’s bigger than I’d like but just seems irreducible, I can pull out that method’s code into a new PORO and suddenly I see how my ugly method can be morphed into a neat and tidy class.

All this is rather abstract so let’s take a look at a concrete example.

An example of refactoring to a PORO

Here’s a piece of code written by a relatively unskilled programmer—me from eight years ago.

Don’t try to understand this method right now. First just observe its size and shape.

class Appointment < ActiveRecord::Base
  def self.save_from_params(params)
    a = params[:id] ? self.find(params[:id]) : self.new

    a.attributes = {
      time_block_type: TimeBlockType.find_by_code(params[:appointment][:time_block_type_code]),
      notes:           params[:appointment][:notes],
      stylist_id:      params[:appointment][:stylist_id],
      is_cancelled:    params[:appointment][:is_cancelled],
      tip:             params[:appointment][:tip],
    }

    if a.time_block_type_code == "APPOINTMENT"
      a.client = self.build_client(params, a.stylist.salon)
    else
      a.client = Client.no_client
      a.length = Appointment.determine_length(
        start_time: params[:appointment][:start_time_time],
        end_time:   params[:appointment][:end_time],
      )
    end

    a.set_time(params[:appointment][:start_time_time], params[:appointment][:start_time_ymd])
    a.set_repeat_logic(params)
    a.save
    a.set_payments_from_json_string(params[:serialized_payments])
    a.set_services_and_products_from_json_string(params[:serialized_products_and_services])
    a.record_transactions

    if !a.new_record?
      a.reload.generate_recurrence_hash_if_needed
      a.reload.save_future
    end
    a
  end
end 

At the time I wrote this method I knew that it was much longer than I’d like but I could figure out how to make it smaller. Now that I’m older and wiser I know that it can be refactored into a new class. I’m going to make use of the factory pattern and call this new class `AppointmentFactory`.

I need to be very careful when creating this new class to ensure that it works precisely the same as the existing method. I’m going to use tests to help reduce the likelihood that I screw anything up. I’m also going to go in very small steps.

Writing the first line of my PORO

The first small step I’m going to take is to move the first line of `save_from_params` into a new `AppointmentFactory` class.

class AppointmentFactory
  def self.create(params)
    params[:id] ? Appointment.find(params[:id]) : Appointment.new
  end
end

The original method will be changed, very slightly, to this:

class Appointment < ActiveRecord::Base
  def self.save_from_params(params)
    a = AppointmentFactory.create(params)

    a.attributes = {
      time_block_type: TimeBlockType.find_by_code(params[:appointment][:time_block_type_code]),
      notes:           params[:appointment][:notes],
      stylist_id:      params[:appointment][:stylist_id],
      is_cancelled:    params[:appointment][:is_cancelled],
      tip:             params[:appointment][:tip],
    }

    if a.time_block_type_code == "APPOINTMENT"
      a.client = self.build_client(params, a.stylist.salon)
    else
      a.client = Client.no_client
      a.length = Appointment.determine_length(
        start_time: params[:appointment][:start_time_time],
        end_time:   params[:appointment][:end_time],
      )
    end

    a.set_time(params[:appointment][:start_time_time], params[:appointment][:start_time_ymd])
    a.set_repeat_logic(params)
    a.save
    a.set_payments_from_json_string(params[:serialized_payments])
    a.set_services_and_products_from_json_string(params[:serialized_products_and_services])
    a.record_transactions

    if !a.new_record?
      a.reload.generate_recurrence_hash_if_needed
      a.reload.save_future
    end
    a
  end
end 

Writing tests for my PORO

Now I’m going to write the first tests for my new `AppointmentFactory` class. I can see based on the code that I’ll have to test two scenarios: 1) when an id is present and 2) when an id is not present.

I’ll start with just the shell of the test.

require 'spec_helper' # Why not request 'rails_helper'? Because this is an old project

RSpec.describe AppointmentFactory do
  describe '#create' do
    context 'when id is present' do
      it 'returns the appointment with that id' do
      end
    end

    context 'when id is not present' do
      it 'returns a new appointment instance' do
      end
    end
  end
end

Why do I write just the shell of the test first? The reason is to reduce the amount of stuff I have to juggle in my head. If I get the test cases I want to write out of my head and onto the screen, I’m free to stop thinking about that part of my task.

The first test case I’ll fill in is the scenario where I’m passing the id of an existing appointment.

Test for existing appointment

require 'spec_helper'

RSpec.describe AppointmentFactory do
  describe '#create' do
    context 'when id is present' do
      let(:existing_appointment) { create(:appointment) }
      let(:appointment) { AppointmentFactory.create(id: existing_appointment.id) }

      it 'returns the appointment with that id' do
        expect(appointment.id).to eq(existing_appointment.id)
      end
    end

    context 'when id is not present' do
      it 'returns a new appointment instance' do
      end
    end
  end
end

This test passes as expected. I’m skeptical, though. I never trust a test that I only see pass and never see fail. How can I be sure that the test is testing what I think it’s testing? How can I be sure I didn’t just make a mistake in the way I wrote the test?

In order to see the test fail, I’ll comment out the correct line and add something that won’t satisfy the test.

class AppointmentFactory
  def self.create(params)
    #params[:id] ? Appointment.find(params[:id]) : Appointment.new
    Appointment.new
  end
end

As I expect, the test now fails.

Failures:                              

  1) AppointmentFactory#create when id is present returns the appointment with that id                                                                       
     Failure/Error: expect(appointment.id).to eq(existing_appointment.id)     
                                       
       expected: 1                     
            got: nil                   
                                       
       (compared using ==)             
     # ./spec/models/appointment_factory_spec.rb:10:in `block (4 levels) in <top (required)>'

Now that I’m comfortable with that test case I’ll move onto the case where I’m not passing the id of an existing appointment.

Test for new appointment instance

require 'spec_helper'

RSpec.describe AppointmentFactory do
  describe '#create' do
    context 'when id is present' do
      let(:existing_appointment) { create(:appointment) }
      let(:appointment) { AppointmentFactory.create(id: existing_appointment.id) }

      it 'returns the appointment with that id' do
        expect(appointment.id).to eq(existing_appointment.id)
      end
    end

    context 'when id is not present' do
      let(:appointment) { AppointmentFactory.create }

      it 'returns a new appointment instance' do
        expect(appointment.id).to be_nil
      end
    end
  end
end

This actually doesn’t pass.

Failures:

  1) AppointmentFactory#create when id is not present returns a new appointment instance
     Failure/Error: let(:appointment) { AppointmentFactory.create }
     ArgumentError:
       wrong number of arguments (0 for 1)
     # ./app/models/appointment_factory.rb:2:in `create'
     # ./spec/models/appointment_factory_spec.rb:15:in `block (4 levels) in <top (required)>'
     # ./spec/models/appointment_factory_spec.rb:18:in `block (4 levels) in <top (required)>'

My create method is expecting an argument but I’m not passing it one. Rather than change my code to pass an empty hash, I’ll change the signature of the create method to default to an empty hash if no argument is provided.

class AppointmentFactory
  def self.create(params = {})
    params[:id] ? Appointment.find(params[:id]) : Appointment.new
  end
end

Fixing a misnomer

My test now passes. And now that I step back and assess what I’ve done, I realize that create is probably not the most sensical name for this method. Maybe something like find_or_create would be more fitting.

class AppointmentFactory
  def self.find_or_create(attributes = {})
    attributes[:id] ? Appointment.find(attributes[:id]) : Appointment.new
  end
end

I’ll of course have to change the naming in the test file as well.

require 'spec_helper'

RSpec.describe AppointmentFactory do
  describe '#find_or_create' do
    context 'when id is present' do
      let(:existing_appointment) { create(:appointment) }
      let(:appointment) { AppointmentFactory.find_or_create(id: existing_appointment.id) }

      it 'returns the appointment with that id' do
        expect(appointment.id).to eq(existing_appointment.id)
      end
    end

    context 'when id is not present' do
      let(:appointment) { AppointmentFactory.find_or_create }

      it 'returns a new appointment instance' do
        expect(appointment.id).to be_nil
      end
    end
  end
end

The very slightly refactored version of my original method

As a reminder, my original save_from_params method looks like this:

class Appointment < ActiveRecord::Base
  def self.save_from_params(params)
    a = AppointmentFactory.find_or_create(params)

    a.attributes = {
      time_block_type: TimeBlockType.find_by_code(params[:appointment][:time_block_type_code]),
      notes:           params[:appointment][:notes],
      stylist_id:      params[:appointment][:stylist_id],
      is_cancelled:    params[:appointment][:is_cancelled],
      tip:             params[:appointment][:tip],
    }

    if a.time_block_type_code == "APPOINTMENT"
      a.client = self.build_client(params, a.stylist.salon)
    else
      a.client = Client.no_client
      a.length = Appointment.determine_length(
        start_time: params[:appointment][:start_time_time],
        end_time:   params[:appointment][:end_time],
      )
    end

    a.set_time(params[:appointment][:start_time_time], params[:appointment][:start_time_ymd])
    a.set_repeat_logic(params)
    a.save
    a.set_payments_from_json_string(params[:serialized_payments])
    a.set_services_and_products_from_json_string(params[:serialized_products_and_services])
    a.record_transactions

    if !a.new_record?
      a.reload.generate_recurrence_hash_if_needed
      a.reload.save_future
    end
    a
  end
end

Tackling the second chunk of code, attribute setting

I want to focus now on moving the chunk starting with a.attributes = out of save_from_params and into AppointmentFactory. I’ll start with a test for just the first of those attributes, time_block_type.

describe 'attributes' do
  let!(:time_block_type) { create(:time_block_type, code: 'APPOINTMENT') }

  let(:appointment) do
    AppointmentFactory.find_or_create(
      appointment: { time_block_type_code: 'APPOINTMENT' }
    )
  end

  it 'sets a time block type' do
    expect(appointment.time_block_type).to eq(time_block_type)
  end
end

I run the test and watch it fail, then add the code to make the test pass.

class AppointmentFactory
  def self.find_or_create(attributes = {})
    appointment = attributes[:id] ? Appointment.find(attributes[:id]) : Appointment.new

    appointment.attributes = {
      time_block_type: TimeBlockType.find_by_code(attributes[:appointment][:time_block_type_code])
    }

    appointment
  end
end

Unfortunately it seems that one of my “old” tests is now failing.

Failures:
                      
  1) AppointmentFactory#find_or_create when id is present returns the appointment with that id
     Failure/Error: let(:appointment) { AppointmentFactory.find_or_create(id: existing_appointment.id) }
     NoMethodError:       
       undefined method `[]' for nil:NilClass
     # ./app/models/appointment_factory.rb:6:in `find_or_create'
     # ./spec/models/appointment_factory_spec.rb:7:in `block (4 levels) in <top (required)>'
     # ./spec/models/appointment_factory_spec.rb:10:in `block (4 levels) in <top (required)>'

Why is this test failing? The problem (which I discovered after much head-scratching) is that if I only pass in something for attributes[:id] and nothing for attributes[:appointment], then the line that tries to access attributes[:appointment][:time_block_type_code] says “You’re trying invoke the method [] on attributes[:appointment], but attributes[:appointment] is nil, and NilClass doesn’t have a method called [].”

Fair enough. The simplest thing I can think of to get the test passing is just to add a guard clause that returns unless attributes[:appointment] actually has something in it.

class AppointmentFactory
  def self.find_or_create(attributes = {})
    appointment = attributes[:id] ? Appointment.find(attributes[:id]) : Appointment.new
    return appointment unless attributes[:appointment].present?

    appointment.attributes = {
      time_block_type: TimeBlockType.find_by_code(attributes[:appointment][:time_block_type_code])
    }

    appointment
  end
end

Testing the rest of the attributes

With that out of the way I’m ready to turn my attention to the rest of the attributes. I’m not feeling a particular appetite to write an individual test for each attribute, and I don’t feel like separate tests would be much more valuable or clear than one single test case with several assertions. (In fact, I think one single test case with several assertions would actually be more clear than if they were separate.)

describe 'attributes' do
  let!(:time_block_type) { create(:time_block_type, code: 'APPOINTMENT') }
  let!(:stylist) { create(:stylist) }

  let(:appointment) do
    AppointmentFactory.find_or_create(
      appointment: {
        time_block_type_code: 'APPOINTMENT',
        notes: 'always late',
        stylist_id: stylist.id,
        is_cancelled: 'true',
        tip: '100'
      }
    )
  end

  it 'sets the proper attributes' do
    expect(appointment.time_block_type).to eq(time_block_type)
    expect(appointment.notes).to eq('always late')
    expect(appointment.stylist).to eq(stylist)
    expect(appointment.is_cancelled).to be true
    expect(appointment.tip).to eq(100)
  end
end

After running that test and watching it fail I’m ready to pull in the code that sets these attributes.

class AppointmentFactory
  def self.find_or_create(attributes = {})
    appointment = attributes[:id] ? Appointment.find(attributes[:id]) : Appointment.new
    return appointment unless attributes[:appointment].present?

    appointment.attributes = {
      time_block_type: TimeBlockType.find_by_code(attributes[:appointment][:time_block_type_code]),
      notes:           attributes[:appointment][:notes],
      stylist_id:      attributes[:appointment][:stylist_id],
      is_cancelled:    attributes[:appointment][:is_cancelled],
      tip:             attributes[:appointment][:tip]
    }

    appointment
  end
end

Now my test passes.

Refactoring my crappy AppointmentFactory code

So far I’ve just been focusing on getting my tests to pass as quickly and easily as possible with little to no regard to code quality. (I picked up this habit from Kent Beck’s Test Driven Development: By Example.)

Now that I have a non-trivial amount of code in my class, I feel ready to do some refactoring for understandability. I can feel confident that correct functionality will be preserved throughout my refactoring thanks to the safety net of my little test suite.

class AppointmentFactory
  def self.find_or_create(attributes = {})
    Appointment.find_or_initialize_by(id: attributes[:id]).tap do |appointment|
      appointment.attributes = filtered_attributes(attributes[:appointment])
    end
  end

  def self.filtered_attributes(appointment_attributes)
    return {} unless appointment_attributes.present?

    {
      time_block_type: TimeBlockType.find_by_code(appointment_attributes[:time_block_type_code]),
      notes:           appointment_attributes[:notes],
      stylist_id:      appointment_attributes[:stylist_id],
      is_cancelled:    appointment_attributes[:is_cancelled],
      tip:             appointment_attributes[:tip]
    }
  end
end

The slightly more refactored version of my original method

This is what the save_from_params method now looks like. Having shaved off 6 lines by moving some code into a PORO, this method is now 24 lines instead of 32. The job is of course far from done but it’s a good start.

class Appointment < ActiveRecord::Base
  def self.save_from_params(params)
    a = AppointmentFactory.find_or_create(params)

    if a.time_block_type_code == "APPOINTMENT"
      a.client = self.build_client(params, a.stylist.salon)
    else
      a.client = Client.no_client
      a.length = Appointment.determine_length(
        start_time: params[:appointment][:start_time_time],
        end_time:   params[:appointment][:end_time],
      )
    end

    a.set_time(params[:appointment][:start_time_time], params[:appointment][:start_time_ymd])
    a.set_repeat_logic(params)
    a.save
    a.set_payments_from_json_string(params[:serialized_payments])
    a.set_services_and_products_from_json_string(params[:serialized_products_and_services])
    a.record_transactions

    if !a.new_record?
      a.reload.generate_recurrence_hash_if_needed
      a.reload.save_future
    end
    a
  end
end

I could of course continue the illustration until the save_from_params method is down to a one-liner but I think you can see where I’m going, and hopefully this much is enough to convey my methodology.

My “refactor to PORO” methodology, boiled down

Here are the rough steps I’ve used in this post to improve the understandability of my code and put it under test coverage. I use this methodology all the time in production projects, whether it be a greenfield project or legacy project.

  1. Try to find a “missing abstraction” in the method whose bulk you want to reduce, and create a new PORO expressing that abstraction
  2. Identify a line (or group of lines) from the original function you want to move into its own class
  3. Write a failing test for that functionality
  4. Cut and paste the original line into the new PORO to make the test pass (making small adjustments if necessary)
  5. Repeat from step 2 until the PORO code gets too big and messy
  6. Refactor the PORO code until you’re satisfied with its cleanliness
  7. Repeat from step 2
  8. Rejoice

The difference between integration tests and controller tests in Rails

I recently came across a Reddit post asking about the difference between integration tests and controller tests. Some of the comments were interesting:

“I always write controller tests. I only write integration tests if there’s some JavaScript interacting with the controller or if the page is really fucking valuable.”

“In this case I would say it depends if you are building a full rails app (front-end and back-end) or only and API (back-end). For the first I would say an integration test should go through the front-end (which eventually calls the controllers). If you are doing an API, integration and controller tests would be the same.”

“Controller tests attempt to test the controller in isolation, and integration tests mimic a browser clicking through the app, i.e. they touch the entire stack. Honestly I only write integration tests. Controller tests are basically exactly the same thing but worse, and they are harder to write. “

“As far as I understand for the direction of Rails, controller tests will be going away and you’ll need to use integration tests.”

“From my experience integration tests are always harder to maintain and are much more fragile. Not trying to neglect it’s value but it comes with a price. I wonder how will people going to test api only rails apps if controller tests are gone?”

Which of these things are accurate and which are BS? I’ll do my best here to clarify. I’ll also add my own explanation of the difference between integration tests and controller tests.

My explanation of integration tests vs. controller tests

Before I even share my explanation I need to provide some context. There are two realities that make questions like “What’s the difference between integration tests and controller tests?” hard to answer.

Problem: terminology

First, there’s no consensus in the testing world on terminology. What one person calls an integration test might be what another person would call an end-to-end test or an acceptance test. No one can say whether any particular definition of a term is right or wrong because there’s no agreed-upon standard.

Problem: framework differences

Second, in what context are we talking about integration tests vs. controller tests? RSpec? MiniTest? Something else? The concepts of integration tests and controller tests map slightly differently onto each framework. Here’s how I’d put it:

General Testing Concept Relevant MiniTest Concept Relevant RSpec Concept
Integration Test Integration Test Feature Spec
End-to-End Test System Test Feature Spec
Controller Test Functional Test Request Spec (and, previously, Controller Spec)

Yikes. In order to talk about the difference between integration tests and controller tests I needed to involve no fewer than eight different terms: integration test, end-to-end test, system test, feature spec, controller test, functional test, request spec and controller spec.

I could share a treatment of integration and controller tests that’s very precise and technically correct but first let me try, for clarity’s sake, to share a useful approximation to the truth.

My approximately-true explanation

If a Rails application can be thought of a stack listed top-to-bottom as views-controllers-models, controller tests exercise controllers and everything “lower”. So controller tests test the “controllers-models” part of the “views-controllers-models” stack.

Integration tests test the views and everything “lower”. So in the views-controllers-models stack, integration tests test all three layers.

The messier truth

The messier truth is that since RSpec is (as far as I can tell) substantially more popular than MiniTest for commercial Rails applications, then you, dear reader, are more likely to be an RSpec user than a MiniTest user.

So instead of “integration tests”, the term that’s applicable to you is feature specs. Luckily the definition is pretty much the same, though. Feature specs exercise the whole application stack (views-controllers-models).

When we start to talk about controller tests (or controller specs in RSpec terminology), things get a little confusing. In addition to the concept of controller specs, there now exists the concept of request specs. What’s the difference between the two? As far as I can tell, the main difference is that request specs run in a closer simulation to a real application environment than controller specs. Therefore, the RSpec core team recommends using request specs over controller specs. So the main thing to be aware of is that when someone says “controller tests”, the RSpec concept to map that to is “request specs”.

To sum up: in RSpec, it’s not integration tests vs. controller tests, it’s feature specs vs. request specs. (For the record, I’m not a big fan of RSpec’s somewhat arcane terminology.)

When to use integration tests vs. controller tests

Now that we’ve covered what these two things are, how do we know when to use them?

When I use controller tests/request specs

I’ll start with controller tests (or again in RSpec terminology, request specs). As I discussed in detail in a different article, I only use controller/request specs in two specific scenarios: 1) when I’m maintaining a legacy project and 2) when I’m maintaining an API-only application.

Why are these the only two scenarios in which I use request specs? Because when I’m doing greenfield development, I try very hard to make my controllers do almost nothing by themselves. I push all possible behavior into models. Legacy projects, however, often possess bloated controllers containing lots of code. I find tests useful in teasing apart those bloated controllers so I can refactor the code to mostly use models instead. The reason I use request specs when developing API-only applications is because integration tests/feature specs just aren’t possible. There’s no browser with which to interact.

When I use integration tests/feature specs

Practically every feature I write gets an integration test. For example, when I’m building CRUD functionality for a resource (let’s say a resource called `Customer`), I’ll write a feature spec for attempting to create a new customer record (using both valid and invalid inputs, checking for either success or failure), a feature spec for attempting to update a customer record, and perhaps a feature spec for deleting a customer record. Since feature specs exercise the whole application stack including controllers, I pretty much always find redundant the idea of writing both a feature spec and a request spec for a particular feature.

Addressing the comments

Finally I want to address some of the comments I saw on the Reddit post I referenced at the beginning of this article because I don’t think all of them are accurate.

Page value

“I always write controller tests. I only write integration tests if there’s some JavaScript interacting with the controller or if the page is really fucking valuable.”

I personally don’t buy this idea. To me, one of the main benefits of having integration test/feature spec coverage is that I can automatically check the whole application for regressions every time I make a new commit. I hate it when a client of mine has to point out an error to me, even if it’s something trivial. I’d much rather have my code get tested by automated tests than by my client. (And yes, I get that tests can’t prove the absence of bugs and that my test suite won’t always catch all regressions, but I think something is a lot better than nothing.)

API-only applications

“In this case I would say it depends if you are building a full rails app (front-end and back-end) or only and API (back-end). For the first I would say an integration test should go through the front-end (which eventually calls the controllers). If you are doing an API, integration and controller tests would be the same.”

Let me address the last sentence first. Yes, if you’re writing an API-only application, the max you can test is “from the controller down”, so the idea of adding an integration test that tests an additional layer doesn’t apply.

Now let me address the first part of the comment. The idea behind the comment makes sense to me. I think what the comment author is saying is that if the application is a “traditional” Rails application, then an integration test would hit the front-end, the user-facing part of the application, and exercise all parts of the code from that starting point.

“The same thing but worse”

“Controller tests attempt to test the controller in isolation, and integration tests mimic a browser clicking through the app, i.e. they touch the entire stack. Honestly I only write integration tests. Controller tests are basically exactly the same thing but worse, and they are harder to write”

I believe I basically agree with this comment but I’d like to get a little more specific. I wouldn’t exactly agree that “controller tests are basically the exact same thing” as integration tests. As I said above, integration tests/feature specs test one additional layer of the application beyond controllers. There’s a lot in that extra layer. It makes a big difference.

As for the second part, “Controller tests are basically the same thing but worse,” it would be helpful to say why they’re “worse”. I would repeat what I said earlier in that controller tests/requests specs for me are usually redundant to any integration tests/feature specs I might have. The exceptions, again, are legacy projects and API-only applications.

Controller tests going away?

“As far as I understand for the direction of Rails, controller tests will be going away and you’ll need to use integration tests.”

Unless I’m mistaken I believe this comment is a little confused. As I mentioned earlier in this article, it’s true that the RSpec core team no longer recommends using controller specs. The recommended replacement though isn’t integration tests but request specs. However, I personally tend to favor integration tests/feature specs over controller specs/request specs anyway.

I’m not able to find any evidence that MiniTest controller tests are going away.

Integration tests fragile and harder to maintain?

From my experience integration tests are always harder to maintain and are much more fragile. Not trying to neglect it’s value but it comes with a price. I wonder how will people going to test api only rails apps if controller tests are gone?

I wouldn’t necessarily disagree. Out of the test types I use, I find integration tests/feature specs to be the most expensive to maintain. I would, however, suggest that the additional value integration tests/feature specs provide over controller tests/request specs more than makes up for their extra cost.

In Which I Dissect and Improve a Rails Test File I Found on Reddit

I recently found a Reddit post which shared the code of a test and basically asked, “Am I doing this right?”

I’m going to dissect this test and make suggestions for improvement. Here’s the original code as shared in the post.

The original code

require 'test_helper'
class UserTest < ActiveSupport::TestCase

  let(:user) { FactoryBot.build :user}

  it "must be valid" do
    value(user).must_be :valid?
  end

  it "can be saved" do
    assert_difference "User.count", 1 do
      user.save
    end
    assert user.persisted?
  end

  describe "won't be valid" do
    it "with an duplicated email" do
      user.save
      user2 = FactoryBot.build :user
      user2.wont_be :valid?
      user2.errors.count.must_equal 1
      user2.errors[:email].must_equal ["has already been taken"]
    end

    it "without an email" do
      user.email = nil
      value(user).wont_be :valid?
      user.errors[:email].first.must_equal "can't be blank"
    end

    it "with an falsy email" do
      user.email = "thisis@falsemail"
      value(user).wont_be :valid?
      user.errors[:email].first.must_equal "is invalid"
    end

    it "with an email without a mx-record" do
      user.email = "hi@thisdomainwillneverexistnorhaveamxrecord.com"
      value(user).wont_be :valid?
      user.errors[:email].first.must_equal "is invalid"
    end

    it "with an email that is on our blacklist" do
      user.email = "test@trashmail.com"
      value(user).wont_be :valid?
      user.errors[:email].first.must_equal "is a blacklisted email provider"
    end
  end
end

Initial overall thoughts

Overall I think the OP did a pretty good job. He or she got the spirit right. The changes I would make are fairly minor.

Before I get into the actual improvements I want to say that I personally am not familiar with the style of using matchers like `must_be` and I actually wasn’t able to turn up any discussion of `must_be` using a quick Google search. So as I add improvements to the test code, I’m also going to convert the syntax to the syntax I’m personally used to using.

Validity of fresh object

Let’s first examine the first test, the test for the validity of a `User` instance:

it "must be valid" do
  value(user).must_be :valid?
end

It’s not a bad idea to assert that a model object must be valid when it’s in a fresh, untouched state. This test could be improved, however, by making it clearer that when we say “it must be valid”, we mean we’re talking about a freshly instantiated object.

Here’s how I might write this test instead:

context "fresh instance" do
  it "is valid" do
    expect(user).to be_valid
  end
end

We can actually make the test even more concise by taking advantage of `subject`:

subject { FactoryBot.build(:user) }

context "fresh instance" do
  it { should be_valid }
end

Persistence

Let’s look at the next test case, the test that asserts that a user can be persisted.

it "can be saved" do
  assert_difference "User.count", 1 do
    user.save
  end
  assert user.persisted?
end

I don’t see any value in this test. It’s true that an object needs to be able to be successfully saved, but we’ll be doing plenty of saving in the other tests anyway. Plus it’s typically pretty safe to take for granted that saving an ActiveRecord object is going to work. Rather than improving this test, I would just delete it.

Email validity

Lastly let’s take a look at the tests for email validity.

describe "won't be valid" do
  it "with an duplicated email" do
    user.save
    user2 = FactoryBot.build :user
    user2.wont_be :valid?
    user2.errors.count.must_equal 1
    user2.errors[:email].must_equal ["has already been taken"]
  end

  it "without an email" do
    user.email = nil
    value(user).wont_be :valid?
    user.errors[:email].first.must_equal "can't be blank"
  end

  it "with an falsy email" do
    user.email = "thisis@falsemail"
    value(user).wont_be :valid?
    user.errors[:email].first.must_equal "is invalid"
  end

  it "with an email without a mx-record" do
    user.email = "hi@thisdomainwillneverexistnorhaveamxrecord.com"
    value(user).wont_be :valid?
    user.errors[:email].first.must_equal "is invalid"
  end

  it "with an email that is on our blacklist" do
    user.email = "test@trashmail.com"
    value(user).wont_be :valid?
    user.errors[:email].first.must_equal "is a blacklisted email provider"
  end
end

The overall idea is sound although the structure is actually backwards from how I would do it. Rather than saying “here are all the ways the object could be invalid”, I would list the various conditions and then, inside of each condition, assert the validity state. So here’s how I might structure these tests:

context "with an duplicated email" do
  it "is not valid" do
    user.save
    user2 = FactoryBot.build :user
    user2.wont_be :valid?
    user2.errors.count.must_equal 1
    user2.errors[:email].must_equal ["has already been taken"]
  end
end

context "without an email" do
  it "is not valid" do
    user.email = nil
    value(user).wont_be :valid?
    user.errors[:email].first.must_equal "can't be blank"
  end
end

context "with an falsy email" do
  it "is not valid" do
    user.email = "thisis@falsemail"
    value(user).wont_be :valid?
    user.errors[:email].first.must_equal "is invalid"
  end
end

context "with an email without a mx-record" do
  it "is not valid" do
    user.email = "hi@thisdomainwillneverexistnorhaveamxrecord.com"
    value(user).wont_be :valid?
    user.errors[:email].first.must_equal "is invalid"
  end
end

context "with an email that is on our blacklist" do
  it "is not valid" do
    user.email = "test@trashmail.com"
    value(user).wont_be :valid?
    user.errors[:email].first.must_equal "is a blacklisted email provider"
  end
end

I’ll also try to improve each of these test cases individually.

Email uniqueness

This test can be written more concisely. Rather than this:

context "with an duplicated email" do
  it "is not valid" do
    user.save
    user2 = FactoryBot.build :user
    user2.wont_be :valid?
    user2.errors.count.must_equal 1
    user2.errors[:email].must_equal ["has already been taken"]
  end
end

We can do this:

context "when email is not unique" do
  let!(:other_user) { FactoryBot.create(:user, email: subject.email) }

  it "is not valid" do
    expect(subject.errors[:email]).to eq(["has already been taken"])
  end
end

Email presence, email format validity, MX record validity, and blacklist check

These four test cases are all very similar so I’ll deal with them as a group. Here are the original version:

context "without an email" do
  it "is not valid" do
    user.email = nil
    value(user).wont_be :valid?
    user.errors[:email].first.must_equal "can't be blank"
  end
end

context "with an falsy email" do
  it "is not valid" do
    user.email = "thisis@falsemail"
    value(user).wont_be :valid?
    user.errors[:email].first.must_equal "is invalid"
  end
end

context "with an email without a mx-record" do
  it "is not valid" do
    user.email = "hi@thisdomainwillneverexistnorhaveamxrecord.com"
    value(user).wont_be :valid?
    user.errors[:email].first.must_equal "is invalid"
  end
end

context "with an email that is on our blacklist" do
  it "is not valid" do
    user.email = "test@trashmail.com"
    value(user).wont_be :valid?
    user.errors[:email].first.must_equal "is a blacklisted email provider"
  end
end

For the most part these tests are fine. There’s only one significant thing I would do differently. I find it redundant to both assert that there’s an error and assert that the object is invalid. If the object has an error I think it’s safe to assume that it’s also invalid. Here are my versions:

context "without an email" do
  it "is not valid" do
    subject.email = nil
    subject.save
    expect(subject.errors[:email]).to include("can't be blank")
  end
end

context "with an invalid email" do
  it "is not valid" do
    subject.email = "thisis@falsemail"
    subject.save
    expect(subject.errors[:email]).to include("is invalid")
  end
end

context "with an email without a mx-record" do
  it "is not valid" do
    subject.email = "hi@thisdomainwillneverexistnorhaveamxrecord.com"
    subject.save
    expect(subject.errors[:email]).to include("is invalid")
  end
end

context "with an email that is on our blacklist" do
  it "is not valid" do
    subject.email = "test@trashmail.com"
    subject.save
    expect(subject.errors[:email]).to include("is a blacklisted email provider")
  end
end

The entire improved test file

Lastly, here’s the full file after I’ve made my improvements:

require 'rails_helper'

RSpec.describe User, type: :model do
  subject { FactoryBot.build(:user) }

  context "fresh instance" do
    it { should be_valid }
  end

  context "when email is not unique" do
    let!(:other_user) { FactoryBot.create(:user, email: subject.email) }

    it "is not valid" do
      expect(subject.errors[:email]).to eq(["has already been taken"])
    end
  end

  context "without an email" do
    it "is not valid" do
      subject.email = nil
      subject.save
      expect(subject.errors[:email]).to include("can't be blank")
    end
  end

  context "with an invalid email" do
    it "is not valid" do
      subject.email = "thisis@falsemail"
      subject.save
      expect(subject.errors[:email]).to include("is invalid")
    end
  end

  context "with an email without a mx-record" do
    it "is not valid" do
      subject.email = "hi@thisdomainwillneverexistnorhaveamxrecord.com"
      subject.save
      expect(subject.errors[:email]).to include("is invalid")
    end
  end

  context "with an email that is on our blacklist" do
    it "is not valid" do
      subject.email = "test@trashmail.com"
      subject.save
      expect(subject.errors[:email]).to include("is a blacklisted email provider")
    end
  end
end

My Vim setup for Rails

Vim is my favorite editor because using Vim I can edit circles around anyone using Atom, Visual Studio, Sublime Text or any other non-keyboard-based editor. If “computers are a bicycle for the mind”, then using Vim is like shifting that bicycle into high gear. I like to shift that gear even higher using a few certain plugins:

vim-rspec lets me run my tests with a keystroke.

vim-rails takes advantage of the common structure of all Rails applications to let me navigate any Rails application super quickly.

ctrlp.vim allows me to quickly search for and open files (not earth-shattering or unique but of course very useful).

Test smell: Obscure Test

You’ve probably heard of the idea of a “code smell” – a hint that something in the code is not quite right and ought to be changed.

Just as there are code smells, there are “test smells”. The book xUnit Test Patterns describes a number of them.

One of the smells described in the book is Obscure Test. An Obscure Test is a test that has a lot of noise in it, noise that’s making it hard to discern what the test is actually doing.

Here’s an example of an Obscure Test I wrote myself:

context 'the element does not exist' do
  before do
    contents = %(
      <?xml version="1.0" encoding="UTF-8"?>
      <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
        <channel>
          <item></item>
        </channel>
      </rss>
    )

    xml_doc = Nokogiri::XML(contents)
    episode_element = xml_doc.xpath('//item').first
    @rss_feed_episode = RSSFeedEpisode.new(episode_element)
  end

  it 'returns an empty string' do
    expect(@rss_feed_episode.content('title')).to eq('')
  end
end

There’s a lot of noise in the contents variable (e.g. <?xml version="1.0" encoding="UTF-8"?>). All that stuff is irrelevant to what the test is actually supposed to be testing. All this test should really care about is that we have an empty set of tags.

Here’s a refactored version of the same test:

context 'the element does not exist' do
  let(:rss_feed_episode) do 
    RSSFeedEpisodeTestFactory.create("<item></item>")
  end

  it 'returns an empty string' do
    expect(rss_feed_episode.content('title')).to eq('')
  end
end

Hopefully this is much more clear. The gory details of how to bring an RSS feed episode into existence are abstracted away into RSSFeedEpisodeTestFactory, a new class I created. Here’s what that class looks like:

class RSSFeedEpisodeTestFactory
  def self.create(inner_contents)
    @inner_contents = inner_contents
    rss_feed_episode
  end

  def self.rss_feed_episode
    RSSFeedEpisode.new(xml_doc.xpath('//item').first)
  end

  def self.xml_doc
    Nokogiri::XML(contents)
  end

  def self.contents
    %(
      <?xml version="1.0" encoding="UTF-8"?>
      <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
        <channel>#{@inner_contents}</channel>
      </rss>
    )
  end
end

Now I can use this factory class wherever I like. It not only helps me keep my tests more understandable but also helps cut down on duplication.

In the video below you can watch me refactor the “obscure” version into the more readable version as part of one of my free live Rails testing workshops.

Rails scaffolding and TDD are incompatible (but that’s okay)

Testing + TDD = a serious learning curve

Learning Rails testing is pretty hard. There are a lot of principles and tools to learn. Getting comfortable with testing in Rails (or any framework) often takes developers years.

Compounding the difficulty is TDD. If I’m just starting out with testing, should I learn TDD? Writing tests at all is hard enough. How am I supposed to write a test first?

And if the “testing + TDD” combo doesn’t generate enough cognitive turmoil, “testing + TDD + scaffolding” makes the scenario even murkier.

Scaffolding and TDD

In my experience most Rails developers take advantage of scaffolding to generate CRUD interfaces really quickly. Scaffolding is of course awesome and a big part of what makes Rails Rails.

Where things can get confusing is when you mix the ideas of “I want to use scaffolding to build an application quickly” and “I want to TDD my Rails app”. You cannot do both at the same time.

It seems obvious to me now but it took me a long time to realize it. In TDD, I write the tests before writing code. Scaffolding generates code but not tests*, the exact opposite. So I can’t apply TDD to scaffold-generated code. Test-after is my only option.

*It’s true that an application can be configured to automatically generate the shells of tests whenever a scaffold is generated, but they’re just shells of tests and they don’t actually test anything.

What to do about this TDD-scaffolding incompatibility

So, scaffolding and TDD are incompatible. What do we do about this?

My answer is nothing. The fact that scaffold-generated code can’t be TDD’d isn’t a bad thing, it’s just a fact. I’m making a conscious trade-off: in exchange for getting a bunch of CRUD code for free, I’m trading away the benefits of TDD in that particular area. I think on balance I come out ahead.

This fact does mean that I have to exercise a little more discipline when I’m working with scaffold-generated code. TDD automatically results in good code coverage because, if I follow the methodology to the letter (which BTW I don’t always), I never write a line of code without writing a test first. The rhythm of the red-green-refactor loop means that I don’t really have to apply much discipline. I just put one foot in front of the other according to the rules of TDD and when the dust settles I have good test coverage.

But when doing test-after, I need to resist the temptation to say “Hey, this code works. Do I really need to write a test? I’ll just move onto the next thing.”

How I write tests for scaffold-generated code

After I generate a scaffold I’ll usually follow something like the following process.

  1. Write model specs for attribute validation
  2. Write a feature spec for the “happy path” of creating the resource
  3. Write a feature spec for the “happy path” of updating the resource
  4. Look for anything that would be tedious to regression-test manually and write a feature spec for that

That’s about all I worry about for scaffold-generated code. If during the course of development a stupid bug pops up that a test would easily have prevented, I’ll backfill that code path with a test. And if I need to extend the functionality provided by the scaffold (in other words, if I need to write new code manually), I’ll TDD that, because at that point there’s nothing stopping me from doing so.

How dependency injection can make Rails tests easier

“Dependency injection” is a fancy-sounding term. When I first heard it I assumed it referred to some super-advanced technique. It wasn’t until years later that I realized that dependency injection is a pretty straightforward technique once you understand what it is.

My aim with this post is to cut through the jargon and show you in simple terms what dependency injection is and why it’s useful.

But first: why are we interested in this topic?

Why bother learning dependency injection?

Depending on how it’s written, some code can be easy to test and some code can be hard to test. Code with entangled dependencies is hard to test.

Why is code with entangled dependencies hard to test? Imagine I have a class `Order` that requires instances of a class called `Payment` in order to function. Let’s then imagine that `Payment` needs some `PaymentType` instances (Visa, MasterCard, cash, etc.) in order to work.

This means that in order to test the class I’m interested in, `Order`, I have to bring two other classes into the picture, `Payment` and `PaymentType`, just to perform the test. And what if `Payment` and `PaymentType` in turn depend on other classes? This test is going to potentially be very tedious to set up.

The opposite of having entangled dependencies is having loose coupling and modularity. Modular, loosely coupled code is easy to test. A number of factors have a bearing on how modular and loosely coupled your code will end up. What I want to show you right now is how dependency injection can help make your code more modular and therefore more easily testable.

An dependency-laden Rails model

Let’s say you’re working on a legacy project that you recently inherited. There’s very little test coverage. You encounter an ActiveRecord model called `CustomerFile`. There’s a method called `parse` that evidently parses a CSV.

class CustomerFile < ActiveRecord::Base
  belongs_to :customer

  def parse
    rows = []

    content = File.read(customer.csv_filename)
    CSV.parse(content, headers: true) do |data|
      rows << data.to_h
    end

    rows
  end
end

Let’s focus on this line for a second: `content = File.read(customer.csv_filename)`.

Apparently a `CustomerFile` object has an associated `customer` object which in turn has a `csv_filename`. How exactly does `customer` get set? It’s not clear. Where exactly is the file that `customer.csv_filename` points to? That’s not obvious either.

We can try to write a test for `CustomerFile` but it probably won’t go very well.

RSpec.describe CustomerFile do
  describe '#parse' do
    let(:customer_file) { CustomerFile.new }

    it 'parses a CSV' do
      # How do we know what to expect?
      # Where is the file that "customer.csv_filename" refers to?
      # expected_first_row = ?

      expect(customer_file.parse[0]).to eq(expected_first_row)
    end
  end
end

Our attempt to write a test hasn’t proven very fruitful. The challenge of writing a test for this class is somewhat “uncomeatable”.

The reason it’s hard to write this test is that `CustomerFile` has a dependency inside of a dependency. We don’t know how to make a `customer`, and even more problematic, we don’t know how to make a CSV file for that customer.

Applying dependency injection for easier testability

Let’s imagine now that `parse` doesn’t require that we have a `customer` with `csv_filename` that points to some mysterious file on the filesystem somewhere.

Let’s imagine a version of `parse` that just takes the file contents as an argument.

class CustomerFile < ActiveRecord::Base
  belongs_to :customer

  def parse(content)
    rows = []

    CSV.parse(content, headers: true) do |data|
      rows << data.to_h
    end

    rows
  end
end

When we try to write a test now, we’ll see that it’s much easier.

RSpec.describe CustomerFile do
  describe '#parse' do
    let(:customer_file) { CustomerFile.new }
    let(:content) { "First Name,Last Name\nJohn,Smith" }

    it 'parses a CSV' do
      expected_first_row = {
        'First Name' => 'John',
        'Last Name' => 'Smith'
      }

      expect(customer_file.parse(content)[0]).to eq(expected_first_row)
    end
  end
end

In this case `parse` doesn’t know or care where the CSV content comes from. This means that we don’t have to bring the filesystem into the picture at all which makes writing this test very convenient. No `customer` object or `customer.csv_filename` value necessary.

If we want to use `parse` for real in the application we can just pass in the file contents like this: `parse(File.read(customer.csv_filename))`.

Conclusion

Modular, loosely coupled code is testable code. You can use dependency injection to help make your code more modular.

Things you can ignore when getting started with Rails testing

Here’s an incomplete list of tools and concepts you might encounter when first trying to learn Rails testing: Capybara, Cucumber, Database Cleaner, factory_bot, Faker, MiniTest, RSpec, system tests, Test::Unit, acceptance tests, end-to-end-tests, mocks, stubs, unit tests and TDD.

That’s a lot of stuff. If you’re like most humans you might look at this long list of things, feel confused about where to start, and say, “I don’t know what to do. I’ll just deal with this later.”

The challenge of getting started with testing would be much easier if you knew exactly what you needed to know and what you could safely ignore. What follows is a list of what you can safely ignore.

Framework Decisions

You don’t need to get hung up on which framework to use. You literally can’t go wrong. The principles of testing are the same no matter which testing framework you use. Plus you can always change your mind later.

When I first got started with Rails, the way I decided on a testing framework was very simple. I noticed that most Rails developers used RSpec, so I just picked RSpec.

(I actually used Test::Unit for a while before realizing most Rails developers used RSpec. So I just switched. It wasn’t a very big deal.)

Cucumber, Capybara and System Tests

Most Rails test suites have two main sub-suites: a suite of model tests and a suite of integration tests.

Model tests test the behavior of ActiveRecord models by themselves. Integration tests actually spin up a browser and do things like fill out forms and click links and buttons as if a human is actually using the application.

Between these two, the bar is lower for model tests. For integration tests you have to do almost all the same stuff as model tests plus more. For this reason I suggest that if you’re a newcomer to Rails testing that you start with model tests (or even just Ruby tests without Rails) and ignore integration tests altogether until you get more comfortable with testing.

So you can ignore Cucumber (which I don’t recommend using at all), Capybara and system tests, which are all integration testing tools.

View Specs

I’ve never written a view spec. To me they seem tautological. I’ve also never encountered any other Rails developer who advocates writing view specs.

If for some reason I had a wildly complicated view, I could see a view spec potentially making sense. But I haven’t yet encountered that case.

Helper Specs

I don’t tend to write helper specs because I don’t tend to write helpers. Helpers are a fairly peripheral area of Rails that you can safely disregard altogether when you’re getting started with Rails testing.

Routing Specs

Like view specs, I find routing specs to be tautological. I don’t write them.

Request Specs

Request specs are a great boon and even necessity if your application has an API or if your controllers do anything non-trivial. But I don’t think you should worry about request specs when you’re just getting started.

Controller Specs

Controller specs are deprecated in favor of request specs.

What Not to Ignore

The main thing I would recommend learning when you’re getting started with Rails testing is model tests.

When you’re learning about model tests you’ll naturally have to get acquainted with RSpec syntax (or whichever framework you choose), factory_bot and Database Cleaner (or analogous tools). But other than the actual testing techniques, that’s about it as far as model test tooling goes.

If you want to make life even easier on yourself you can learn just Ruby and RSpec with no Rails involved. Then, after you get comfortable with RSpec syntax and basic testing techniques, you can approach Rails testing with more confidence.

How to get started with Rails testing

Where to start with Rails testing

Getting started with Rails testing can be overwhelming because there’s so much to learn. I think the learning process can be made easier by following the following three steps.

Get familiar with testing principles

In order to write tests in Rails it’s necessary to have some understanding of the tools that are used for Rails testing. But also, perhaps more than that, you need to have an understanding of the technology-agnostic principles that apply no matter what language or framework you’re using.

The principles of automated testing are quite numerous. Just as you could spend a lifetime studying programming and never run out of things to learn, you could probably spend a lifetime studying testing and never run out of things to learn.

So the trick early on is to find the few testing principles that you need in order to get started and ignore all the rest. You can learn other principles as you go.

Here are a few principles I think are helpful to be familiar with at the beginning.

Test-driven development

I personally think TDD is life-changingly great. At the same time, I wouldn’t recommend being concerned with following TDD to the letter when you’re first getting started with testing.

The fundamental principle of TDD is the red-green-refactor loop. First you write a failing test for the behavior you want to bring into existence. Then you write the code to make it pass, only concerning yourself with making the test pass and not with code quality at all. Then, once the test is passing, go back to the code and refactor it to make it nice.

When I was getting started with testing I had many occasions where I wanted to write the test first but I wasn’t sure how. What should you do if that happens to you? My advice would be to give yourself permission to break the TDD “rules” and just write the code before the test. As you gain more experience with testing it will get easier and easier to write the test first.

Four-phase testing

Automated tests tend to consist of four steps: setup, exercise, verification and teardown. Here’s an example in MiniTest.

class UserTest < Minitest::Test
  def test_soft_delete_user
    user = User.create!(email: 'test@example.com') # setup
    user.update_attributes!(active: false)         # exercise
    assert user.active == false                    # assertion
    user.destroy!                                  # teardown
  end
end

In the first step, setup, we create the data that the test needs in order to do its stuff.

In the second step, exercise, we walk the system under test* through the steps that are necessary to get the system into the state we’re interested in.

In the third step, assertion, we ask, “Did the code do the thing it was expected to do?”

Finally, in the teardown, we put the system back the way we found it. (In Rails testing an explicit teardown step is usually not necessary because tests are often run inside of database transactions. The data doesn’t have to get deleted because it never gets persisted in the first place.)

*System under test (SUT) is a fancy way of saying “the part of the application we’re testing”.

Test independence/deterministic tests

It’s important that when a test runs, we get the same result every time. The passing or failing of the test shouldn’t depend on things like the date when the test was run or whether a certain other test was run before it or not.

That’s why the teardown step above is important. If a test leaves behind data after it runs, that data has the potential to interfere with another test that runs after it. Again, in Rails, developers tend not to explicitly destroy each test’s data but rather make use of database transactions to avoid persisting each test’s data in the first place. The transaction starts at the beginning of each test and at the end of each test the transaction is aborted.

Fixtures and factories

The first step of a test, setup, can get quite tedious and repetitive. There are easier ways of bringing the test data into existence than by instantiating objects and creating database records at the beginning of every single test.

There are two ways that setting up test data is normally handled: factories and fixtures. Both strategies have pros and cons. Most projects I’ve worked on use either one or the other but there’s no reason they couldn’t be used together.

Fixtures in Rails are usually defined in terms of YML files. The fixture data is persisted once at the beginning of the test suite run. Each test still runs in a database transaction so that any data modifications the test makes will be undone before the next test is run.

Fixtures have the advantage of speed, although the trade-off is that the data setup is a little bit distant from the test code itself. It might not always be clear what the data is that a test depends on or where it’s coming from.

With factories, the data for each test is defined inside the test code itself instead of in a separate YML file. Factories have the advantage of clarity. If the tests were written well then it’s very clear what data the test depends on to run. The trade-off is speed.

Mocks and stubs

I bring up mocks and stubs to tell you that you can safely ignore them when you’re getting started.

You can get quite a ways with Rails testing before a lack of mock and stub knowledge will hinder you. But if you’re curious, the books xUnit Patterns and Growing Object-Oriented Software, Guided by Tests do a pretty good job of explaining the concepts.

Get familiar with Rails testing tooling

Testing frameworks

The two dominant testing frameworks for Rails are RSpec and MiniTest. RSpec seems to be much more popular although that certainly doesn’t mean it’s better.

“Should I use RSpec or MiniTest?” is a question I see lot. Luckily you can’t really choose wrong. The underlying testing principles are pretty much the same whether you use RSpec or MiniTest. The main difference between the two is that RSpec is built on a domain-specific language (DSL) while MiniTest is just Ruby. A lot of people find that RSpec’s DSL improves readability although the tradeoff is that you have to learn the DSL.

I personally started out with Test::Unit (very similar to MiniTest) when I was doing Rails projects on my own. Later I switched to RSpec because almost every job or freelance client I’ve had used RSpec. So even if you pick the “wrong” testing framework to start with, you can always switch to the other one later.

Since I’m an RSpec guy I’ll focus mainly on RSpec-related tools.

Factory Bot

Factory Bot is the de-facto standard library for Rails. The main benefits of Factory Bot in my view are that it helps DRY up test setup code and that it eliminates the need to come up with arbitrary test data (e.g. fake names, addresses, etc.).

Faker

Factory Bot only goes so far with its ability to generate fake data. If I want to generate things like fake but valid email addresses, phone numbers, etc., I use Faker in conjunction with Factory Bot.

Capybara

Capybara is a tool that lets us use Ruby to drive the browser. This enables us to write tests that exercise the whole application stack including HTML, CSS and JavaScript.

Build some practice projects

Just like with programming itself, the way to get good at testing is to just start doing it.

Here’s the way I generally go about developing a new feature or resource in Rails. (I do a Git commit after each step if not more often.)

  1. Run rails generate scaffold <scaffold name> <options>
  2. Write some validation specs for the model I just created
  3. Write a feature spec for the “happy path” (i.e. all valid inputs) of the “create” and “update” functionality for the resource I just created

For the first few models I create in a Rails project I might not do much more than that. It’s usually not until a few models in that my application builds behavior that’s “interesting” enough to write a meaningful test for.

If you want to learn more about how I write Rails tests, I run free online Rails testing workshops about once a week. All the videos from previous sessions are up on YouTube (linked here). I also have a free Ruby testing micro-course.