Category Archives: RSpec

A VCR + WebMock “hello world” tutorial

Introduction

VCR and WebMock are tools that help deal with challenges related to tests that make network requests. In this post I’ll explain what VCR and WebMock are and then show a “hello world”-level example of using the two tools.

Why VCR and WebMock exist

Determinism

One of the principles of testing is that tests should be deterministic. The passing or failing of a test should be determined by the content of the application code and nothing else. All the tests in the test suite should pass regardless of what order they were run in, what time of day they were run, or any other factor.

For this reason we have to run tests in their own encapsulated world. If we want determinism, we can’t let tests talk to the network, because the network (and things in the network) are susceptible to change and breakage.

Imagine an app that talks to a third-party API. Imagine that the tests hit that API each time they’re run. Now imagine that on one of the test runs, the third-party API happens to go down for a moment, causing our tests to fail. Our test failure is illogical because the tests are telling us our code is broken, but it’s not our code that’s broken, it’s the outside world that’s broken.

If we’re not to talk to the network, we need a way to simulate our interactions with the network so that our application can still behave normally. This is where tools like VCR and WebMock come in.

Production data

We also don’t want tests to alter actual production data. It would obviously be bad if we for example wrote a test for deleting users and then that test deleted real production users. So another benefit of tools like VCR and WebMock is that they save us from having to touch real production data.

The difference between VCR and WebMock

VCR is a tool that will record your application’s HTTP interactions and play them back later. Very little code is necessary. VCR tricks your application into thinking it’s receiving responses from the network when really the application is just receiving prerecorded VCR data.

WebMock, on the other hand, has no feature for recording and replaying HTTP interactions in the way that VCR does, although HTTP interactions can still be faked. Unlike VCR’s record/playback features, WebMock’s network stubbing is more code-based and fine-grained. In this tutorial we’re going to take a very basic look at WebMock and VCR to show a “hello world” level usage.

What we’re going to do

This post will serve as a simple illustration of how to use VCR and WebMock to meet the need that these tools were designed to meet: running tests that hit the network without actually hitting the network.

If you’d like to follow along with this tutorial, I’d suggest first setting up a Rails app for testing according to the instructions in this post.

The scenario

We’re going to write a small search feature that hits a third-party API. We’re also going to write a test that exercises that search feature and therefore hits the third-party API as well.

WebMock

Once we have our test in place we’ll install and configure WebMock. This will disallow any network requests. As a result, our test will stop working.

VCR

Lastly, we’ll install and configure VCR. VCR knows how to talk with WebMock. Because of this, VCR and WebMock can come to an agreement together that it’s okay for our test to hit the network under certain controlled conditions. VCR will record the HTTP interactions that occur during the test and then, on any subsequent runs of the test, VCR will use the recorded interactions rather than making fresh HTTP interactions for each run.

The feature

The feature we’re going to write for this tutorial is one that searches the NPI registry, a government database of healthcare providers. The user can type a provider’s first and last name, hit Search, and then see any matches.

Below is the controller code.

# app/controllers/npi_searches_controller.rb

ENDPOINT_URL = "https://npiregistry.cms.hhs.gov/api"
TARGET_VERSION = "2.1"

class NPISearchesController < ApplicationController
  def new
    @results = []
    return unless params[:first_name].present? || params[:last_name].present?

    query_string = {
      first_name: params[:first_name],
      last_name: params[:last_name],
      version: TARGET_VERSION,
      address_purpose: ""
    }.to_query

    uri = URI("#{ENDPOINT_URL}/?#{query_string}")
    response = Net::HTTP.get_response(uri)
    @results = JSON.parse(response.body)["results"]
  end
end

Here’s the template that goes along with this controller action.

<!-- app/views/npi_searches/new.html.erb -->

<h1>New NPI Search</h1>

<%= form_tag new_npi_search_path, method: :get do %>
  <%= text_field_tag :first_name, params[:first_name], class: "p-1" %>
  <%= text_field_tag :last_name, params[:last_name], class: "p-1" %>
  <%= submit_tag "Search", class: "p-1" %>
<% end %>

<% @results.each do |result| %>
  <div>
    <%= result["basic"]["name_prefix"] %>
    <%= result["basic"]["first_name"] %>
    <%= result["basic"]["last_name"] %>
    <%= result["number"] %>
  </div>
<% end %>

The test

Our test will be a short system spec that types “joel” and “fuhrman” into the first and last name fields respectively, clicks Search, then asserts that Joel Fuhrman’s NPI code (a unique identifier for a healthcare provider) shows up on the page.

# spec/system/npi_search_spec.rb

require "rails_helper"

RSpec.describe "NPI search", type: :system do
  it "shows the physician's NPI number" do
    visit new_npi_search_path
    fill_in "first_name", with: "joel"
    fill_in "last_name", with: "fuhrman"
    click_on "Search"

    # 1386765287 is the NPI code for Dr. Joel Fuhrman
    expect(page).to have_content("1386765287")
  end
end

If we run this test at this point, it passes.

Installing and Configuring WebMock

We don’t want our tests to be able to just make network requests willy-nilly. We can install WebMock so that no HTTP requests can be made without our noticing.

First we add the webmock gem to our Gemfile.

# Gemfile

group :development, :test do
  gem "webmock"
end

Second, we can create a new file at spec/support/webmock.rb.

# spec/support/webmock.rb

# This line makes it so WebMock and RSpec know how to talk to each other.
require "webmock/rspec"

# This line disables HTTP requests, with the exception of HTTP requests
# to localhost.
WebMock.disable_net_connect!(allow_localhost: true)

Remember that files in spec/support won’t get loaded unless you have the line in spec/rails_helper.rb uncommented that loads these files.

# spec/rails_helper.rb

# Make sure to uncomment this line
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

Seeing the test fail

If we run our test again now that WebMock is installed, it will fail, saying “Real HTTP connections are disabled”. We also get some instructions on how to stub this request if we like. We’re not going to do that, though, because we’re going to use VCR instead.

Failures:

  1) NPI search shows the physician's NPI number
   Failure/Error: response = Net::HTTP.get_response(uri)

   WebMock::NetConnectNotAllowedError:
     Real HTTP connections are disabled. Unregistered request: GET https://npiregistry.cms.hhs.gov/api/?address_purpose=&first_name=joel&last_name=fuhrman&version=2.1 with headers {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Host'=>'npiregistry.cms.hhs.gov', 'User-Agent'=>'Ruby'}

     You can stub this request with the following snippet:

     stub_request(:get, "https://npiregistry.cms.hhs.gov/api/?address_purpose=&first_name=joel&last_name=fuhrman&version=2.1").
       with(
         headers: {
           'Accept'=>'*/*',
           'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
           'Host'=>'npiregistry.cms.hhs.gov',
           'User-Agent'=>'Ruby'
           }).
         to_return(status: 200, body: "", headers: {})

       ============================================================

Installing and Configuring VCR

