之前看到rails 101還有rails 101 s裡面都寫到想要熟悉restful,resource是什麼,
最好的方法就是多寫幾次然後背起來。
坦白說的確是這樣沒錯,
就像你要去做最大概似估計值,你一定要先背起來每個分配的pdf長怎樣才能過關斬將。
不過單純的背誦其實是很無聊的,不如借rspec寫test來順便練習一下。
首先要明白為什麼要寫測試:
因為手動去測試所有頁面,是很麻煩的事情
這樣在update或者做更動的時候,才知道舊的功能會不會受到影響
而寫test並不等於TDD,關於這一點以tinyDenny這麼tiny的開發經驗應該是插不上嘴,
可以去搜尋DHH的talks XD
以及網路上許多文章可以參考TDD是什麼。
再來是介紹我們要用的工具,
Rspec是什麼?
基本上他就是在test rack based app的framework,
不只能測試rails,還包括了sinatra......等,
而要測試rails也可以使用mini-test,但是rspec算是堪用。
現在就先在Gemfile裡面加入這幾個gem吧:
group :development, :test do
gem 'rspec-rails', '~> 3.1.0'
gem 'faker'
end
group :test do
gem 'fabrication'
end
> faker的用途是產生假資料
fabrication的用途是產生假的物件
兩者間的不同現在看還很模糊,但待會開始做之後就知道了
安裝完後,其實我們只要執行rails g
產生model或是controller時就會自動幫我們生出對應的spec檔案,
不過我們剛剛建立model和controller時候其實還沒裝上rspec,
所以現在有一些步驟必須要做。
rails g rspec:install
這裡會產生基本的設定檔案(config檔)
rails g rspec:model post
產生model的spec、controller的也依此類推就不再重複貼上了
更多關於generator的資訊可以直接看document:
https://www.relishapp.com/rspec/rspec-rails/docs/generators
有了這些基本的東西後,我們來了解一下rspec是如何進行測試的:
撰寫測試碼(在我們剛剛新建的spec檔案裡面,他們都位於spec/底下)
bundle exec rspec spec/
這一行就會執行所有的測試
如果想要只執行某隻檔案也行bundle exec rspec spec/models/post\_spec.rb
,也可以指定想要執行的行數n :n
接著就可以來寫測試啦!
require 'rails_helper'
RSpec.describe PostsController, :type => :controller do
end
有興趣的話可以自己看看。
describe
描述的是什麼東西的測試,簡言之就是給人看的,以及劃出想要測試的scope而describe並不是亂寫一通,這裡介紹兩個習慣上的寫法
寫model method的測試前面會用#字號開頭 describe "#publish!"
在controller的describe就寫對應的http methoddescribe "GET index"
再來就可以看我們第一支test,是要測試posts controller底下的index action:
require 'rails_helper'
RSpec.describe PostsController, :type => :controller do
describe "GET #index" do
it "should assgins all posts to @posts" do
post = Post.create(title:"Testing post", content:"lorem ipsm")
get :index
expect(assigns(:posts)).to eq [post]
end
end
end
這裡多了一些新東西,一樣一個一個來看:
it "...." do
,中間的區塊可以想成跟describe一樣是給人看以及劃分出要測試的scope的
post這一行就是新增一個post然後assign給post這個variable
get :index
這裡前面get是指定使用的http method(也就是get post patch delete),後面則是用symbol來指定action
expect
:這裡就是我們期待進行完前面操作會產生的結果
後方括號中是我們想要測試的對象,這裡是assigns(:posts)
,也就是我們在controller中的@posts
再來是預期傳回來的結果,現在新的寫法是expect(assigns(:posts)).to eq [post]
這裡用陣列來裝post的原因是我們預期傳回來的post其實不只一個,因為index是要列出所有post的,可以到rails c
裡面看一下我們取出來之後的物件就是裝在陣列裡面,所以預期傳回來的就是[post]
這個陣列
看文件可以看到更多matcher的用法
弄懂這些之後下一步則是測試要找到post對應id的show action:
describe "GET #show" do
it "should assigns post to @post" do
post = Post.create(title:"Testing post", content:"lorem ipsm")
get :show, id: post
expect(assigns(:post)).to eq post
end
end
而edit action就先略過不講了,把show的controller test裡面的show替換成edit就完成了
如果忘記對應的http method不要氣餒,也不要急著google找答案,
只要在terminal執行rake routes
就可以看到啦!
我們現在還遺漏了new,所以將它補上來:
describe "GET #new" do
it "should assigns a new post to @post" do
get :new
expect(assigns(:post)).to be_a_new(Post)
end
end
be\_a\_new(ClassName)
的寫法,記得前面不要加上eq,這裡不談code,如果加上eq的話這裡就有兩個動詞放在一起,不合文法啦!這裡期待(expect)收到的結果就是@post = Post.new
接著是create、update還有、destroy
(redirect還有render_template的地方將會在講完create和update之後一起講)
describe "POST #create" do
context "with valid params" do
it "should create a post" do
valid_post_params = {title: "Valid title", content: "Valid content"}
expect {
post :create, post: valid_post_params
}.to change(Post, :count).by(1)
end
it "should redirect to post" do
valid_post_params = {title: "Valid title", content: "Valid content"}
post :create, post: valid_post_params
expect(response).to redirect_to Post.last
end
end
context "with invalid params" do
it "should not create a post" do
invalid_post_params = {title: "", content: "doesn't matter"}
expect {
post :create, post: invalid_post_params
}.not_to change(Post, :count)
end
it "should render edit template" do
invalid_post_params = {title: "", content: "doesn't matter"}
post :create, post: invalid_post_params
expect(response).to render_template(:new)
end
end
end
首先先看到context,這個一樣是方便給人看的,你可以在裡面打任何字,但通常我們會在裡面做條件的設定,
像這裡的條件就是新增post是valid的情況下posts controller會做些什麼。
為了做出區隔來,所以這裡也新增了對post的validation:
class Post < ActiveRecord::Base
validates :title, presence: true, length: {maximum: 50}
end
> validation將會在後面對model上下其手時才會深入探討,不過其實也不難理解,這裡就是驗證title要存在,且字元數要小於50
再來就是expect後發這個區塊裡的東西
post :create, post: valid\_post\_params
這裡多了一個參數是post,因為我們在執行post的http method時,就是告訴它對著post:
把東西丟進去就對了
.to change(Post, :count).by(1)
很直觀的,如果create成功,那Post的count就會自動多一個。.not\_to change(Post, :count)
沒create成功,not_to 就是to的反向(這裡也可以改寫成.to change(Post, :count).by(0)
,只是我比較喜歡前面的寫法)你不用自己去產生count這個欄位,可以開Rails console去玩玩看就知道了,電腦會自動幫你計算現在有多少個
下一個就是update了,弄懂create之後,update的測試其實也不難,
我們要測試的就是有沒有__成功更新物件的值而已__
describe "PUT #update" do
let(:post) { Post.create(title: "For test", content: "lalala") }
context "with valid params" do
it "should update post's attributes" do
valid_post_params = {title: "Changed title" }
put :update, id: post, post: valid_post_params
post.reload
expect(post.title).to eq valid_post_params[:title]
end
it "should redirect to show page" do
valid_post_params = {title: "Changed title" }
put :update, id: post, post: valid_post_params
expect(response).to redirect_to(post)
end
end
context "with invalid params" do
it "should not update post's attributes" do
invalid_post_params = {title: "", content: "doesn't matter"}
put :update, id: post, post: invalid_post_params
post.reload
expect(post.content).not_to eq invalid_post_params[:content]
end
it "should render edit template" do
invalid_post_params = {title: "", content: "doesn't matter"}
put :update, id: post, post: invalid_post_params
expect(response).to render_template(:edit)
end
end
end
這是今天最後一個新朋友,叫做let
let
在使用的時候才會用到,所以不直接用variable來做(每一個it都是一個新的)
但是用let!
可以讓他在宣告的時候就直接執行這一行
也因為他被叫到才會執行的特性,我們更新完它之後要對他執行reload才能看到update後的結果。
這裡補充一下redirect和render_template都是要藉由
response
來完成,
其實我們對網站最基本的互動不外乎就是丟request還有收到response,所以蠻自然的,不需要去特別強記
而render跟redirect是在controller就曾經做過的事情,現在都串起來啦!
最後則是destroy,相較前兩個action,destroy顯得簡單很多
describe "DELETE #destroy" do
let!(:post) { Post.create(title: "OK bye", content: "doesn't matter") }
it "should delete a post" do
expect {
delete :destroy, id: post
}.to change(Post, :count).by(-1)
end
it "should redirect to index page" do
delete :destroy, id: post
expect(response).to redirect_to(posts_url)
end
end
這裡也很簡單,比較特別的地方就是我們在let後方加了一個Bang!!!!
這代表它不會等到我們呼叫它才執行,而是會直接執行,
不這樣做的話,刪除跟建立的動作會同時進行,這樣change的值其實是0,測試自然也沒有用了。
這篇的篇幅有點超過預期,不過之後再修改rspec就不會像現在一樣講的這麼囉唆,
我們可以發現這支spec裡面其實有很多重複的code,
之後會簡單的重構一下他,
但並不會徹底的把所有重複的code都拿掉(ex: shared example),
經驗法則告訴我,把一樣的東西抽出來的時候固然很爽看起來很乾淨,
可是當要找bug滑來滑去的時候就煩死人了,
最好還是囉唆一點能一眼就看出來這段test在測試什麼會比較好。
fabrication跟faker都還沒派上用場,先別急,以後會有機會介紹他們,
今天就先到這裡啦!
ps.
現在主流的側法應該是放在spec/request底下去測試,不過我認為剛開始先從controller開始會比較容易記住。
久了之後自然會發現其實從request去測試是比較符合實際情形的。