RSpec on Rails
Tutorial
https://ihower.tw
2016/8
Agenda
• Rails RSpec
• Model Spec, Routing Spec, Controller Spec,
View Spec, Helper Spec
• Request Spec Feature Spec
•
• CI (Continuous Integration)
• Web
Install rspec-rails
• gem “rspec-rails”
• bundle
• rails g rspec:install
• git rm -r test
rake -T spec
• rake spec
• bundle exec rspec spec/xxx/xxx
Generators
• rails g model A
• rails g controller B
• rails g scaffold C
spec/rails_helper.rb
spec/spec_helper.rb
config.fail_fast = true
config.profile_examples = 3
config.order = :random
More Matchers
• expect(target).to eq(XXX)
• expect{ Post.find(9999) }.to
raise_error(ActiveRecord::RecordNotFound)
• expect(target).to be_xxx # target.xxx?
• expect(target).to be_a_xxx
• expect(target).to be_an_xxx
• expect(collection).to be_empty
• expect([1,2,3]).to be_include(3)
• expect({ foo: "foo" }).to have_key(:foo)
• expect(3).to be_a_kind_of(Fixnum)
• Custom matcher
rspec-rails
•
• model
• controller ( stub/mock)
• view
• helper
• routing
•
• controller ( stub/mock model )
• ( controllers )
• request
• feature ( capybara)
https://robots.thoughtbot.com/rails-test-types-and-the-testing-pyramid
Model spec syntax
let(:valid_attributes){ { :name => "Train#123"} }
expect(Event.new).to_not be_valid
expect(Event.new(valid_attributes)).to_not be_valid
Exercise 0
• Rails (ticket_office)
• rspec-rails gem
• scaffold
Exercise 1:
Train Model Spec
• Train model
• valid
Kata
• Ticket Office
• GET /trains/{train_id}
• POST /trains/{train_id}/reservations
Routing spec syntax
expect(:get => "/events").to route_to("events#index")
expect(:get => "/widgets/1/edit").not_to be_routable
But…
• ( Rails
)
• resources routing spec model
validations associations
• custom route
Controller spec syntax
get :show
post :create, :params => { :user => { :name => "a" } }
patch :update
delete :destroy
# more arguments
request.cookies[:foo] = "foo"