First we’ll add the vcr gem to our Gemfile.

# Gemfile

group :development, :test do
  gem 'vcr'
end

Next we’ll add the following config file. I’ve added annotations so you can understand what each line is.

# spec/support/vcr.rb

VCR.configure do |c|
  # This is the directory where VCR will store its "cassettes", i.e. its
  # recorded HTTP interactions.
  c.cassette_library_dir = "spec/cassettes"

  # This line makes it so VCR and WebMock know how to talk to each other.
  c.hook_into :webmock

  # This line makes VCR ignore requests to localhost. This is necessary
  # even if WebMock's allow_localhost is set to true.
  c.ignore_localhost = true

  # ChromeDriver will make requests to chromedriver.storage.googleapis.com
  # to (I believe) check for updates. These requests will just show up as
  # noise in our cassettes unless we tell VCR to ignore these requests.
  c.ignore_hosts "chromedriver.storage.googleapis.com"
end

Adding VCR to our test

Now we can add VCR to our test by adding a block to it that starts with VCR.use_cassette "npi_search" do. The npi_search part is just arbitrary and tells VCR what to call our cassette.

# spec/system/npi_search_spec.rb

require "rails_helper"

RSpec.describe "NPI search", type: :system do
  it "shows the physician's NPI number" do
    VCR.use_cassette "npi_search" do # <---------------- add this
      visit new_npi_search_path
      fill_in "first_name", with: "joel"
      fill_in "last_name", with: "fuhrman"
      click_on "Search"

      expect(page).to have_content("1386765287")
    end
  end
end

Last time we ran this test it failed because WebMock was blocking its HTTP request. If we run the test now, it will pass, because VCR and WebMock together are allowing the HTTP request to happen.

If we look in the spec/cassettes directory after running this test, we’ll see that there’s a new file there called npi_search.yml. Its contents look like the following.

---
http_interactions:
- request:
    method: get
    uri: https://npiregistry.cms.hhs.gov/api/?address_purpose=&first_name=joel&last_name=fuhrman&version=2.1
    body:
      encoding: US-ASCII
      string: ''
    headers:
      Accept-Encoding:
      - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
      Accept:
      - "*/*"
      User-Agent:
      - Ruby
      Host:
      - npiregistry.cms.hhs.gov
  response:
    status:
      code: 200
      message: OK
    headers:
      Date:
      - Sat, 17 Apr 2021 11:13:41 GMT
      Content-Type:
      - application/json
      Strict-Transport-Security:
      - max-age=31536000; includeSubDomains
      Set-Cookie:
      - TS017b4e40=01acfeb9489bd3c233ef0e8a55b458849e619bdc886c02193c4772ba662379fa1f8493887950c06233f28bbbaac373afba8b58b00f;
        Path=/; Domain=.npiregistry.cms.hhs.gov
      Transfer-Encoding:
      - chunked
    body:
      encoding: UTF-8
      string: '{"result_count":1, "results":[{"enumeration_type": "NPI-1", "number":
        1386765287, "last_updated_epoch": 1183852800, "created_epoch": 1175472000,
        "basic": {"name_prefix": "DR.", "first_name": "JOEL", "last_name": "FUHRMAN",
        "middle_name": "H", "credential": "MD", "sole_proprietor": "YES", "gender":
        "M", "enumeration_date": "2007-04-02", "last_updated": "2007-07-08", "status":
        "A", "name": "FUHRMAN JOEL"}, "other_names": [], "addresses": [{"country_code":
        "US", "country_name": "United States", "address_purpose": "LOCATION", "address_type":
        "DOM", "address_1": "4 WALTER E FORAN BLVD", "address_2": "SUITE 409", "city":
        "FLEMINGTON", "state": "NJ", "postal_code": "088224664", "telephone_number":
        "908-237-0200", "fax_number": "908-237-0210"}, {"country_code": "US", "country_name":
        "United States", "address_purpose": "MAILING", "address_type": "DOM", "address_1":
        "4 WALTER E FORAN BLVD", "address_2": "SUITE 409", "city": "FLEMINGTON", "state":
        "NJ", "postal_code": "088224664", "telephone_number": "908-237-0200", "fax_number":
        "908-237-0210"}], "taxonomies": [{"code": "207Q00000X", "desc": "Family Medicine",
        "primary": true, "state": "NJ", "license": "25MA05588600"}], "identifiers":
        []}]}'
  recorded_at: Sat, 17 Apr 2021 11:13:41 GMT
recorded_with: VCR 6.0.0

Each time this test is run, VCR will ask, “Is there a cassette called npi_search?” If not, VCR will allow the HTTP request to go out, and a new cassette will be recorded for that HTTP request. If there is an existing cassette called npi_search, VCR will block the HTTP request and just use the recorded cassette in its place.

Takeaways

  • Tests should be deterministic. The passing or failing of a test should be determined by the content of the application code and nothing else.
  • We don’t want tests to be able to alter production data.
  • WebMock can police our app to stop it from making external network requests.
  • VCR can record our tests’ network interactions for playback later.

Rails model spec tutorial, part two

Prerequisites

If you’d like to follow along in this tutorial, I recommend first setting up a Rails application according to my how I set up a Rails application for testing
post. (To make things easier, you could use Instant Rails, a tool I created for generating Rails applications.) It doesn’t matter what you call the application.

The tutorial

Learning objectives

Before we list the learning objectives for Part Two of the tutorial, let’s review the learning objectives for Part One.

  1. How to come up with test cases for a model based on the model’s desired behavior
  2. How to translate those test cases into actual working test code, in a methodical and repeatable manner
  3. How to use a test-first approach to make it easier both to write the tests and to write the application code

In Part One we dealt with plain old Ruby objects (POROs) rather than actual Rails models. Without having done that, it might be unclear where the testing principles stopped and the Rails-specific work began.

In Part Two of the tutorial we’ll layer on the Rails work so you can easily tell which parts are which.

The scenario

We’ll be working on the exact same scenario as Part One: normalizing messy phone numbers. We’ll even be using all the exact same test cases. The reason we’re keeping those things the same is to show the Rails-models-versus-POROs differences in sharp relief.

The PhoneNumber model

As mentioned in the Prerequisites section, you’ll need to create a fresh Rails according to my how I set up a Rails application for testing
post.

Once we’ve done that, we can generate a new model called PhoneNumber.

$ rails g model phone_number value:string
$ rails db:migrate

This will automatically generate a test file at spec/models/phone_number_spec.rb.

# spec/models/phone_number_spec.rb

require "rails_helper"

RSpec.describe PhoneNumber, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"
end

Our first test case

Just like the first test case in Part One, the first test here will verify that a number like 555-856-8075 gets stripped down to 5558568075.

