Category Archives: Ruby on Rails

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 I code without service objects

What service objects are

Service objects and Interactors are fashionable among Rails developers today. I can’t speak for everyone’s motives but the impression I get is that service objects are seen as a way to avoid the “fat models” you’re left with if you follow the “skinny controllers, fat models” guideline. If your models are too fat you can move some of their bulk to some service objects to thin them out.

Unfortunately it’s not quite possible to say what service objects are because the term “service object” means different things to different people. To some developers, “service object” is just a synonym for plain old Ruby object (PORO). To other developers, a service object is more or less an implementation of the command pattern. Interactors are similarly close to the command pattern.

For the purposes of this post we’ll go with the “command pattern” conception of a service object: a Ruby class with a single method, call, which carries out a sequence of steps.

I have some gripes with the way service objects are advocated in the Ruby community. I’ll explain why.

Pattern vs. paradigm

The command pattern has its time and place. Sometimes it makes plenty of sense to bundle up an action into a single object and let that object orchestrate the steps that need to happen for that action.

But the command pattern is a pattern, not a paradigm. A software design pattern is a specific solution meant to meet a specific problem. A programming paradigm (like object-oriented programming or functional programming) is a style that can be applied at the level of entire applications.

What pains me a little bit to see is that many developers seem to have confused the idea of a pattern with the idea of a paradigm. They see service objects or Interactors and think, “This is how we’ll code now!” They have a service-object-shaped hammer and now everything is a nail.

Imperative vs. declarative

An imperative programming style expresses code mainly in terms of what to do. A declarative programming style expresses code mainly in terms of what things are.

For those not very familiar with imperative vs. declarative, here’s an explanation by way of example.

An imperative sandwich

If I were to tell a robot to make me a peanut butter and jelly sandwich using the imperative style, I would say, “Hey robot, get two pieces of bread, put some peanut butter on one, put some jelly on the other, put them together, put it on a plate, and give it to me.”

A declarative sandwich

If I were to do the same thing in a declarative style, I would say, “A sandwich is a food between two pieces of bread. It comes on a plate. A peanut butter and jelly sandwich is a sandwich that contains peanut butter and jelly. Make me one.”

My preference for declarative

I’m strongly of the view that declarative code is almost always easier to understand than imperative code. I think for the human brain it’s much easier to comprehend a static thing than a fleeting action.

At some point the things do have to take action (the peanut butter and jelly sandwich has to get made) but we can save the action part for the very highest level, the tip of the pyramid. Everything below can be declarative.

Service objects are all imperative

Service objects and Interactors are basically all imperative. Their names tend to be verbs, like NotifySlack or AuthenticateUser. Their guts tend to be imperative as well. To me this is a pity because, like I said above, I think most things are more clearly expressed in a declarative style. Even things that seem at first glance to lend themselves more naturally to an imperative style can often be expressed even more naturally in a declarative way.

Takeaways

  • The command pattern is a pattern, not a paradigm. It’s not a style, like OOP or FP, that should be applied to all your code.
  • Declarative code is often easier to understand than imperative code.
  • Regular old OOP is a fine way to program. Not everything in Rails has to utilize a fancy tool or concept.

What is a Rails model?

In the model-view-controller pattern that Rails is built on, it’s pretty clear what views and controllers are, but models are a little less clear. In my years with Rails I’ve had the opportunity to come across a number of different conceptions regarding what models are. These conceptions often overlap but often don’t share the same exact contours.

Our conception of what a model is is super important because it has a huge influence on how we structure our Rails applications, which in turn determines how easy and economical it’s going to be to maintain our applications.

Here’s how I think about what Rails models are.

How I conceive of Rails models

First of all, forget about Rails

I think one of the most helpful mindset shifts a Rails developer can make with regard to models is to think about models outside the concept of Rails, at least for a moment. Instead, let’s consider: What is a model in general? Why do we use models?

And in fact, not to get too crazy, but we can even think about models even outside the context of programming. Scientists and mathematicians use models as well. Why do people in these professions use models? If you asked a scientist what a model is, what would they say?

Non-programming models

According to Wikipedia, “Scientific modeling is a scientific activity, the aim of which is to make a particular part or feature of the world easier to understand, define, quantify, visualize, or simulate by referencing it to existing and usually commonly accepted knowledge.”

