Objectives |
---|
Understand the difference between unit and integration tests. |
Identify various aspects of Rails apps that we might want to test. |
Test model methods using rspec-rails. |
Test controller actions using rspec-rails. |
Resource | Description |
---|---|
RSpec matchers | Reference for RSpec |
shoulda | Magic for model specs |
FactoryGirl | Factories let you build up objects quickly for your specs |
DatabaseCleaner | Cleans out your database before each test. |
Tests can be broadly split into two categories. Unit Tests and Integration Tests. Both are important.
In Unit Tests which we'll talk about today we try to isolate each component (or class/method) and test it on it's own. We separate our Controllers from our Views and test the boundary of its interface.
Unit tests tend to run faster because we're testing small components. By having to isolate components from each other to test them we're forced to write better OO code. The functionality can't blur across several modules without us having to do a lot of work in the test to stub that out.
In Integration Tests we combine components together, sometimes just a few and other times the entire system. In Rails, integration tests often drive a browser and test the entirety of the system--the full request response cycle. These tests tend to take much longer to run. They test the collusion of components and that the interface between them is behaving as we expect.
Both types of tests are important. There are also other types but they can generally be broken down into finer grained versions of the above. Together the Unit and Integration tests you write become part of your test suite.
Many companies require that all the code they develop comes with tests. Often before merging code into master, the entire test suite is run and all tests must pass.
RSpec is a testing gem for Ruby. It helps us write tests that sound like user stories or planning comments ("This method does..."). <a href"https://github.com/rspec/rspec-rails" target="_blank">rspec-rails is a testing framework specifically for Rails. We'll use rspec-rails to test our models and controllers.
rspec-rails helps us implement the four-phase testing methodology (with setup, exercise, verify, and tear down steps). Here's what a simple rspec-rails test might look like:
#
# spec/models/pet_spec.rb
#
RSpec.describe Pet, type: :model do
# setup
let(:pet) { Pet.create({name: "Olive", age: 4}) }
describe "#is_cute?" do
it "returns true" do
expect(pet.is_cute?).to be true #exercise and verify
end
end
# teardown is automatic
end
- Add rspec-rails to your Gemfile in the
development
andtest
groups:
#
# Gemfile
#
group :development, :test do
gem 'rspec-rails'
end
-
Run
bundle install
(orbundle
for short) in your terminal so that rspec-rails is actually added to your project. -
Add tests to your rails project using the terminal:
$ rails g rspec:install
This creates a spec
directory. It also adds spec/spec_helper.rb
and .rspec
files that are used for configuration. See those files for more information.
-
Configure your specs by going into the
.rspec
file and removing the line that says--warnings
if there is one. Consider adding--format documentation
-
If you created models before adding rspec-rails, create a spec file for each of your models. (This is only necessary if you had a model created before you installed rspec-rails.)
$ rails g rspec:model MODEL_NAME
Typical spec folders and files for a Rails project include:
spec/models/user_spec.rb
spec/controllers/users_controller_spec.rb
spec/views/user/show.html.erb_spec.rb
spec/features/signup_spec.rb
As you can see spec files should always be named ending in
_spec.rb
.
To run all test specs, go to the terminal and type rspec
or bundle exec rspec
.
To run only a specific set of tests, type rspec
and the file path for the tests you want to run in the terminal:
# run only model specs
rspec spec/models
# run only specs for `ArticlesController`
rspec spec/controllers/articles_controller_spec.rb
# To search for and run a single spec inside a file:
rspec spec/controllers/articles_controller_spec.rb -e 'is cute'
Run rspec
from the terminal now to check that your install worked.
A test should consist of:
-
Setup: Using
let
orbefore
orsubject
to preconfigure data that is needed to test or set the test subject. You can keep your code dry by re-using these across multiple tests.- Including Definition: A name for the test. This should use an active verb. Ex. "is invalid without an email". This should also be descriptive enough that it can be used as documentation by other developers. This isn't strictly one of the 4 parts of a test, but it IS really important, future developers will like you if your test name tells them what the code should do.
-
Exercise: Any code inside the test-block itself that makes a change to the object under test prior to validating that it behaves properly.
-
Validation: Finally validating that the Object Under Test has behaved in the expected way. In RSpec this usually involves using
expect
. Sometimes this is called the assertion. -
Tear-down: Cleaning up after the test. Usually this is handled for you by RSpec and may include using DatabaseCleaner to wipe the testing database.
subject(:cat) { Animal.new(type: 'cat', name: 'fluffy') }
let(:food) { Food.new }
before do
food.flavor = 'chicken gizzards'
end
- Use
subject
to define the item being tested. - Use
let
to set variables for a test.- These are reset when each test starts!
- Use
before
to set more complex pre-test steps.- before blocks can use variables defined in
let
- before blocks can use variables defined in
let!
,subject
,before
andafter
blocks are all run for each test. Values in them are reset for each test.
describe '#eat' do
it "isn't hungry after eating" do
cat.eat(food)
- Use active verbs for test names.
- use 'it is valid' rather than 'it should be valid'
it
is for individual tests, divide the tests up usingdescribe
andcontext
- Exercise can be any extra logic that needs to be run to combine the object under test with its collaborators, or to run the method you are testing.
- Sometimes exercise and validation are on the same line; that's ok.
This is where we make assertions about the object under test and its behavior.
expect(cat.hungry?).to be false
- In general test one and only one thing per test.
- However that can sometimes mean using more than one
expect
.
- However that can sometimes mean using more than one
Usually RSpec and other gems we might be using take care of most of this for us. However in some cases you may need to do some sort of cleanup.
after do
cat.pet
end
- isolation
- behavior
- by component
We try to test each component or piece independently. Code written following good object-oriented practices and with concerns well separated is far easier to test. So, if we write our tests before our code our tests can help to push us to write good object-oriented code and to separate concerns.
Break tests into test files for each class. And then groups of tests for each method in the class. And then possibly into context
s for specific conditions under which the method may be used. (E.g. with valid or invalid data, with strings or integers, when x=true or x=false).
Isolate tests from each other. One test should never depend on another test to change or prepare something. Each test should be able to run on its own without the others.
Test behavior.
We can set up a User
instance for testing purposes with User.create
or we can use a tool called FactoryGirl to do this for us.
#
# spec/models/user_spec.rb
#
require 'rails_helper'
RSpec.describe User, type: :model do
subject(:user) { FactoryGirl.create(:user) }
end
#
# spec/factories/user.rb
#
FactoryGirl.define do
factory :user do
sequence(:email) { |n| "g#{n}@g.com" }
password "testtest"
first_name 'Jon'
last_name 'Snow'
confirmed_at { Time.now }
end
end
It's also possible to use FFaker to generate some data either for
User.create
or for FactoryGirl. But FFaker can run into intermittent issues because it can produce duplicate data or results you may not expect. Therefore many developers prefer to use FactoryGirl'ssequence
.
Assuming we've already set user
with first and last names, we can then test that the full_name
method correctly calculates the full name:
#
# spec/models/user_spec.rb
#
require 'rails_helper'
RSpec.describe User, type: :model do
...
describe "#full_name" do
it "joins first name and last name" do
expect(user.full_name).to eq("#{user.first_name} #{user.last_name}")
end
end
end
Previously we talked about model validations. You'll probably want to test these. The shoulda
gem provides an easier way to write specs for common model validations.
Validating that a Post is invalid without a title:
it "is invalid without a title" do
post = Post.new(description: 'foo')
expect(post.valid?).to be false
end
end
The same test as above written using shoulda:
it { should validate_presence_of(:title) }
- shoulda also provides test helpers for controllers
- See the shoulda docs
To test authentication, we need to define a current_user
before each of our tests run. The last line in this before do
block -- allow_any_instance_of(...
-- creates a "stub" (fake) current_user
instance method for the ApplicationController and sets it up as a getter that only ever returns the @current_user
we made with ffaker.
#
# spec/controllers/articles_controller_spec.rb
#
require 'rails_helper'
RSpec.describe ArticlesController, type: :controller do
let(:signed_in_user) { FactoryGirl.create(:user) }
before do
# stub a method on ApplicationController
allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(signed_in_user)
end
describe "GET #index" do
it "assigns @articles" do
all_articles = Article.all
get :index
expect(assigns(:articles)).to eq(all_articles)
end
it "renders the :index view" do
get :index
expect(response).to render_template(:index)
end
end
describe "GET #new" do
it "assigns @article" do
get :new
expect(assigns(:article)).to be_instance_of(Article)
end
it "renders the :new view" do
get :new
expect(response).to render_template(:new)
end
end
describe "POST #create" do
context "success" do
it "adds new article to current_user" do
articles_count = signed_in_user.articles.count
post :create, article: {title: "blah", content: "blah"}
expect(signed_in_user.articles.count).to eq(articles_count + 1)
end
it "redirects to 'article_path' after successful create" do
post :create, article: {title: "blah", content: "blah"}
expect(response.status).to be(302)
expect(response.location).to match(/\/articles\/\d+/)
end
end
context "failure" do
it "redirects to 'new_article_path' when create fails" do
# create blank article (assumes validations are set up in article model for presence of title and content)
post :create, article: { title: nil, content: nil}
expect(response).to redirect_to(new_article_path)
end
end
end
end
We could use a tool like Capybara to test client-side views and interactions (e.g. does clicking on "Logout" do what we expect?). We won't cover view testing today, though!
It's extremely important to maintain tests (especially on the master branch) and deal with test failures as soon as possible. If tests are left to languish until there are many failures, your tests lose their value and become untrustworthy. The investment your team made in testing is wasted.
Intermittent test failures are the bane of many a developers life. It's important to track these down too...they're usually caused by a poorly written test.
- FactoryGirl
- shoulda - Make Rails model tests super easy.
- DatabaseCleaner - used to wipe the database before each test, not necessary on smaller apps as tests are rolled-back.
Fork and clone the rspec testing app. Follow the instructions there.