Unlike the test in Part One where instantiating a PhoneNumber object just involved doing PhoneNumber.new, we’ll be using Factory Bot in this test to create an instance of the PhoneNumber model. We could have gotten away with just doing PhoneNumber.new(value: "555-856-8075") instead of using Factory Bot in this instance, but using Factory Bot is so common in Rails model tests that I wanted to show an example using it.

# app/models/phone_number.rb

require "rails_helper"

RSpec.describe PhoneNumber, type: :model do
  context "phone number contains dashes" do
    it "strips out the dashes" do
      phone_number = FactoryBot.create(
        :phone_number,
        value: "555-856-8075"
      )

      expect(phone_number.value).to eq("5558568075")
    end
  end
end

If we run this test it will fail because we haven’t yet added any code to strip out dashes.

Failures:

  1) PhoneNumber phone number contains dashes strips out the dashes
     Failure/Error: expect(phone_number.value).to eq("5558568075")
     
       expected: "5558568075"
            got: "555-856-8075"
     
       (compared using ==)

Let’s strip out the non-numeric characters via a before_validation callback.

# app/models/phone_number.rb

class PhoneNumber < ApplicationRecord
  before_validation :strip_non_numeric_from_value

  def strip_non_numeric_from_value
    self.value = self.value.gsub(/\D/, "")
  end
end

If we run the test again, it passes.

The other two formats

Now let’s add a test scenario for the format of (555) 856-8075.

require "rails_helper"

RSpec.describe PhoneNumber, type: :model do
  context "phone number contains dashes" do
    it "strips out the dashes" do
      phone_number = FactoryBot.create(
        :phone_number,
        value: "555-856-8075"
      )

      expect(phone_number.value).to eq("5558568075")
    end
  end

  context "phone number contains parentheses" do
    it "strips out the non-numeric characters" do
      phone_number = FactoryBot.create(
        :phone_number,
        value: "(555) 856-8075"
      )

      expect(phone_number.value).to eq("5558568075")
    end
  end
end

If we run this test, we’ll see that it already passes, thanks to the before_validation callback we added above.

Let’s now add the final format, +1 555 856 8075.

require "rails_helper"

RSpec.describe PhoneNumber, type: :model do
  context "phone number contains dashes" do
    it "strips out the dashes" do
      phone_number = FactoryBot.create(
        :phone_number,
        value: "555-856-8075"
      )

      expect(phone_number.value).to eq("5558568075")
    end
  end

  context "phone number contains parentheses" do
    it "strips out the non-numeric characters" do
      phone_number = FactoryBot.create(
        :phone_number,
        value: "(555) 856-8075"
      )

      expect(phone_number.value).to eq("5558568075")
    end
  end

  context "phone number contains country code" do
    it "strips out the country code" do
      phone_number = FactoryBot.create(
        :phone_number,
        value: "+1 555 856 8075"
      )

      expect(phone_number.value).to eq("5558568075")
    end
  end
end

This one does not pass. Even though we’re stripping out non-numeric characters, we’re not stripping out country codes.

Failures:

  1) PhoneNumber phone number contains country code strips out the country code
     Failure/Error: expect(phone_number.value).to eq("5558568075")
     
       expected: "5558568075"
            got: "15558568075"
     
       (compared using ==)

We can make this test pass using the same exact code we used in Part One.

class PhoneNumber < ApplicationRecord
  before_validation :strip_non_numeric_from_value

  def strip_non_numeric_from_value
    self.value = self.value.gsub(/\D/, "")
      .split("")
      .last(10)
      .join
  end
end

And also just like in Part One, we don’t want the magic number of 10 sitting there. Let’s assign that number to a constant.

class PhoneNumber < ApplicationRecord
  EXPECTED_NUMBER_OF_DIGITS = 10
  before_validation :strip_non_numeric_from_value

  def strip_non_numeric_from_value
    self.value = self.value.gsub(/\D/, "")
      .split("")
      .last(EXPECTED_NUMBER_OF_DIGITS)
      .join
  end
end

Refactoring

There’s a small way we can make our test a little tidier.

Rather than repeatedly creating a new phone_number variable using FactoryBot.create, we can DRY up our code a little by putting the FactoryBot.create in a let! block at the beginning and then updating the phone number value for each test.

require "rails_helper"

RSpec.describe PhoneNumber, type: :model do
  let!(:phone_number) do
    FactoryBot.create(:phone_number)
  end

  context "phone number contains dashes" do
    before { phone_number.update!(value: "555-856-8075") }

    it "strips out the dashes" do
      expect(phone_number.value).to eq("5558568075")
    end
  end

  context "phone number contains parentheses" do
    before { phone_number.update!(value: "(555) 856-8075") }

    it "strips out the non-numeric characters" do
      expect(phone_number.value).to eq("5558568075")
    end
  end

  context "phone number contains country code" do
    before { phone_number.update!(value: "+1 555 856 8075") }

    it "strips out the country code" do
      expect(phone_number.value).to eq("5558568075")
    end
  end
end

Takeaways

Rails model tests can be written by coming up with a list of desired behaviors and translating that list into test code.

When learning how to write Rails model tests, it can be helpful to first do some tests with plain old Ruby objects (POROs) for practice.

Writing tests before we write the application code can make the process of writing the application code easier.

Rails model spec tutorial, part one

Overview

Because there are so many tools and concepts involved in Rails model testing, I’ve separated this beginner tutorial into two parts.

Part One will deal with writing tests just for plain old Ruby objects (POROs) in order to get comfortable with the process without bringing specifics of Rails into the picture.

Part Two will repeat what was done in Part One but in the context of an actual Rails model instead of a PORO.

Before we get into the tutorial itself we’ll discuss a little bit of necessary context: what a model is and how model specs are different from other types of specs.

What a model is

It may seem obvious what a Rails model is. To many Rails developers, the model is the MVC layer that talks to the database.

But in my experience, there are actually a lot of different conceptions as to what Rails models are, and not all of them agree with each other. I think it’s important for us to firmly establish what a Rails model is before we start talking about how to test Rails models.

To me, a model is an abstraction that represents a small corner of reality in a simplified way. Models exist to make it easier for a programmer to think about and work with the concepts in a program.

Models are not a Rails idea or even an OOP idea. A model could be represented in any programming language and in any programming paradigm.

In the context of Rails, models don’t have to be just the files that inherit from ActiveRecord::Base. I create most of my models as plain old Ruby objects (POROs) and put them in app/models right alongside Active Record models because to me the distinction between Active Record models and PORO models isn’t very important of a distinction.

Why model specs are different from other types of specs

Because models aren’t a Rails idea but rather a programming idea, testing models in Rails isn’t that conceptually different from testing models in any other language or framework.

In a way this is a great benefit to a learner because it means that if you know how to test models in one language, your testing skills will easily translate to any other language.

On the other hand this is a difficulty because, relative to other types of Rails tests, model tests don’t have such a straight and narrow path to success. The generic-ness of Rails models means that there’s not a rigid template that can be followed in order to write Rails models, unlike, for example, system specs.