Let’s see if we can draw inspiration from this to describe models in programming.

My definition of a model

I might adapt the above definition of a scientific model to programming as follows: “Modeling is a programming activity, the aim of which is to make a particular part of a program easier to understand and work with by representing parts of reality in a simplified way that can be expressed in code.”

A model is not a simulation

It’s common for object-oriented programming tutorials to use examples like Dog and Cat each inheriting from Animal, and with methods like bark and meow. I find these kinds of examples misleading. These entities aren’t models, they’re simulations, and we programmers almost never build simulations.

Instead we create models with names more like User, PatientPayment or HashWithIndifferentAccess. These models are useful precisely because they break off a tiny part of the infinite hugeness of reality and provide us with a simplified abstraction we can work with, rather than having to deal with all of reality at once.

In programming we would never try to model an entire dog or cat. Rather, we try to model the tiny slices of reality related to dogs and cats that we care about. For example, for a pet store application, we might keep track of a dog’s name, the dog’s owner’s contact information, and other things like that, but never e.g. its skeletal structure or biological taxonomy. And we certainly wouldn’t create a method that invokes a bark (whatever that could even mean).

A model doesn’t need to have anything to do with Active Record

I used to think that Rails applications were supposed to have exactly one model per database table and that every model file was supposed to inherit from ActiveRecord::Base.

Now, with my broader conception of models, not only do I not think models have to inherit from Active Record, but most of my models don’t. Most of my models are just plain old Ruby objects (POROs). In fact, I don’t even see why models should have to take the form of objects at all, but since I’m personally a big OOP fan and so I almost always write my models in the form of objects.

A model doesn’t have to have anything to do with business/domain logic

Some developers define models as the part of the application that has to do with domain logic. I don’t see it this way.

To me, a model can model anything. A model can model domain logic (i.e. stuff that has to do with the peculiarities of the organization/industry the application serves) or it can model application logic (i.e. stuff that’s purely technical, and doesn’t particularly have anything to do with your specific domain).

A Rails app can be made almost entirely of models

I view Rails controllers as just very thin entry points that deal mostly with HTTP requests and responses. The real “meat” of the application is in its models. And I find that models, in the form of POROs, can account for 95%+ of my needs.

I don’t use Interactors or service objects or any other pseudo-patterns like that. In fact, it could be argued that Interactors and service objects are models, they’re just models that have been adulterated with some confused ideas and therefore are less effective at the job models are intended to do: to simplify and abstract reality to make reality easier to work with in code.

Takeaways

  • Modeling is an activity that aims to make a particular part of a program easier to understand and work with by representing parts of reality in a simplified way that can be expressed in code.
  • A model is not a simulation. Rather, it’s an abstraction.
  • A model doesn’t have to inherit from Active Record.
  • A model doesn’t have to specifically deal with domain/business logic. A model can deal with any aspect of reality, including imagined realities that only exist in the context of your program.
  • Rails apps can be made almost entirely of models. No Interactors or service objects are necessary.

Testing implementation vs. behavior in Rails

One of the biggest mistakes I see in Rails testing (especially model testing) is testing implementation instead of behavior.

Let’s say I run a factory that makes cars. I want to make sure that each car I make actually works. How should I go about making sure each car works?

Here are two possible methods.

Testing implementation

One way I could try to make sure each car works is to check to see if it has all the right stuff. I could check for the presence of an engine, an ignition, a brake system, wheels, and everything else that’s needed in order to get from point A to point B. If the car has all this stuff, then it’s all set.

Testing behavior

Another way I could try to make sure the car works is to start it up and try to drive it somewhere.

The first way is bad and the second way is good. Here’s why.

Why testing behavior is better

If I “verify” that my car works by checking for the presence of various parts, then I haven’t really actually verified anything. I haven’t demonstrated that the system under test (the car) actually meets spec (can drive).

If I test the car by actually driving it, then the questions of whether the car has various components become moot. If for example the car can travel down the road, we don’t need to ask if the car has wheels. If it didn’t have wheels it wouldn’t be moving.

All of our “implementation” questions can be translated into more meaningful “behavior” questions.

  • Does it have an ignition? -> Can it start up?
  • Does it have an engine and wheels? -> Can it drive?
  • Does it have brakes? -> Can it stop?

