Introduction
- Rspec-rails, 是一套 Rails 的 BDD Testing framework, 非常強大
- Factory Girl, 產生測試資料
- Capybara, 用來搭配 Rspec 的測試工具, 可以模擬使用者操作瀏覽器
- (TODO) watchr (存檔馬上測) / rcov(測試涵蓋度) / shoulda (matcher)
Gemfile
group :development, :test do
gem "rspec" # 可以不需要,如果要用原來的 rspec 才需引入
gem 'rspec-rails' # rspec 與 rails 整合的套件
gem "factory_girl_rails"
end
安裝 rspec : rails generate rspec:install
Rspec
關於 BDD
BDD 是基於 TDD 發展出來的,不同 TDD 的地方在於 BDD 寫出系統行為的規格,好處是可以盡量避免細節的遺漏、更容易理解及維護。
指令
- 執行所有測試 :
rspec
- 只跑 controllers 的測試 :
rspec spec/controllers
.rspec
指令參數
當安裝完 rspec 時, `.rspec` 參數檔會自動被建立在網站根目錄
--color
: 在執行測試時的訊息會有顏色方便閱讀
--require spec_helper
: 載入 spec/spec_helper.rb
這個檔案
--warning
: 為了避免干擾,移除它可以避免 rspec 與其他 gem 不相容的警告 warning: loading in progress, circular require considered harmful
-fs
: 輸出 specdoc 文件
-fh
: 輸出 html 文件
目錄結構
- spec
- rails_helper.rb (自動建立的設定檔)
- spec_helper.rb (自動建立的設定檔)
- models (手動建立的,用來測試 models)
- views (手動建立的,用來測試 views)
- controllers (手動建立的,用來測試 controllers)
- posts_spec.rb (手動建立的,用來測試 Post controller)
語法
測試檔大致上的架構會是這樣
require 'rails_helper' # 一定要先加上,用來讀取 rails rspec 的設定檔
# 用 describe 來包 it
describe '描述大方向要做的事情' do
it '要做的事情的細項' do
# rspec 的語法
end
end
describe (= RSpec.describe) / it
describe '描述大方向要做的事情' do
describe PostController, type: :controller do
# describe 也可以 包 describe
describe 'posts/edit.html.erb' do
# 一個 it 最好只有一種測試目的
it "#index" # 用 # 代表 instance method
it ".index" # 用 . 代表 class method
it "render partial" # 當錯誤發生時,會顯示 posts/edit.html.erb render partial ,所以 describe / it 命名好可以很快知道哪裡沒通過測試
# 也可以再包一個 describe
describe ' ... '
end
before / after
before(:all) : runs the block one time before all of the examples are run; sets the instance variables one time before all of the "it" blocks are run.
before(:each) : runs the block one time before each of your specs in the file; resets the instance variables in the before block every time an "it" block is run.
expect / to
expect(1+1).to eq(2) # 相等
expect{ post :create, post: @post_params }.to change{Post.all.size}.by(1) # Model 新增成功 ; 判斷 Update 是否成功就直接用 eq 比值
expect{ post :destroy, id: 1 }.to change{Post.all.count}.by(-1) # Model 刪除成功
expect(columns).not_to include('nickname') # hash 是否有 'nickname'
expect(response).not_to have_http_status(302) # http = 200
expect(response).to render_template(:new) # render 的 view
expect(response).to redirect_to(posts_path) # redirect 的頁面
expect(Post.create!).to eq(Post.last) # 驗證是否成功新增一筆資料
expect { ... }.to rqise_error(NotPaidError) # 例外
# should 是舊語法,盡量改用 expect
tweet.status.length.should be <= 140
mock 模仿真的物件 (TODO)
stub 回傳設定好的值
# 用意是讓 Post 這個 model 執行 save 時都一律回傳 false, 以便測試到失敗的例子
allow_any_instance_of(Post).to receive(:save).and_return(:false)
@user = stub("user")
@user.stub(:name).and_return("Apple") # @user.name = "Apple"
@client = stub("client")
@client.stub_chain(:foo, :bar, :baz).and_return("blah") # @client.foo.bar.baz = "blah"
User.stub(:find).and_return("Apple")
receive(:ping).once
receive(:ping).twice
receive(:ping).exactly(3).times
receive(:ping).at_most(3).times
receive(:ping).at_least(3).times
receive(:transfer).and_raise(TransferError)
get / post
get :index
get :edit, id: @post[:id] # 傳入 get 參數
post :create, post: @post_params # 傳入 post 的參數
post :update, post: @post_params, id: @post[:id] # 傳入 post 及 get 的參數
pending
describe Post do
it "has a name" #沒有do / end
xit "has a name"
pending "add some examples to (or delete) #{__FILE__}"
end
Matcher
be_treu / be_afalse / be_nil / be_empty / be_blank / be_admin (target.admin?)
be_a_kind_of(Array) = be_an_instance_of(Array)
have_key(:foo) / include(4) / have(3).items (target.items.length = 3)
實際撰寫的邏輯
controller
# 一般情況 : action 是否正常
it "#index" do
get :index
expect(response).to have_http_status(200)
expect(response).to render_template(:index)
end
# 將需要成功/失敗的先用 stub 處理
it "create / update 失敗" do
先用 stub 讓 .save 失敗再做其他判斷
end
view
# 一般情況的 render
it "can render" do
@post = Post.create(:title => "Big Title", :content => "content")
@posts = Array.new(2, @post)
render
expect(rendered).to include("Big Title")
end
# render partial
it "render partial" do
@post = Post.create(:title => "Big Title", :content => "content")
render
expect(response).to render_template(partial: "_form")
end
# 忽略 view helper
it "renders content when @post has content" do
allow(view).to receive(:render_content).and_return("Stub Content") # view 有個 render_content 的 helper, 讓它強制回傳 "Stub Content"
render
expect(rendered).to include("Stub Content")
end
model
# 一般情況 create
it "is accessable" do
post = Post.create!(title: "title")
expect(post).to eq(Post.last)
end
# validate 不通過
it "validates title" do
expect(Post.new).not_to be_valid
end
# 關聯 many / belongs_to # (有 gem 可以直接做這件事)
it "has_many comments" do
post = Post.create(:title => "title", :content => "content")
comment = Comment.create(:content => "content", :post_id => post.id)
expect(post.comments).to include(comment)
end
routing
# 一般 route
it "#index" do
expect(get: '/posts/1').to route_to(
controller: 'posts',
action: 'show',
id: 1
)
可以簡化為 :
expect(get: '/posts').to route_to("posts#index")
end
整合測試 (MVC一連串的行為測試,不再單獨測功能)
先建立 spec/requests/posts_spec.rb
# 實際模擬
describe "posts", type: :request do
before(:all) do
@post = Post.create(title: 'post from request spec')
end
it "#index" do
get "/posts"
expect(response).to have_http_status(200)
expect(response).to render_template(:index)
expect(response.body).to include('post from request spec')
end
it "#create" do
params = {title: 'create title', content: 'create content'}
post "/posts", post: params
expect(response).to have_http_status(302) # 會轉址
expect(Post.last.title).to eq(params[:title])
end
end
Factory Girl
介紹
在開始或測試一定會需要測試資料, 例如模擬真實上線時頁面的樣子,
或需要搭配 Rspec 測試 models 的 validation,
需要大量又隨機的資料自己手動新增也太辛苦, 可以藉由 Factory Girl 幫你產生
Getting started (TODO 未完成)
Factory Girl 建議是一個 model 對應一個 Factory Girl 檔案, 例如 models/user.rb 對應到 sepc/factiries/user.rb
spec/factories/user.rb
FactoryGirl.define do
factory :user do
name "foo"
email "bar@foo.com"
password "12341234"
birthday "1982-03-30"
end
end
(TODO 未完成)
db/seeds.rb
require 'factory_girl_rails'
puts 'Creating Roles.'
%w(guest user admin super-admin super-duper-admin).each do |role|
Role.find_or_create_by(name: role)
end
spec/factories/roles.rb:
FactoryGirl.define do
factory :role do
name { "role_#{rand(9999)}" }
factory :guest_role, parent: :role do
name 'guest'
end
factory :user_role, parent: :role do
name 'user'
end
factory :admin_role, parent: :role do
name 'admin'
end
end
end
spec/factories/users.rb:
FactoryGirl.define do
factory :user do
email 'user@example.com'
factory :admin_user, parent: :user do
email 'admin@example.com'
after(:create) {|user| user.add_role(:admin)}
end
end
end
ref :