RSpec-Rails, Part 1

David Ryan Morphew
6 min readNov 2, 2021

Adding Simple Model Tests To Rails

Photo by Noah Windler on Unsplash

Errors are your friends, or perhaps, even better—your “frenemies.” They’re there to do a very useful job in letting you know not only that something didn’t work, but also, perhaps, why it didn’t work.

Enter Test-Driven Development: your daily hangout with frenemies.

In this blog, I just want to walk through a beginning to test-driven development in Ruby on Rails, using the testing framework of RSpec-Rails. I’ll only talk about unit tests for a model in this blog, as well.

But, with TDD, remember that big things have small beginnings.

Installation

First, if we’re going to work with RSpec in Rails, we need to install the “rspec-rails” gem. Following the guide here:

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

Thereafter, run:

bundle install

Now, to get RSpec ready for use, and to add the spec directory for your test files, run:

rails generate rspec:install

Now you’ll be able to run tests using bundle exec rspec, and you can add test files under the spec directory with the rspec-rails generator commands in the command line.

Adding a Model Test With the RSpec Generator

In this example, I want to have an author model and anauthors table in my Rails app. So, let’s add an author_spec file.

rails generate rspec:model author

This will generate a model within our spec directory, and an author_spec file under the model directory. We can use this test file to add tests for our methods we use for our model, and any validations or relationships we have for our authors table in the database. Here’s the file generated:

spec/models/author_spec.rbrequire 'rails_helper'RSpec.describe Author, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

Validation of Attributes Tests

Adding Our First Validation Test: Presence of Name Attribute

So, let’s start with a simple validation. We don’t want to create an entry for our author in our database without having a name attribute. So, we’ll want to make sure that that attribute is present before we allow an entry to be saved in the database.

Since my app is focused on ancient authors, we’ll stick with just name instead of parsing it out into first_name and last_name, since those conventions are not always easily mapped onto individuals from the (distant) past.

So, first, let’s add the description of what we want to happen in a string after it , opening up a block for the test we’ll use:

RSpec.describe Author, type: :model do
it "requires the presence of a name" do

end
end

This is not yet a test. If you run the tests, it’ll say that everything is passing here, but that’s not really useful. We want to test some sort of expectation or assertion now:

RSpec.describe Author, type: :model do  it "requires the presence of a name" do
expect(Author.new).not_to be_valid
end
end

Now, when we have our author model in place, if we don’t have a validation to check for the presence of a name attribute, we should fail the test.

Failures:1) Author requires the presence of a name
Failure/Error: expect(Author.new).not_to be_valid
expected #<Author id: nil, name: nil, created_at: nil, updated_at: nil> not to be valid
# ./spec/models/author_spec.rb:5:in `block (2 levels) in <top (required)>'
Finished in 0.11698 seconds (files took 1.82 seconds to load)
1 example, 1 failure
Failed examples:rspec ./spec/models/author_spec.rb:4 # Author requires the presence of a name

When we initialize a new author instance (Author.new) we do not want it to be valid, unless it has a name attribute (Author.new(name: “Marcus Aurelius”)).

So, let’s add that to an Author model:

app/models/author.rbclass Author < ApplicationRecord
validates :name, presence: true
...
end

Now, it passes. We won’t be able to save a new author instance unless a name attribute exists for that instance.

Adding Model Validations of Relations Tests

Adding Our Validation for an Author’s has_many relationship to Works

Now, each of our authors needs to be able to have many different written works, which we’ll just call works in our database table with the Work model.

So, let’s add a new test model for works:

rails generate rspec:model work

We’ll skip ahead this time to adding a validation for a work with a title attribute:

spec/models/work_spec.rbRSpec.describe Work, type: :model do
it "requires the presence of a title" do
expect(Work.new).not_to be_valid
end
end

Fail fast again:

Failures:1) Work requires the presence of a title
Failure/Error: expect(Work.new).not_to be_valid
expected #<Work id: nil, title: nil, created_at: nil, updated_at: nil, author_id: nil> not to be valid
# ./spec/models/work_spec.rb:5:in `block (2 levels) in <main>'
Finished in 0.05534 seconds (files took 1.25 seconds to load)
2 examples, 1 failure
Failed examples:rspec ./spec/models/work_spec.rb:4 # Work requires the presence of a title