Lastly, behavior tests are better than implementation tests because behavior tests are more loosely coupled to the implementation. I ask “Can it start up?” instead of “Does it have an engine?” then I’m free to, for example, change my car factory from a gasoline-powered car factory to an electric car factory without having to change the set of tests that I perform. In other words, behavior tests enable refactoring.

Different granularities of behavior testing

I want to be sure that my argument isn’t misconstrued as a discussion between unit tests and integration tests. The implementation tests being performed weren’t actually verifying the proper behavior of the various components (engine, ignition, etc.) in isolation. The implementation tests were merely verifying the presence of components.

In fact, performing some “unit” tests and some “integration” tests in my car factory would probably be a good idea. It would be smart to make sure each part works individually before I combine them all. I’d just want to make sure my unit and integration tests were testing behavior, not implementation.

Verifying that my car can drive is a test at the highest granularity. At a lower level of granularity, I might want to verify that the engine works, even before the engine is connected to an actual car. To test my engine, I wouldn’t want to check to see whether the engine has six cylinders, each with a piston inside. Instead I would want to give my engine some fuel and actually see if it runs.

So please don’t confuse the implementation vs. behavior question with the unit test vs. integration test question. We sometimes want to perform unit tests and we sometimes want to perform integration tests, but we pretty much always want to test behavior instead of implementation.

How this relates to Rails

I sometimes come across examples of Rails model tests that I consider pointless. These tests verify the presence of validations, associations, callbacks and other implementation details. These tests are pointless because they don’t actually test anything. Having these tests is like checking for the presence of your engine and brakes rather than actually just driving your car.

It’s also pointless to write system specs that e.g. check for the presence of various form fields and buttons, for the same exact reason.

Takeaway

Test behavior, not implementation.

My hybrid approach to Dockerizing Rails applications

Why I started using Docker

I was a latecomer to the Docker craze. I simply didn’t have a problem that I left like Docker would solve for me.

Then, in 2020, a scenario arose for me that finally made me feel a need for Docker. My team at work was growing. I was finding that onboarding new developers was painful due to all the setup work we had to do on each new developer’s machine. I suspected that Docker could ease my pain.

The details of my Docker use case

First of all, I want to explain the exact use case that I used Docker for. There are two ways to Dockerize a Rails application. You can either Dockerize a Rails application to help with your development environment or your production infrastructure. My use case was to make my development environment easier.

I was tired of having each use have to install RVM, Ruby, RubyGems, PostgreSQL, Redis, etc. I wanted developers to be able to run a single command and have a complete development ready to go.

My good experiences with Docker

Regarding the objective of a complete development environment ready to go, I must say that Docker delivered on its promise. It took me a lot of difficult work but once I Dockerized my Rails application, it just worked. It was magical.

It was really helpful not to have to spend $HOURS to $DAYS getting a new developer set up with the development environment. It was nice not having to pair with people while we googled esoteric errors that only came up on their machine.

It was also really nice not to have to juggle services on my local machine. No more manually starting Rails, Sidekiq and Redis separately.

My bad experiences with Docker

Sadly, Docker’s benefits didn’t come without costs. Here were the downsides in rough descending order of severity.

Worse performance when running Rails commands

When a Rails app is Dockerized, you can no longer run commands like rails db:migrate because your local computer (the “host machine”) is not the one running Rails, your Docker container is. So you have to run docker-compose run web rails db:migrate.

Running Rails commands via Docker Compose took noticeably longer than running Rails commands straight on the host machine. These commands were so sluggish as to be intolerable.

Since tests are a big part of the workflow where I work, it was a real drag to have to pay this “performance tax” every time I wanted to run a test.

Worse performance when interacting with the app in the browser

Clicking around on stuff in the browser was slower as well. This issue wasn’t quite as bad as the command line issue but it was still bad enough to be a noticeable bummer.

No easy way to run tests non-headlessly

Most of the time I run my system specs headlessly. But it’s not uncommon for me to run across a difficult test which I need to run non-headlessly in order to see what’s going on with it.

Running tests non-headlessly directly on my host machine works fine. Trying to get my Docker container to open a browser for my was a nightmare though and I never did get it figured out.

No easy binding.pry

