We have updated the content of our program. To access the current Software Engineering curriculum visit curriculum.turing.edu.
Model Testing in RSpec for a Sinatra App
Learning Goals
- set up RSpec within a Sinatra web app
- test model methods and validations using best practices in RSpec
Vocab
- RSpec
- Model Testing
Repository
We will continue to use the Set List repository that we used in the Intro to ActiveRecord lesson.
Warmup
1) Read this Thoughtbot article about the four-phase test design. 2) Thinking about what you just read, what did this look like in Minitest?
Lecture
Intro to RSpec
- Slightly different than Minitest, but not by much.
describe
blocks as an outside wrapper to group related tests: use for thingscontext
blocks to add… context (but technically the same method asdescribe
): use for statesit
blocks to indicate an outcome (something to test)scenario
blocks to indicate an outcome (something to test)expect
instead of assert
- Model testing describes our “bottom-up” design, and shows other developers how our model code should work within our application.
Code-Along
Setting up Model Tests
STEP 1: Install rspec
Add the following line to the block labeled group :development, :test
in your Gemfile
gem 'rspec'
Run bundle
.
Next, make sure you are in the root folder of your app.
STEP 2: Configurations in .rspec file
touch .rspec
Your .rspec
file can contain certain flags that are helpful when you run your tests.
--require spec_helper
--format=documentation
--order=random
Note: rspec
can take command-line flags also take some flags to change its output. For a full list run rspec --help | less
. These flags can be stored within this .rspec
file to be used each time.
See this Stack Overflow answer for additional details.
STEP 3: Set up the spec_helper.rb
file:
mkdir spec
touch spec/spec_helper.rb
Add the following to your spec_helper.rb
file:
require 'bundler'
Bundler.require(:default, :test)
require File.expand_path('../../config/environment.rb', __FILE__)
First this will require the bundler
gem, then use that gem to require the other gems we have loaded in the default
and test
groups in our Gemfile.
Finally, we require the environment.rb
file, which loads up the rest of our application so that we can use it in our tests.
Create a Model Spec
mkdir spec/models
touch spec/models/song_spec.rb
In spec/models/song_spec.rb
:
There are many ways we could choose to use RSpec describe
and context
blocks to organize our tests, but for our purposes today, we’re going to use the following:
RSpec.describe Song, type: :model do
describe "Class Methods" do
describe ".total_play_count" do
it "returns total play counts for all songs" do
Song.create(title: "Song 1", length: 180, play_count: 3)
Song.create(title: "Song 2", length: 220, play_count: 4)
expect(Song.total_play_count).to eq(7)
end
end
end
end
Let’s discuss:
- the dot in
.total_play_count
: check out this best practice - the space between the created songs and the expectation
At this point you should be able to run your tests from the command line using the command rspec
.
Make it Pass
What do we get? Errors! Great. We can follow errors. These errors are a bit different from Minitest Errors. Let’s take a look:
Randomized with seed 28022
Song
Class Methods
.total_play_count
returns total play counts for all songs (FAILED - 1)
Failures:
1) Song Class Methods .total_play_count returns total play counts for all songs
Failure/Error: expect(Song.total_play_count).to eq(7)
NoMethodError:
undefined method `total_play_count' for #<Class:0x007fea2ab582d8>
# /Users/ian/.rvm/gems/ruby-2.4.0/gems/activerecord-5.1.4/lib/active_record/dynamic_matchers.rb:22:in `method_missing'
# ./spec/models/song_spec.rb:9:in `block (4 levels) in <top (required)>'
Finished in 0.02851 seconds (files took 0.80607 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/models/song_spec.rb:5 # Song Class Methods .total_play_count returns total play counts for all songs
Randomized with seed 28022
First we see the Randomized seed, which is a record of the random order the tests were run this time around.
Next we see the descriptors from our describe, context, and it blocks. Now we see a failure which should be a bit more familiar to you:
NoMethodError:
undefined method `total_play_count' for #<Class:0x007fea2ab582d8>
Add that method into our Song model.
# app/models/song.rb
def self.total_play_count
end
Run our spec again and it tells us:
1) Song Class Methods .total_play_count returns total play counts for all songs
Failure/Error: expect(Song.total_play_count).to eq(7)
expected: 7
got: nil
(compared using ==)
# ./spec/models/song_spec.rb:9:in `block (4 levels) in <top (required)>'
The error we see now should be pretty familiar. What is causing our method to return nil
instead of 7?
We need to populate it with something – the sum of the play_count for each Song in the database.
ActiveRecord has just what we need:
# app/models/song.rb
def self.total_play_count
sum(:play_count)
end
What’s happening here? Well, sum
is an ActiveRecord method that will sum a particular column of values in a single table within our database (it can’t sum things across different tables). How does it know which column? We pass it the column name as an argument using ‘symbol notation’.
How does it know that we’re trying to call this method on our songs
table? The implicit receiver of the sum
method is self
in the method definition, which in this case is the class Song.
This is an example of a Class Method – ActiveRecord calls on the entire class are usually used for performing work on EVERY row in the Class’ table
Great! Run our tests again, and we still get an error.
1) Song Class Methods .total_play_count returns total play counts for all songs
Failure/Error: expect(Song.total_play_count).to eq(7)
expected: 7
got: 1972034
What’s going on here?
It looks like the total that’s being reported by our test is the full total of our all the songs currently in our database.
Run it one more time to check. Notice that the actual value that we’re getting increased? So, not only are we not testing with only the data we’re providing in the test, but on top of that, every time we run the test we’re adding new songs to our development database.
This is not the behavior we want. We’re “polluting” the database that we’re using when we browse the site locally. Wouldn’t it be better if we could run our test suite without making these changes?
Every time we run our tests, we want to start with a fresh slate with no existing data in our test database. Because of this, we need to have two different databases: one for testing purposes and one for development purposes. This way, we will still have access to all of our existing data when we run shotgun and look at our app in the browser, but we won’t have to worry about those pieces interfering with our tests because they’ll be in a separate database.
How will our app know which environment – test or dev – we want to use at any moment? By default (like when we start the server with shotgun), we will be in development. If we want to run something in the test environment, we need an indicator. We’ll use an environment variable: ENV["RACK_ENV"]
.
We’re going to set an environment variable in our spec helper file and then use that variable to determine which database to use. In spec_helper.rb
add the following above all the require
lines:
ENV["RACK_ENV"] = "test"
It’s very important that this line comes before you require the environment. If you want to trace why, take a look first at line 14 in your config/environment.rb
file, which should lead you to the config/database.rb
file. In that file, you’ll see that the database name gets set based on the current environment.
One more step and then we should be in good shape. From the command line:
$ rake db:test:prepare
This should both create and run the migrations for a test database (you should be able to see the new file in your db
directory).
Now run your test again from the command line using rspec
. Passing test? Great!
Run the test again a few times. Failing test! Hmm.
What’s happening here? Before we were saving new songs to our development database every time we ran our test suite. Now we’re doing the same thing to our test database. What we’d like to do is to clear out our database after each test. We could create these methods in each one of our tests, but there’s a tool that will help us here: Database Cleaner.
In the test/development section of your Gemfile add the following line:
gem 'database_cleaner'
Run bundle install
Then in your spec_helper.rb
, add the following after your current require
lines:
DatabaseCleaner.strategy = :truncation
RSpec.configure do |c|
c.before(:all) do
DatabaseCleaner.clean
end
c.after(:each) do
DatabaseCleaner.clean
end
end
This will clean the database before all tests and after each test. This ensures that if we stop our test suite at any point before it finishes, we will still have a clean database.
Save and run your tests again from the command line. Passing test? Great! Run it one more time to double check.
Testing Validations
One thing we haven’t really worried about up to this point was whether or not a new Song had all of its pieces in place when we were saving it to the database. We want to make sure that when someone tries to save a song that they’re providing us with ALL the information our app needs. We don’t want to have someone save a song with, for example, no title.
Add the following test to your song_spec
within the main describe Song
block, but outside of your existing describe 'Class Methods'
block.
describe "Validations" do
it "is invalid without a title" do
song = Song.new(length: 207, play_count: 2)
expect(song).to_not be_valid
end
end
Run your test suite from the command line with rspec
and look for the new failure.
1) Song Validations is invalid without a title
Failure/Error: expect(song).to_not be_valid
expected `#<Song id: nil, title: nil, year: 207, play_count: 2, created_at: nil, updated_at: nil>.valid?` to return false, got true
# ./spec/models/song_spec.rb:7:in `block (3 levels) in <top (required)>'
- Under Song, Class Methods, .total_play_count you should see a green
returns total play counts for all songs
. That is our old test still passing. - Under Song, Validations, you should see a red
is invalid without a title (FAILED -1)
The output of this error is telling us that it expected .valid?
to return false
when called on our new song, and instead got true
.
Great! It seems like this is testing what we want, but how can we actually make this test pass?
Writing Validations
ActiveRecord actually helps us out here by providing a validates
method which we’ll pass the column name in the form of a symbol, and an options hash {presence: true}
. The convention we use is the following format:
Go into the app/models/song.rb
model and add the following line:
validates :title, presence: true
Alternatively, you can write this as: validates_presence_of :title
. This is nice if you want to validate the presence of multiple columns.
Run your tests again, and… passing. Great news.
Worktime
- In pairs, add the following tests and make each one pass:
- a test for an
.average_play_count
class method - tests that a song cannot be created without a
title
orlength
- a test for an
Remember to use your four phases of testing!
Finished?
- Take a look at the BetterSpecs community guidelines.
- Check out the RSpec Documentation: For now you’ll likely be most interested in the
rspec-core
, andrspec-expectations
links.
Recap
- What goes into your spec_helper in a Sinatra app? What does each piece do?
- Create a Venn Diagram comparing MiniTest & RSpec. Think about set up of methods and how you check expected outcomes.