System specs are relatively easy to get started with because you can more or less follow a certain step-by-step formula for writing system specs for CRUD features and be well on your way. There’s not as much of a step-by-step formula for writing model tests.

Nonetheless, there are certain principles and tactics you can learn. Let’s take a look at some of the ideas that can help you get started.

The tutorial

Learning objectives

Here are the things you can expect to have a better understanding of after completing this tutorial.

  1. How to come up with test cases for a model based on the model’s desired behavior
  2. How to translate those test cases into actual working test code, in a methodical and repeatable manner
  3. How to use a test-first approach to make it easier both to write the tests and to write the application code

The scenario

The model we’re going to be writing tests for is a model that represents a phone number. Our phone number model will be designed, at least for starters, to mainly deal with just one particular challenge that tends to be common in applications that deal with phone numbers.

This challenge is that it’s usually desired to display phone numbers in a consistent format (e.g. “(555) 555-5555”), but phone numbers are often entered by users in a variety of formats (e.g. “555-555-5555” or “1 555 555 5555”).

A common way of dealing with this problem is to strip down the user input so that it’s all numbers and store the all-numbers version in the database. So, for example, 555-555-5555 would become 5555555555.

We want our phone number model to be able to take phone numbers in any of the following formats:

555-856-8075
(555) 856-8075
+1 555 856 8075

And strip them down to look like this:

5558568075

Our PhoneNumber class won’t know anything about databases, it will just be responsible for converting a “messy” phone number to a normalized one.

The approach

We’re going to take the following three scenarios

555-856-8075
(555) 856-8075
+1 555 856 8075

and write tests for each one.

The shell of the spec and PhoneNumber class

Our beginning spec will contain the bare minimum: an outer describe block and a require_relative that includes our “application code”, phone_number.rb.

require_relative './phone_number.rb'

RSpec.describe PhoneNumber do
end

The phone_number.rb file will only contain the shell of a class.

class PhoneNumber
end

Our first test

A big part of the art of model testing is coming up with various scenarios and deciding how our code should behave under those scenarios.

The first scenario we’ll test here is: “When we have a phone number where the digits are separated by dashes, the dashes should all get stripped out.”

We’ll add a context block that describes our scenario and an it block that describes our expected behavior. Inside the it block, we’ll create our test data (an instance of PhoneNumber) and an expectation: the expectation that we’ll get a dashless version of our original dashed phone number.

require_relative './phone_number.rb'

RSpec.describe PhoneNumber do
  context "phone number contains dashes" do
    it "strips out the dashes" do
      phone_number = PhoneNumber.new("555-856-8075")

      expect(phone_number.value).to eq("5558568075")
    end
  end
end

We haven’t yet defined an initializer for PhoneNumber, so passing a phone number to PhoneNumber when we instantiate an object couldn’t possibly work, but let’s defer worrying about that to the future. We don’t want to try to think about too many things at once. For now all we care about is that our test is constructed properly.

Let’s run the spec.

$ rspec phone_number_spec.rb

Very unsurprisingly, we can an error.

ArgumentError: wrong number of arguments (given 1, expected 0)

Let’s write just enough code to make this error go away and nothing else.

class PhoneNumber
  def initialize(value)
  end
end

When we run the test again we get a new error:

NoMethodError: undefined method 'value' for #<PhoneNumber:0x00007f8541abcb60>

Indeed, when we do expect(phone_number.value).to eq("5558568075"), we’re sending a message (value) to which PhoneNumber doesn’t respond. Let’s fix this error by making PhoneNumber respond to value. The simplest way we can do this is by adding an attr_reader.

class PhoneNumber
  attr_reader :value

  def initialize(value)
    @value = value
  end
end

Now we get yet a different error when we run the spec.


Failures:

  1) PhoneNumber phone number contains dashes strips out the dashes
     Failure/Error: expect(phone_number.value).to eq("5558568075")
     
       expected: "5558568075"
            got: "555-856-8075"

At this point it finally feels like we’re getting somewhere. The error feels like something we can work with. Instead of getting the expected value of 5558568075, we’re getting 555-856-8075.

Let’s write the code that fixes this error—and actually makes the test pass.

class PhoneNumber
  attr_reader :value

  def initialize(value)
    @value = value.gsub(/-/, "")
  end
end

Now when we run the test it passes.

The other two formats

Of our three scenarios, we have so far covered just the first one. Let’s address the next two.

555-856-8075
(555) 856-8075
+1 555 856 8075

We can add a second context block to our test, very similar to our first context block.

require_relative "./phone_number.rb"

RSpec.describe PhoneNumber do
  context "phone number contains dashes" do
    it "strips out the dashes" do
      phone_number = PhoneNumber.new("555-856-8075")

      expect(phone_number.value).to eq("5558568075")
    end
  end

  context "phone number contains parentheses" do
    it "strips out the non-numeric characters" do
      phone_number = PhoneNumber.new("(555) 856-8075")

      expect(phone_number.value).to eq("5558568075")
    end
  end
end

When we run this, it fails. We get:

1) PhoneNumber phone number contains parentheses strips out the dashes
     Failure/Error: expect(phone_number.value).to eq("5558568075")
     
       expected: "5558568075"
            got: "(555) 8568075"
     
       (compared using ==)

This makes sense because we’re of cours not doing anything yet to strip out parentheses or spaces. Let’s add parentheses and spaces to our regex.

class PhoneNumber
  attr_reader :value

  def initialize(value)
    @value = value.gsub(/[- ()]/, "")
  end
end

Now, when we run the test, it passes.

Refactoring

And in fact, we can use the safety net of this test to do a little refactoring. Our regular expression is a little bit more complicated than it needs to be. We can change it to just \D, which means “anything that’s not a number”.

class PhoneNumber
  attr_reader :value

  def initialize(value)
    @value = value.gsub(/\D/, "")
  end
end

The last scenario

The final scenario we want to address is when the phone number has a country code. Again, we can add a context block that’s similar to the other two context blocks.

require_relative "./phone_number.rb"

RSpec.describe PhoneNumber do
  context "phone number contains dashes" do
    it "strips out the dashes" do
      phone_number = PhoneNumber.new("555-856-8075")

      expect(phone_number.value).to eq("5558568075")
    end
  end

  context "phone number contains parentheses" do
    it "strips out the non-numeric characters" do
      phone_number = PhoneNumber.new("(555) 856-8075")

      expect(phone_number.value).to eq("5558568075")
    end
  end

  context "phone number contains country code" do
    it "strips out the country code" do
      phone_number = PhoneNumber.new("+1 555 856 8075")

      expect(phone_number.value).to eq("5558568075")
    end
  end
end

When we run this, it fails. The plus sign and spaces get taken care of but not the 1.

1) PhoneNumber phone number contains country code strips out the country code
     Failure/Error: expect(phone_number.value).to eq("5558568075")
     
       expected: "5558568075"
            got: "15558568075"
     
       (compared using ==)