Since the docker-compose up command runs all services under the same parent process or whatever, you can’t just stick a binding.pry in your code and drop into the console inside the rails server process like you can if you’re not using Docker. To be fair, I understand there are ways around this, and I didn’t try very hard to solve this particular problem, so I might be griping about nothing with this one.

The hybrid Docker solution I developed instead

The solution I ultimately landed on was a hybrid approach. I decided to use Rails natively and Docker for Redis and PostgreSQL. That way I don’t have that impenetrable seal between me and my Rails application, but I still don’t have to manually install and run PostgreSQL and Redis.

This still leaves Sidekiq and webpack-dev-server. Luckily I found an easy fix for this. I just reverted to an old-fashioned solution, Foreman.

My Docker Compose config

Here’s what my docker-compose.yml looks like.

---
# Docker Compose 2.4 is for local development
# https://www.heroku.com/podcasts/codeish/57-discussing-docker-containers-and-kubernetes-with-a-docker-captain - Source on that.
version: '2.4'

services:
  postgres:
    image: postgres:13.1-alpine
    mem_limit: 256m
    volumes:
      - postgresql:/var/lib/postgresql/data:delegated
    ports:
      - "127.0.0.1:5432:5432"
    environment:
      PSQL_HISTFILE: /root/log/.psql_history
      POSTGRES_USER: mednote_development
      POSTGRES_PASSWORD: pgpassword
    restart: on-failure
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 2s
      retries: 10
    logging:
      driver: none

  redis:
    image: redis:4.0.14-alpine
    mem_limit: 64m
    volumes:
      - redis:/data:delegated
    ports:
      - "127.0.0.1:6379:6379"
    restart: on-failure
    logging:
      driver: none

volumes:
  postgresql:
  redis:
  storage:

That’s all I need for the Docker portion.

My Procfile

docker:    docker-compose up
web:       bundle exec puma -p 3000
worker:    bundle exec sidekiq
webpacker: bundle exec bin/webpack-dev-server

I can run all these things by doing foreman start -f Procfile.dev. Then, in a separate terminal window (or rather, a separate tmux pane) I run rails server so I can still use binding.pry tidily.

The trade-offs

The downsides of this approach are that each developer will still have to install RVM, Ruby, and RubyGems manually. But to me that’s much less of a downside than the downsides I experienced when my app was “fully” Dockerized.

Takeaways

  • Docker can be a great help in making it easier for each developer to get set up with a development environment.
  • Dockerizing an app “fully” can unfortunately come with some side effects that you may find unacceptable.
  • Using a hybrid approach can provide a sensible balance of the areas where using Docker is better versus the areas where going outside of Docker is better.

The two ways to Dockerize a Rails application

Here’s something that should be Docker 101, but for some reason people don’t really tell you: there’s no such thing as just “Dockerizing” a Rails app. There are two use cases for using Docker with Rails. Before you go down the path of Dockerizing your app, you have to pick which use case you’re after. The implementation for each is very different from the other.

You can Dockerize an app for development or you can Dockerize it for production.

Dockerizing for development

The reason you would want to Dockerize an app for development is to make it easier for a new developer to get a development environment set up on their machine.

When you have your app Dockerized for development, Docker can install and run services for you. For example, if your app uses PostgreSQL, Redis, webpack-dev-server, and Sidekiq, Docker will run all those processes for you, and before that Docker will install PostgreSQL and Redis if you don’t already have them.

This means that each new developer who starts the project doesn’t have to go through a potentially long and painful process of installing all the dependencies for your app before getting to work. They can just clone the repo, run a single command, and be up and running.

Dockerizing for production

The benefits of Dockerizing for production overlap with Dockerizing for development, but they’re not exactly the same.

In my development example I mentioned that you might have certain services running locally like PostgreSQL, Redis, webpack-dev-server, and Sidekiq. In a production environment, you of course don’t have all these processes running on a single machine or VM. Rather, you might have (if you’re using AWS for example), your database hosted on RDS, a Redis instance on ElasticCache, Sidekiq running on a worker EC2 instance, and no webpack-dev-server because that doesn’t apply in production. So the need is very different.

So unlike a development use case, a production Docker use case doesn’t include installing and running various services for you, because running all kinds of services on a host machine is not how a “modern” production infrastructure configuration works.