Add validation to the Work model:

app/models/work.rbclass Work < ApplicationRecord
validates :title, presence: true
...
end

Now we should be all green again!

Now, for the has_many, belongs_to Validations

We’ll use these collection / association methods of Rails’ ActiveRecord ORM:

For the has_many side of the relationship:

  1. object.collection
  • the “.collection” method returns an array of all of the associated objects that our receiver (the object on the left side of the dot) has many of.
  • object has many of the (different) objects in the collection.
  • Example: author.works, since an author has many works.

For the belongs_to side of the relationship:

2. object.association

  • the “.association” here is the object to which our receiver (again, the object on the left side of the dot) belongs.
  • object here belongs to another object (the association).
  • Example: work.author, since a work belongs to one author (for ancient texts that I’ll be using, this is a safe bet).

3. object.create_association

  • you can create a new instance of the association, already associated with the receiving object, in one go.

The methods go hand-in-hand, since a has_many relationship is going to coordinate with a belong_to relationship.

Author has_many works Validation Test

Let’s start with our author model test:

RSpec.describe Author, type: :model do let(:author) {
Author.create(
name: "Epictetus"
)
}
...it "has many works" do
first_work = Work.create(title: "Discourses")
second_work = Work.create(title: "Enchiridion")
author.works << [first_work, second_work]
expect(author.works.first).to eq(first_work)
expect(author.works.second).to eq(second_work)
end
end

Here, we create an new author with:

let(:author) {
Author.create(
name: "Epictetus"
)
}

We then add two new books:

first_work = Work.create(title: "Discourses")
second_work = Work.create(title: "Enchiridion")

And then we add them to the author’s collection of works:

author.works << [first_work, second_work]

After that, we are able to ask if those two works are correctly associated as the first and second works in the author’s collection:

expect(author.works.first).to eq(first_work)   
expect(author.works.second).to eq(second_work)

If we have not set up the has_many relationship in our Author model, this test should fail:

Failures:1) Author has many works
Failure/Error: first_work = Work.create(name: "Discourses")

ActiveModel::UnknownAttributeError:
unknown attribute 'name' for Work.
# ./spec/models/author_spec.rb:16:in `block (2 levels) in <top (required)>'

So, let’s add the has_many macro to our Author model:

class Author < ApplicationRecord
validates :name, presence: true
has_many :works
...
end

All green!

Work belongs_to Author Validation Test

So, let’s add the belongs_to validation to the model test file:

RSpec.describe Work, type: :model do let(:first_work) {
Work.create(
title: "Discourses"
)
}
let(:second_work) {
Work.create(
title: "Enchiridion"
)
}
...
it "belongs to an author" do
author = first_work.create_author(name: "Epictetus")
second_work.author = author
expect(first_work.author).to eq(author)
expect(second_work.author).to eq(author)
end
end

Here, we first create two works:

let(:first_work) {
Work.create(
title: "Discourses"
)
}
let(:second_work) {
Work.create(
title: "Enchiridion"
)
}

We create an author that is already associated with the first work:

author = first_work.create_author(name: "Epictetus")

We also associate the second work to that same author:

second_work.author = author

Then, we can test that both works belong to that author:

expect(first_work.author).to eq(author)
expect(second_work.author).to eq(author)

As before, we should see failure, if we haven’t already added the belongs_to macro to our User model. So, we add that now:

class Work < ApplicationRecord
validates :title, presence: true
belongs_to :author
...
end

Now, we should be all green again.

To Be Continued

So far, we’ve added some simple model tests for attribute presence and a has_many and belongs_to relationship between two models. This is just the beginning for the unit testing I’m planning for this app. I plan on addressing a has_many :through relation next time, as well as a test of an attributes uniqueness in a model test. Stay tuned for more to come!

--

--

David Ryan Morphew

I’m very excited to start a new career in Software Engineering. I love the languages, frameworks, and libraries I’ve already learned / worked with (Ruby, Rails,