To get rid of the 1, we can just grab everything that’s not the 1, i.e. the last 10 digits of the phone number.

class PhoneNumber
  attr_reader :value

  def initialize(value)
    @value = value.gsub(/\D/, "").split("").last(10).join
  end
end

The 10 is a “magic number” though, which is bad, so let’s assign it to a constant instead of letting it be mysterious.

class PhoneNumber
  attr_reader :value
  EXPECTED_NUMBER_OF_DIGITS = 10

  def initialize(value)
    @value = value.gsub(/\D/, "")
      .split("")
      .last(EXPECTED_NUMBER_OF_DIGITS)
      .join
  end
end

Now all our tests pass and our code is pretty good as well.

Takeaways

  1. A Rails model is an abstraction that represents a small corner of reality in a simplified way.
  2. Because models are so much more of an open-ended concept than certain other components of Rails (e.g. controllers), writing model specs can often be a more ambiguous task for beginners.
  3. One good way to write model specs is to list the specific fine-grained behaviors that you want to exist and to write test cases for each of those behaviors.
  4. Writing tests before we write the application code can make the process of writing the application code easier.

What’s next

In Part Two of the tutorial, we’ll go through the same process we did in Part One, but with a real Active Record model instead of just a plain old Ruby object.

Continue to Part Two

A beginner-friendly Rails model spec tutorial

What this is

Using you’re using RSpec to write Rails tests, there are several different types of tests you can write. There are system specs, request specs, view specs, model specs, and a number of others.

Among all these types of tests, I consider model specs to be perhaps the most important. Models are where I put the “meat” of my application’s code. Since model code is my most important code, model specs are my most important specs.

If haven’t done much model testing before and you aren’t sure how to come at it, this tutorial will show you how to take an idea for a feature and translate it into model specs.

Who this is for

This is a very beginner-level tutorial, intended for developers with very little model testing experience.

If you’ve never done any testing at all before, I would first recommend my Rails testing “hello world” tutorial and my Getting Started with Rails Testing articles.

But if you have a little bit of familiarity with RSpec and want to understand how to get started with testing models, let’s get started with Part One of this two-part tutorial.

Continue to Part One

How to run RSpec with headless Chrome/Chromium on Alpine Linux

Why Alpine Linux?

When you Dockerize a Rails application, you have to choose which distribution of Linux you want your container to use.

When I first started Dockerizing my applications I used Ubuntu for my containers because that was the distro I was familiar with. Unfortunately I discovered that using Ubuntu results in slow builds, slow running of commands, and large image sizes.

I discovered that Alpine Linux is popular for Docker containers because it affords better performance and smaller images.

Alpine + Capybara problems

Alpine had its own drawback though: I couldn’t run my tests because it wasn’t as straightforward in Alpine to get a Capybara + ChromeDriver configuration working on Alpine.

The evident reason for this is that Alpine can’t run a normal Chrome package the way Ubuntu can. Instead, it’s typical on Alpine to use Chromium, which doesn’t quite play nice with Capybara the way Chrome does.

How to get Alpine + Capyabara working

There are three steps to getting Capybara working on Alpine.

  1. Use selenium-webdriver instead of webdrivers
  2. Install chromium, chromium-chromedriver and selenium on your Docker image
  3. Configure a Capybara driver

Use selenium-webdriver instead of webdrivers

The first step is very simple: if you happen to be using the webdrivers gem in your Gemfile, replace it with selenium-webdriver.

Install chromium, chromium-chromedriver and selenium on your Docker image

The next step is to alter your Dockerfile so that chromium, chromium-chromedriver and selenium are installed.

Below is a Dockerfile from one of my projects (which is based on Mike Rogers’ fantastic Docker-Rails template).

I’ll call out the relevant bits of the file.

chromium chromium-chromedriver python3 python3-dev py3-pip

This line, as you can see, installs chromium and chromium-chromedriver. It also installs pip3 and its dependencies because we need pip3 in order to install Selenium. (If you don’t know, pip3 is a Python package manager.)

Here’s the line that installs Selenium:

RUN pip3 install -U selenium

And here’s the full Dockerfile.

FROM ruby:2.7.2-alpine AS builder

RUN apk add --no-cache \
    build-base libffi-dev \
    nodejs yarn tzdata \
    postgresql-dev postgresql-client zlib-dev libxml2-dev libxslt-dev readline-dev bash \
    #
    # For testing
    chromium chromium-chromedriver python3 python3-dev py3-pip \
    #
    # Nice-to-haves
    git vim \
    #
    # Fixes watch file issues with things like HMR
    libnotify-dev

RUN pip3 install -U selenium

FROM builder AS development

# Add the current apps files into docker image
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# Install any extra dependencies via Aptfile - These are installed on Heroku
# COPY Aptfile /usr/src/app/Aptfile
# RUN apk add --update $(cat /usr/src/app/Aptfile | xargs)

ENV PATH /usr/src/app/bin:$PATH

# Install latest bundler
RUN bundle config --global silence_root_warning 1

EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0", "-p", "3000"]

FROM development AS production

COPY Gemfile /usr/src/app
COPY .ruby-version /usr/src/app
COPY Gemfile.lock /usr/src/app

COPY package.json /usr/src/app
COPY yarn.lock /usr/src/app

# Install Ruby Gems
RUN bundle config set deployment 'true'
RUN bundle config set without 'development:test'
RUN bundle check || bundle install --jobs=$(nproc)

# Install Yarn Libraries
RUN yarn install --check-files

# Copy the rest of the app
COPY . /usr/src/app

# Precompile the assets
RUN RAILS_SERVE_STATIC_FILES=enabled SECRET_KEY_BASE=secret-key-base RAILS_ENV=production RACK_ENV=production NODE_ENV=production bundle exec rake assets:precompile

# Precompile Bootsnap
run RAILS_SERVE_STATIC_FILES=enabled SECRET_KEY_BASE=secret-key-base RAILS_ENV=production RACK_ENV=production NODE_ENV=production bundle exec bootsnap precompile --gemfile app/ lib/

The next step is to configure a Capybara driver.

Configure a Capybara driver

Below is the Capybara configuration that I use for one of my projects. This configuration is actually identical to the config I used before I started using Docker, so there’s nothing special here related to Alpine Linux, but the Alpine Linux configuration described in this post won’t work without something like this.

# spec/support/chrome.rb

driver = :selenium_chrome_headless

Capybara.server = :puma, {Silent: true}

Capybara.register_driver driver do |app|
  options = ::Selenium::WebDriver::Chrome::Options.new

  options.add_argument("--headless")
  options.add_argument("--no-sandbox")
  options.add_argument("--disable-dev-shm-usage")
  options.add_argument("--window-size=1400,1400")

  Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
end

Capybara.javascript_driver = driver

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by driver
  end
end

Remember to make sure that the line in spec/rails_helper that includes files from spec/support is uncommented so this file gets loaded.