Takeaways

  • There are two ways to Dockerize a Rails app: for development and for production.
  • Dockerizing for development makes it easier to spin up a development environment.
  • Dockerizing for production helps avoid the problem of having snowflake servers.
  • Dockerizing for development could probably benefit just about any team, but Dockerizing for production is probably much more dependent on your app’s unique hosting needs.

What are all the Rails testing tools and how do I use them?

One of the most common questions for Rails developers new to testing is “What are all the Rails testing tools and how do I use them?”

I’ll explain what the major tools are but I want to preface it by saying that the most important thing to learn to be a successful tester is testing principles, not testing tools. If you think of testing like a taco, the tools are the tortilla and the principles are the stuff inside the taco. The tortilla is essential but it’s really only a vehicle.

The following are the tools I use for my testing.

RSpec

RSpec is a test framework. A test framework is what gives us a structure for writing our tests as well as the ability to run our tests.

There are other test frameworks but RSpec is the most popular one for commercial Rails projects. The second most popular test framework is Minitest.

Test frameworks differ syntactically but the testing principles and practices are going to be pretty much the same no matter what framework you’re using. (If you’re not sure whether you should learn RSpec or Minitest, I write about that here.)

Factory Bot

One of the challenges of Rails testing is generating test data. For example, if you’re writing a test that logs a user in and then takes some action, you’re going to have to create a user in the database at the beginning of the test. Many tests require much more complicated test data setup.

There are two common ways of generating test data in Rails tests: fixtures and factories.

Fixtures

Fixtures typically take the form of one or more YAML files with some hard-coded data. The data is translated into database records one time, before any of the tests are run, and then deleted afterward. (This happens in a separate test database instance of course.)

Factories

With factories, database data is generated specifically for each test. Instead of loading all the data once at the beginning and deleting it at the end, data is inserted before each test case and then deleted before the next test case starts. (More precisely, the data isn’t deleted, but rather the test is run inside a database transaction and the data is never committed in the first place, but that’s a mechanical detail that’s not important right now.)

Relative merits of fixtures and factories

I tend to prefer factories because I like having my data generation right inside my test, close to where the test is happening. With fixtures the data setup is too distant from where the test happens.

In my experience, for whatever reason, most people who use RSpec use factories and most people who use Minitest use fixtures. If you’d like to learn more about factories and fixtures, I write more about it here.

Capybara

Some Rails tests only exercise Ruby code. Other tests actually open up a browser and simulate user clicks and keystrokes.

Simulating user input this way requires us to use some sort of tool to manipulate the browser. Capybara is a library that uses Ruby to wrap a driver (usually the Selenium driver), letting us simulate clicks and keystrokes using convenient Ruby methods.

For more examples of how to use Capybara, go here.

VCR and WebMock

One principle of testing is that tests should be deterministic, meaning they run the same way every time no matter what.

When an application’s behavior depends on external services (e.g. a third-party API like Stripe) it makes it harder to have deterministic tests. The tests can be made to fail by an internet connection failure or a temporary outage of the external service.

Tools like VCR and WebMock can help smooth out these challenges. VCR can let us run our tests against the real external service, but capture all the service’s responses in local files so that subsequent test runs don’t talk to the external service but rather just go off of the saved responses. That way, even if the internet connection fails or the service goes down, the tests still work.

WebMock is a tool that serves a similar purpose, although I usually use it in a more limited way. I don’t consider my test suite to be deterministic unless it doesn’t talk to the network at all, so I use WebMock to enforce that my test suite isn’t making any network requests.

Tools I don’t use

Cucumber is a somewhat popular tool when it comes to acceptance testing. It’s my view that Cucumber adds an extra layer of complexity and indirection without adding any value. Here are some details on why I don’t recommend Cucumber.

I also don’t use Shoulda matchers. Shoulda matchers make it easy and convenient to write certain kinds of tests, but the kinds of tests Shoulda helps you write are not a good kind of test to write in the first place. Shoulda helps you write tests that test your code’s implementation rather than its behavior. Here are more details on why I don’t recommend Shoulda.

Takeaways

Rails testing tools take some time to learn, but the important part (and perhaps more difficult part) is learning testing principles.

If you’re just getting started with Rails testing, the next step I would suggest is to learn about the different types of Rails tests and when to use them.

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.