One: Case Study
Debug sucks, and test rocks! If you use rails, then you should know how to use RSpec to test your rails app.
I use a simple case to practice basic RSpec. If you want practice, go to check the saasbook/bdd-tdd-cycle practice at the end of this articel. Here is the case.
User `name:string and email:string` `name and email should not be nil.`
I will test the validation of model and basic controller actions. Some revelent good resources are list at the end of this post.
Two: Set Up
Gem
We will use the most popular gems combo here.
group :development, :test do
gem 'rspec-rails'
gem 'factory_girl_rails' #give us some test data to play with
end
group :test do
gem 'capybara' #simulate users’ actions
#detects changes and run test automatically, time saver!
#if you use mac, make sure `gem install rb-fsevent`
gem 'guard-rspec'
gem 'launchy' # open browser to help you debug
end
Generate
Then run bundle install
to install these gems and run rails generate rspec:install
to generate necessary files. Also, if you use capybara, make sure require "capybara/rspec"
is included in your spec/spec_helper.rb
file.
Configration
Usually, I don’t want the auto generated test file, so I would add these config to the config/application.rb
file:
config.generators do |g|
g.controller_specs false
g.view_specs false
g.helper_specs false
g.routing_specs false
g.request_specs false
end
Three: Factory Girl
It would be good if we have some data that is isolated from database to play with. And Factory Girl is a great tool to deal with this problem.
Here we want two users, one is valid and another is not. Create a new file named users.rb
inside spec/factories/users.rb
. And:
FactoryGirl.define do
factory :user do |f|
f.name "John Smith"
f.email "hey@gmail.com"
end
factory :invalid_user, parent: :user do |f|
# inherits from user
f.name nil
end
end
Four: Model
Now we can test our model easily. In this article, I just want to test validation of this simple User model. User with name and email are valid, invalid without name or email.
Create a new file called user_spec.rb
(make sure ‘_spec’ is included to be detected by RSpec).
require 'rails_helper'
RSpec.describe UserModel, type: :model do
describe User do
it "has valid user" do
# use create, just FactoryGirl(:user) is deprecated now
expect(FactoryGirl.create(:user)).to be_valid
end
it "is invalid without a name" do
# use build to skip validation, it will not call 'save' method
# when use create, our test will always fail
expect(FactoryGirl.bulid(:user, name: nil)).to_not be_valid
end
it "is invalid without email" do
expect(FactoryGirl.bulid(:user, email: nil)).to_not be_valid
end
end
end
Now I think the minmum model test is done. Model is the most inportant part of rails app, make sure they are test with high coverge. You can use simplecov to generate report. Now let’s move on to controller test!
Five: Controller
In this short article, I will just test create, update and destroy, and skip index etc.
Create
describe "POST #create" do
context "with valid attributes" do
it "create a new user" do
expect{
post :create, user: FactoryGirl.attributes_for(:user)
}.to change(User, :count).by(1)
end
it "redirect to new user" do
post :create, user: FactoryGirl.attributes_for(:user)
expect(response).to redirect_to(User.last)
expect(flash[:notice]).to include('successfully')
end
end
context "with invalid attributes" do
it "create a new user" do
expect{
post :create, user: FactoryGirl.attributes_for(:invalid_user)
}.to_not change(User, :count)
end
it "render new template" do
post :create, user: FactoryGirl.attributes_for(:invalid_user)
expect(response).to render_template :new
end
end
end
Something to notice: user expect() instead should; organise your test with context if there are too many examples.
Delete
describe 'DELETE destroy' do
before(:each) do
# make our test DRY
@user = FactoryGirl.create(:user)
end
it "deletes the user" do
# note syntax, http method and rails action
expect{
delete :destroy, id: @user
}.to change(User,:count).by(-1)
end
it "redirects to users#index" do
delete :destroy, id: @user
expect(response).to redirect_to users_url
end
end
Update
describe 'PUT update' do
before :each do
@user = FactoryGirl.create(:user, name: "John", email: "hey@gmail.com")
end
context "valid update" do
it "change user attributes" do
put :update, id: @user, user: FactoryGirl.attributes_for(:user, name: "John", email: "hello@gmail.com")
@user.reload
expect(@user.name).to eq("John")
expect(@user.email).to eq("hello@gmail.com")
end
it "redirects to the updated user" do
put :update, id: @user, user: Factory.attributes_for(:user)
expect(response).to redirect_to @user
end
end
context "invalid update" do
it "does not update user's attributes" do
put :update, id: @user, user: FactoryGirl.attributes_for(:user, name: "Michael", email: nil)
@user.reload
expect(@user.name).to_not eq("Michael")
expect(@user.email).to eq("hey@gmail.com")
end
it "renders the edit view" do
put :update, id: @user, user: FactoryGirl.attributes_for(:invalid_user)
expect(response).to render_template :edit
end
end
end