RSpec-Rails, Part 1
Adding Simple Model Tests To Rails
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 failureFailed 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 failureFailed 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:
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!