What are the different kinds of Rails tests and when should I use each?

When starting out with Rails testing, it’s hard to know where to start.

First, there’s the decision of which framework to use. Then, if you’ve chosen RSpec (which most people do), you’re presented with a bewildering set of possible test types to use.

In this post I’ll show you what types of tests there are. I’ll show you which ones you should use and which ones you can ignore. Since most commercial Rails projects use RSpec, I’m going to focus on the eight types of tests that the RSpec library offers. (Although if I were to use Minitest, my strategy regarding test types would be pretty much the same.)

The eight types of RSpec specs

The RSpec library offers a lot of different spec types.

  • Model specs
  • System specs/feature specs*
  • Request specs/controller specs*
  • Helper specs
  • View specs
  • Routing specs
  • Mailer specs
  • Job specs

There are two lines with asterisks. These are cases where the RSpec team decreed one spec type obsolete and replaced it with a new type. I’m only including those ones for completeness.

So the up-to-date list is really the following.

  • Model specs
  • System specs
  • Request specs
  • Helper specs
  • View specs
  • Routing specs
  • Mailer specs
  • Job specs

Here’s when I use each.

  • Model specs – always
  • System specs – always
  • Request specs – rarely
  • Helper specs – rarely
  • View specs – never
  • Routing specs – never
  • Mailer specs – never
  • Job specs – never

Let’s talk about each of these spec types in detail. I’ll explain why I use the ones I use and why I ignore the ones I ignore.

Spec types I always use

Believe it or not, the overwhelming majority of the Rails tests I write make use of just two of the eight different spec types offered by RSpec. You might think that this would leave large gaps in my test coverage but it doesn’t. My test coverage is consistently above 95%.

System specs

System specs are “high-level” tests that simulate a user’s keystrokes and mouse clicks. System specs literally open up a browser window (although perhaps an invisible browser window if the tests are run “headlessly”) and use certain tools to manipulate the browser to exercise your application through simulated user input.

The reason I find system specs so valuable is that they test my whole stack, not just a slice of it, and they test my application in the same exact way that a real user will be using it. System specs are the only type of test that give me confidence my whole application really works.

I write so many system specs that I’ve developed a repeatable formula for adding system specs to any new CRUD feature.

Model specs

Even though system specs are indispensable, they’re not without drawbacks. System specs are somewhat “heavy”. They’re often more work to write and more expensive to run than other types of tests. For this reason I like to cover my features with a small number of coarse-grained system specs and a comparatively large number of fine-grained model specs.

As the name implies, model specs are for testing models. I tend to only bring model specs into the picture once a model has reached a certain level of “maturity”. At the beginning of a model’s life, it might have all its needs covered by built-in Rails functionality and not need any methods of its own. Some people write tests for things like associations and validations but I don’t because I find those types of tests to be pointless.

I use model specs to test my models’ methods. When I do so, I tend to use a test-first approach and write a failing test before I add a new line of code so that I’m sure every bit of code in my model is covered by a test.

Spec types I rarely use

Request specs

Request specs are more or less a way to test controller actions in isolation. I tend not to use request specs much because in most cases they would be redundant to my system specs. If I have system specs covering all my features, then of course a broken controller would fail one or more of my tests, making tests specifically for my controllers unnecessary.

I also try to keep my controllers sufficiently simple as to not call for tests of their own.

There are just three scenarios in which I do use request specs. First: If I’m working on a legacy project with fat controllers, sometimes I’ll use request specs to help me harness and refactor all that controller code. Second: If I’m working on an API-only Rails app, then system specs are physically impossible and I drop down to request specs instead. Lastly, if it’s just too awkward or expensive to use a system spec in a certain case then I’ll use a request spec instead. I write more about my reasoning here.

Helper specs

The reason I rarely write helper specs is simple: I rarely write helpers.

Spec types I never use

View specs and routing specs

I find view specs and routing specs to be redundant to system specs. If something is wrong with one of my views or routes, it’s highly likely that one of my system specs will catch the problem.

Mailer specs and job specs

I don’t write mailer specs or job specs because I try very hard to make all my mailers and background jobs one-liners (or close). I don’t think mailers and background jobs should do things, I think they should only call things. This is because mailers and background jobs are mechanical devices, not code organization devices.

To test my mailers and background jobs, I put their code into a PORO model and write tests for that PORO.

Takeaways

RSpec offers a lot of different spec types but you can typically meet 98% of your needs with just system specs and model specs.

If you’re a total beginner, I’d suggest starting with system specs.

Which test framework should I learn, RSpec or Minitest?

A common Rails testing question is which testing framework to use. RSpec and Minitest are the two that most people are deciding between. To many beginners it’s not clear which is the better choice.

We could weigh the technical pros and cons of each framework. Many people find things to love and hate about both RSpec and Minitest. You can find some passionate flame wars online if you look.

But before we get into all that, there are some realities to consider that overshadow the relative technical merits of the two frameworks. There are two particular facts we should think about.

Fact #1: usually, someone else decides for you

Most of us don’t have much choice as to whether to use RSpec or Minitest at work.

At some point we’ll get a job. At that job they’ll either use RSpec there or Minitest (or something else or nothing at all). Whatever they use at work, that’s what we’ll be using. Our personal preferences are moot.

Fact #2: usually, they’ve chosen RSpec

For better or worse, it’s my experience and the experience of most Rails developers I’ve talked with that most commercial projects use RSpec. (Note how I said most commerical projects. Most commercial projects use RSpec and most OSS Ruby projects, in my experience, use Minitest. I do not know why this is the way it is.)

Out of curiosity I did a (totally unscientific) poll regarding which test framework they use at work. Take it with a grain of salt, but here are the results.

Even if my numbers are off by quite a bit, RSpec is still the more popular framework.

What does this mean?

My take is that this means if your goal is to get a Rails job, learning RSpec over Minitest will give you a higher probability that your skills match the tech stack that’s used at any particular company.

Some people may object to this way of looking at it. They might argue that if you always you go with whatever’s most popular instead of what’s the best technical choice, you may end up using a Windows laptop or switching from Rails to Node.js.

This argument is flawed though. We’re free to make our own choices on the big things but we can’t dictate what comes along with those choices. We can choose to use Rails instead of a different framework, but we can’t reasonably say that we’re only going to work on Rails projects that use, for example, Minitest and MySQL and Angular and no other combination of technologies. We have to compromise a little or face extremely limited job options.

Also, it doesn’t matter much

Having said all that, I actually don’t believe your choice of which test framework to learn matters!

RSpec and Minitest differ syntactically but they don’t really have meaningful conceptual differences. The principles of testing are the same regardless of which test framework you’re using, or even which language you’re using for that matter.

You’re very unlikely to become an expert in Minitest and then get turned down for a job because they use RSpec there, or vice versa. Employers typically realize that if someone is skilled with testing, they’ll be able to pick up any test framework relatively easily.

So try both