request.session[:bar] = “bar"
post :create, :params => { :name => "a" },
:session => { :zoo => "zoo" },
:flash => { :notice => "c"},
:format => :html
: params Rails 5.0
Matcher syntax
expect(response).to render_template(:new)
expect(response).to redirect_to(events_url)
expect(response).to have_http_status(200)
expect(assigns(:event)).to be_a_new(Event)
Isolation Mode
• controller spec render view
RSpec
• render_views
Exercise 2:
Train Controller show spec (stub
version)
• trains/show
• Train#find stub
DB
View
isolated from controller too
assign(:widget, double(“Widget”, :name => "slicer"))
render
expect(rendered).to match /slicer/
Helper spec syntax
expect(helper.your_method).to eq("Q_Q")
Exercise 3:
Train show view
• train show json view
• rails4 jbuilder
• Train
stub
Exercise 4:
• Train, Seat, SeatReservation,
Reservation models
• Train#available_seats
• controller view stub
( or Partial Stub )
What have we learned?
• stub&mock
•
stub&mocks
• ActiveRecord
Exercise 5:
• ReservationsController
• Train#reserve mock
• Train#reserve spec
• ReservationsController mock
Exercise 5`:
• Train#reserve spec
• ReservationsController
( mock)
Exercise 6:
• GET /trains/{id}
• POST /trains/{id}/reservations
• POST /trains/{id}/reservations
Factory v.s. Fixtures
• rails fixtures YAML DB
• model
validation
• factory ActiveRecord
• factory_girl gem fabrication gem
•
• ActiveReocrd
• factory_girl trait
unit test
• model object DB build
create build_stubbed
factory_girl
FactoryGirl.define do
factory :user do
firstname "John"
lastname "Doe"
sequence(:email) { |n| "test#{n}@example.com"}
association :profile, :factory => :profile
end
factory :profile do
bio "ooxx"
end
end
factory_girl
before do
@user = build(:user) # DB
@event = create(:event) # DB
end
it "should post user data"
post :create, :params => { :user => attributes_for(:user) }
# ...
end
• https://github.com/thoughtbot/factory_girl/
blob/master/GETTING_STARTED.md
• https://thoughtbot.com/upcase/videos/
factory-girl
Tip: support
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
# support/factory_helpers.rb
module FactoryHelpers
# ...
end
Rspec.configure do |config|
config.include FactoryHelpers
end
Exercise 7: Extract to
factory method
• Train Extract support/
factory.rb
Tip: stub
before(:each) {
allow(controller).to receive(:current_user) { ... }
}
Tip: focus
• :focus => true describe
it
• rspec --tag focus
•
config.filter_run :focus => true
config.run_all_when_everything_filtered = true
Request
• full-stack
• stub
• Web APIs JSON, XML
• Request
controllers
• sessions ( )
• Matchers controller spec
Request spec syntax
describe "GET /events" do
it "works! (now write some real specs)" do
get “/events”
expect(response).to have_http_status(200)
end
end
Example: controller
it "creates a Widget and redirects to the Widget's page" do
get "/widgets/new"
expect(response).to render_template(:new)
post "/widgets", :widget => {:name => "My Widget"}
expect(response).to redirect_to(assigns(:widget))
follow_redirect!
expect(response).to render_template(:show)
expect(response.body).to include("Widget was successfully created.")
end
Exercise 8:
• 1. 2. 3.
Feature spec
• capybara gem request spec
• http://rubydoc.info/github/jnicklas/capybara/
master
• Capybara HTML
Capybara example
feature "signing up" do
background do
User.create(:email => 'user@example.com', :password => 'caplin')
end
scenario "signing in with correct credentials" do
visit "/" # or root_path
click_link 'Log In'
within("#session") do
fill_in 'Login', :with => 'user@example.com'
fill_in 'Password', :with => 'caplin'
choose('some_select_option_yes')
check('some_checkbox')
end
click_button 'Sign in'
expect(User.count).to eq(1) # you can test model
expect(page).to have_content 'Login successfuuly' # and/or test page
end
end
find css selector xpath
Debugging
• save_and_open_page
• capybara-screenshot gem
•
JavaScript Driver
• Capybara javascript
• javascript_driver
Ruby README
• https://github.com/teampoltergeist/poltergeist
PhantomJS
• https://github.com/thoughtbot/capybara-webkit
QtWebKit
• https://rubygems.org/gems/selenium-webdriver
Firefox
• test js: true
JavaScript Driver
• Browser tools Rails Ruby
thread
• DB transaction database cleaner
• https://github.com/DatabaseCleaner/database_cleaner
• https://github.com/amatsuda/database_rewinder
• javascript (
Ajax)
• Capybara 5
• Capybara.default_wait_time
• using_wait_time(2) { …. }
https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capybara
• Extract behavior to helper methods
• Page Object
https://robots.thoughtbot.com/acceptance-tests-at-a-single-level-of-
abstraction
Page Object
http://www.infoq.com/cn/articles/martin-fowler-basic-rule-of-thumbon-for-Web-testing
Page Object example
https://teamgaslight.com/blog/6-ways-to-remove-pain-from-feature-testing-in-ruby-on-rails
https://thoughtbot.com/upcase/videos/page-objects
https://robots.thoughtbot.com/better-acceptance-tests-with-page-objects
https://medium.com/neo-innovation-ideas/clean-up-after-your-capybara-1a08b47a499b#.oyl7zi44d
https://www.sitepoint.com/testing-page-objects-siteprism/
(1)
• Debugging ?
• puts
• https://tenderlovemaking.com/2016/02/05/i-am-
a-puts-debuggerer.html
• byebug
• --only-failures option
• https://relishapp.com/rspec/rspec-core/docs/
command-line/only-failures
(2)
• Time.now ?
• http://api.rubyonrails.org/classes/
ActiveSupport/Testing/TimeHelpers.html
travel_to
• config.include
ActiveSupport::Testing::TimeHelpers
• Timecop gem
(3)
• email ?
• mail = ActionMailer::Base.deliveries.last
• config.before(:each)
{ ActionMailer::Base.deliveries.clear }
• https://github.com/email-spec/email-
spec/
(4)
• ?
• spec/fixtures/
• File.new(Rails.root + ‘spec/fixtures/
foobar.png') paperclip
Photo.create(:description => "Test", :attachment
=> File.new(Rails.root + ‘spec/fixtures/ac_logo.png'))
• feature spec capybara attach_file
http://www.rubydoc.info/github/jnicklas/capybara/
master/Capybara%2FNode%2FActions%3Aattach_file
(5)
• devise https://github.com/plataformatec/
devise Test helpers
config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::ControllerHelpers, type: :view
config.include Devise::Test::IntegrationHelpers, type: :feature
(6)
• sidekiq ?
• http://api.rubyonrails.org/classes/
ActiveJob/TestHelper.html#method-i-
perform_enqueued_jobs
• config.include ActiveJob::TestHelper
• enqueue job
perform_enqueued_jobs
{ … }
(7)
• after_commit ?
• unit test transaction after_commit
• https://github.com/grosser/test_after_commit
• database_cleanner truncation transaction
• trigger
• http://mytrile.github.io/blog/2013/03/28/testing-after-
commit-in-rspec/
• Rails 5.0 workaround
(8)
• rake ?
• model class
method
• model spec
(9)
• legacy ?
• happy path feature spec C/P
• Unit test
•
• Unit Test
(10)
• ?
• Test double (?)
• CPU
• https://github.com/grosser/parallel_tests
• CI concurrent build
Tools
• shoulda rails matcher
• database_cleaner DB
• vcr HTTP response 3-party
service
• simplecov
• cucumber
• CI (continuous integration (CI)
BDD
http://www.tenlong.com.tw/items/9862019484?
item_id=997422
simplecov
?
Coverage
!
CI
• 3-party service
• https://www.codeship.io
• https://circleci.com/
• https://travis-ci.org/
• build your own
• Jenkins
?
Spec
• Rspec spec
•
• Custom Matcher
Test Pyramid
http://watirmelon.com/2012/01/31/introducing-the-software-testing-ice-cream-cone/
http://martinfowler.com/bliki/TestPyramid.html
developer 

QA XD

salesforce ?
https://www.quora.com/What-is-the-best-way-to-test-the-web-application
What is the best way to test the web application?
Why?
• low-level
debug
• high-level
debug
• view
• developer
QA
• bug failures
trace
•
•
( Mocks )
https://thoughtbot.com/upcase/videos/testing-antipatterns
Isolation
• it
Expectation
•
• one failure one problem 

trace
describe "#amount" do
it "should discount" do
user.vip = true
order.amount.should == 900
user.vip = false
order.amount.should == 1000
end
end
context
describe "#amount" do
context "when user is vip" do
it "should discount ten percent" do
user.vip = true
order.amount.should == 900
end
end
context "when user is not vip" do
it "should discount five percent" do
user.vip = false
order.amount.should == 1000
end
end
end
• Private methods
•
Private methods
class Order
def amount
if @user.vip?
self.caculate_amount_for_vip
else
self.caculate_amount_for_non_vip
end
end
private
def caculate_amount_for_vip
# ...
end
def caculate_amount_for_non_vip
# ...
end
end
it "should discount for vip" do
@order.send(:caculate_amount_for_vip).should == 900
end
it "should discount for non vip" do
@order.send(:caculate_amount_for_vip).should == 1000
end
Private methods
• public
private/protect methods
• private
• public
• Public methods
Private/Protected
describe User do
describe '.search' do
it 'searches Google for the given query' do
HTTParty.should_receive(:get).with('http://www.google.com',
:query => { :q => 'foo' } ).and_return([])
User.search query
end
end
end
HTTP
describe User do
describe '.search' do
it 'searches for the given query' do
User.searcher = Searcher
Searcher.should_receive(:search).with('foo').and_return([])
User.search query
end
end
end
class User < ActiveRecord::Base
class_attribute :searcher
def self.search(query)
searcher.search query
end
end
class Searcher
def self.search(keyword, options={})
HTTParty.get(keyword, options)
end
end
?
TATFT
test all the f**king time
bug
bug
bug
DHH Way
• 100% test coverage
• Code-to-test 1:2 1:3
• 1/3
• Active Record associations, validations, or scopes.
• ( Unit Test )
• Cucumber
• Specification by Example
• TDD (DHH 20% TDD)
• Model DB Fixtures
• Controller
• Views system/browser testing
https://signalvnoise.com/posts/3159-testing-like-the-tsa
http://david.heinemeierhansson.com/2014/test-induced-design-damage.html
?
coverage?
code review
pair programming
EPIC FAIL
Unit Test
Reference:
• http://guides.rubyonrails.org/testing.html
• https://github.com/eliotsykes/rspec-rails-examples#api-request-specs-docs--
helpers
• The RSpec Book
• The Rails 3 Way
• Foundation Rails 2
• xUnit Test Patterns
• everyday Rails Testing with RSpec
• http://betterspecs.org/
• http://pure-rspec-rubynation.heroku.com/
• http://jamesmead.org/talks/2007-07-09-introduction-to-mock-objects-in-ruby-at-lrug/
• http://martinfowler.com/articles/mocksArentStubs.html
• http://blog.rubybestpractices.com/posts/gregory/034-issue-5-testing-antipatterns.html
• http://blog.carbonfive.com/2011/02/11/better-mocking-in-ruby/
Thanks.

RSpec on Rails Tutorial