In a sense it might sound depressing that the answer to the RSpec/Minitest question is a) we don’t have a choice and b) it doesn’t matter anyway. I actually find these facts freeing.

If the choice between RSpec and Minitest doesn’t matter that much then we’re free to evaluate both according to our own independent judgment and taste and not worry about whether we’re making the “right” choice. Whatever we choose, we’re likely to develop skills that will apply to any job, whether they use Minitest or RSpec there.

So my advice is to try both frameworks and see which one you like better. Neither one is objectively superior to the other.

But if you just want me to pick for you, I say RSpec

My very simplistic logic is that RSpec is what you’ll most likely be forced to use at work, so that’s what you might as well learn.

But again, I encourage you to try both and decide for yourself. This is ultimately not a very important decision. Learning testing principles is much more important than learning testing frameworks.

How I set up a Rails application for testing

Below is how I set up a fresh Rails application for testing. I’ll describe it in three parts:

  1. An application template that can add all the necessary gems and configuration
  2. My setup process (commands I run to create a new Rails app)
  3. A breakdown of the gems I use

Let’s start with the application template.

My application template

First, if you don’t know, it’s possible to create a file called an application template that you can use to create a Rails application with certain code or configuration included. This is useful if you create a lot of new Rails applications with parts in common.

Here’s an application template I created that will do two things: 1) install a handful of testing-related gems and 2) add a config file that will tell RSpec not to generate certain types of files. A more detailed explanation can be found below the code.

gem_group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'capybara'
  gem 'webdrivers'
  gem 'faker'
end

initializer 'generators.rb', <<-CODE
  Rails.application.config.generators do |g|
    g.test_framework :rspec,
      fixtures:         false,
      view_specs:       false,
      helper_specs:     false,
      routing_specs:    false,
      request_specs:    false,
      controller_specs: false
  end
CODE

The first chunk of code will add a certain set of gems to my Gemfile. A more detailed explanation of these gems is below.

The second chunk of code creates a file at config/initializers/generators.rb. The code in the file says “when a scaffold is generated, don’t generate files for fixtures, view specs, helper specs, routing specs, request specs or controller specs”. There are certain kinds of tests I tend not to write and I don’t want to clutter up my codebase with a bunch of empty files. That’s not to say I never write any of these types of tests, just sufficiently rarely that it makes more sense for me to create files manually in those cases than for me to allow files to get generated every single time I generate a scaffold.

The setup process

When I run rails new, I always use the -T flag for “skip test files” because I always use RSpec instead of the Minitest that Rails comes with by default.

Also, incidentally, I always use PostgreSQL. This choice of course has little to do with testing but I’m including it for completeness.

In this particular case I’m also using the -m flag so I can pass in my application template. Application templates can be specified using either a local file path or a URL. In this case I’m using a URL so that you can just copy and paste my full rails new command as-is if you want to.

$ rails new my_project -T -d postgresql \
  -m https://raw.githubusercontent.com/jasonswett/testing_application_template/master/application_template.rb

Once I’ve created my project, I add it to version control. (I could have configured my application template to do this step manually, but I wanted to explicitly show it as a separate step, partially to keep the application template clean and easily understandable.)

$ git add .
$ git commit -a -m'Initial commit'

The gems

Here’s an explanation of each gem I chose to add to my project.

rspec-rails

RSpec is one of the two most popular test frameworks for Rails, the other being Minitest.

The rspec-rails gem is the version of the RSpec gem that’s specifically fitted to Rails.

factory_bot_rails

Factory Bot is a tool for generating test data. Most Rails projects that use RSpec also use Factory Bot.

Like rspec-rails, factory_bot_rails is a Rails-specific version of a more general gem, factory_bot.

capybara

Capybara is a tool for writing acceptance tests, i.e. tests that interact with the browser and simulate clicks and keystrokes.

The underlying tool that allows us to simulate user input in the browser is called Selenium. Capybara allows us to control Selenium using Ruby.

webdrivers

In order for Selenium to work with a browser, Selenium needs drivers. There are drivers for Chrome, drivers for Edge, etc. Unfortunately it can be somewhat tedious to keep the drivers up to date. The webdrivers gem helps with this.

faker

By default, Factory Bot (the tool for generating test data) will give us factories that look something like this:

FactoryBot.define do
  factory :customer do
    first_name { "MyString" }
    last_name { "MyString" }
    email { "MyString" }
  end
end

This is fine for just one record but becomes a problem if we have multiple records plus a unique constraint. If in this example we require each customer to have a unique email address, then we’ll get a database error when we create two customer records because the email address of MyString will be a duplicate.

One possible solution to this problem is to replace the instances of "MyString" with something like SecureRandom.hex. I don’t like this, though, because I often find it helpful if my test values resemble the kinds of values they’re standing in for. With Faker, I can do something like this:

FactoryBot.define do
  factory :customer do
    first_name { Faker::Name.first_name }
    last_name { Faker::Name.last_name }
    email { Faker::Internet.email }
  end
end

This can make test problems easier to troubleshoot than when test values are simply random strings like c1f83cef2d1f74f77b88c9740cfb3c1e.

Honorable mention

I also often end up adding the VCR and WebMock gems when I need to test functionality that makes external network requests. But in general I don’t believe in adding code or libraries speculatively. I only add something once I’m sure I need it. So I typically don’t include VCR or WebMock in a project from the very beginning.

Next steps

After I initialize my Rails app, I usually create a walking skeleton by deploying my application to a production and staging environment and adding one small feature, for example the ability to sign in. Building the sign-in feature will prompt me to write my first tests. By working in this way I front-load all the difficult and mysterious work of the project’s early life so that from that point on, my development work is mostly just incremental.

If you’re brand new to Rails testing and would like to see an example of how I would actually write a test once I have the above application set up, I might recommend my Rails testing “hello world” post.

The difference between let, let! and instance variables in RSpec

The purpose of let and the differences between let and instance variables

RSpec’s let helper method is a way of defining values that are used in tests. Below is a typical example.

require 'rspec'

RSpec.describe User do
  let(:user) { User.new }

  it 'does not have an id when first instantiated' do
    expect(user.id).to be nil
  end
end

Another common way of setting values is to use instance variables in a before block like in the following example.

require 'rspec'

RSpec.describe User do
  before { @user = User.new }

  it 'does not have an id when first instantiated' do
    expect(@user.id).to be nil
  end
end

There are some differences between the let approach and the instance variable approach, with one in particular that’s quite significant.

Differences between let and instance variables

First, there’s the stylistic difference. The syntax is of course a little different between the two approaches. Instance variables are of course prefixed with @. Some people might prefer one syntax over the other. I personally find the let syntax ever so slightly tidier.

There are also a couple mechanical differences. Because of how instance variables work in Ruby, you can use an undefined instance variable and Ruby won’t complain. This presents a slight danger. You could for example accidentally pass some undefined instance variable to a method, meaning you’d really be passing nil as the argument. This means you might be testing something other than the behavior you meant to test. This danger is admittedly remote though. Nonetheless, the let helper defined not an instance variable but a new method (specifically, a memoized method—we’ll see more on this shortly), meaning that if you typo your method’s name, Ruby will most certainly complain, which is of course good.

The other mechanical difference is that let can create values that get evaluated lazily. I personally find this to be a dangerous and bad idea, which I’ll explain below, but it is a capability that the helper offers.

Perhaps the most important difference between let and instance variables is that instance variables, when set in a before block, can leak from one file to another. If for example an instance variable called @customer is set in “File A”, then “File B” can reference @customer and get the value that was set in File A. Obviously this is bad because we want our tests to be completely deterministic and independent of one another.

How let works and the difference between let and let!

How let works

I used to assume that let simply defines a new variable for me to use. Upon closer inspection, I learned that let is a method that returns a method. More specifically, let returns a memoized method, a method that only gets run once.

Since that’s perhaps kind of mind-bending, let’s take a closer look at what exactly this means.

An example method

Consider this method that 1) prints something and then 2) returns a value.

def my_name
  puts 'thinking about what my name is...'
  'Jason Swett'
end

puts my_name

When we run puts my_name, we see the string that gets printed (puts 'thinking about what my name is...') followed by the value that gets returned by the method (Jason Swett).

$ ruby my_name.rb
thinking about what my name is...
Jason Swett

Now let’s take a look at some let syntax that will create the same method.

require 'rspec'

describe 'my_name' do
  let(:my_name) do
    puts 'thinking about what my name is...'
    'Jason Swett'
  end

  it 'returns my name' do
    puts my_name
  end
end

When we run this test file and invoke the my_name method, the same exact thing happens: the method `puts`es some text and returns my name.

$ rspec my_name_spec.rb
thinking about what my name is...
Jason Swett
.

Finished in 0.00193 seconds (files took 0.08757 seconds to load)
1 example, 0 failures

Just to make it blatantly obvious and to prove that my_name is indeed a method call and not a variable reference, here’s a version of this file with parentheses after the method call.

require 'rspec'

describe 'my_name' do
  let(:my_name) do
    puts 'thinking about what my name is...'
    'Jason Swett'
  end

  it 'returns my name' do
    puts my_name() # this explicitly shows that my_name() is a method call
  end
end

Memoization

Here’s a version of the test that calls my_name twice. Even though the method gets called twice, it only actually gets evaluated once.

require 'rspec'

describe 'my_name' do
  let(:my_name) do
    puts 'thinking about what my name is...'
    'Jason Swett'
  end

  it 'returns my name' do
    puts my_name
    puts my_name
  end
end

If we run this test, we can see that the return value of my_name gets printed twice and the thinking about what my name is... part only gets printed once.

$ rspec my_name_spec.rb
thinking about what my name is...
Jason Swett
Jason Swett
.

Finished in 0.002 seconds (files took 0.08838 seconds to load)
1 example, 0 failures

The lazy evaluation of let vs. the immediate evaluation of let!

When we use let, the code inside our block gets evaluated lazily. In other words, none of the code inside the block gets evaluated until we actually call the method created by our let block.

Take a look at the following example.

require 'rspec'

describe 'let' do
  let(:message) do
    puts 'let block is running'
    'VALUE'
  end

  it 'does stuff' do
    puts 'start of example'
    puts message
    puts 'end of example'
  end
end

When we run this, we’ll see start of example first because the code inside our let block doesn’t get evaluated until we call the message method.

$ rspec let_example_spec.rb
start of example
let block is running
VALUE
end of example
.

Finished in 0.00233 seconds (files took 0.09836 seconds to load)
1 example, 0 failures

The “bang” version of let, let!, evaluates the contents of our block immediately, without waiting for the method to get called.

require 'rspec'

describe 'let!' do
  let!(:message) do
    puts 'let block is running'
    'VALUE'
  end

  it 'does stuff' do
    puts 'start of example'
    puts message
    puts 'end of example'
  end
end

When we run this version, we see let block is running appearing before start of example.

$ rspec let_example_spec.rb 
let block is running
start of example
VALUE
end of example
.

Finished in 0.00224 seconds (files took 0.09131 seconds to load)
1 example, 0 failures

I always use let! instead of let. I’ve never encountered a situation where the lazily-evaluated version would be helpful but I have encountered situations where the lazily-evaluated version would be subtly confusing (e.g. a let block is saving a record to the database but it’s not abundantly clear exactly at what point in the execution sequence the record gets saved). Perhaps there’s some performance benefit to allowing the lazy evaluation but in most cases it’s probably negligible. Confusion is often more expensive than slowness anyway.

Takeaways

  • The biggest advantage to using let over instance variables is that instance variables can leak from test to test, which isn’t true of let.
  • The difference between let and let! is that the former is lazily evaluated while the latter is immediately evaluated.
  • I always use the let! version because I find the execution path to be more easily understandable.

The difference between system specs and feature specs

If you’re like me, you might have found the difference between RSpec’s “feature specs” and “system specs” to be a little too nuanced to be easily understandable. Here’s my explanation of the difference between the two.

Two levels of Rails tests

For some background, I want to talk about the main two types of Rails tests that I use, and where feature specs, the predecessor to system specs, come intro the picture.

I think of Rails tests as existing on two basic levels.

High-level, course-grained

One level of tests is at a high level. These tests simulate a user visiting pages, filling in forms, clicking links and buttons, etc. Different people use different terms for these tests including “integration test”, “end-to-end test” and “acceptance test”.

Because these types of tests are often expensive to write and run, they tend not to cover every nook and cranny of the application’s behavior.

In RSpec terminology, these types of tests have historically been called feature specs.

Low-level, fine-grained

The other level of tests is at a lower level. There are all kinds of tests you could possibly write with Rails/RSpec (model specs, request specs, view specs, helper specs) but I tend to skip most of those in most scenarios and only write model specs.

From feature spec to system spec

The backstory

When Rails 5.1 came out in April 2017, one of the features it introduced was system tests. Here’s why this was done.

By default Rails ships with Minitest. The inclusion of Minitest provides a way to write model tests and certain other kinds of tests, but it historically hasn’t provided a way to write full-blown end-to-end without doing some extra work yourself, like bringing Capybara and Database Cleaner into the picture. This is the rationale for Rails 5.1’s addition of system tests, in my understanding. (To be clear, we’re still talking about system tests, not system specs.)

System specs wrap system tests

According to the RSpec docs, “System specs are RSpec’s wrapper around Rails’ own system tests.” This means that it’s no longer required to explicitly include the Capybara gem, and because system tests are already run inside a transaction, you don’t need Database Cleaner.

Summary

System specs are a wrapper around Rails’ system tests. The benefits of using system specs instead of feature specs is that you don’t have to explicitly include the Capybara gem, nor do you have to use Database Cleaner.