iT邦幫忙

DAY 11
0

從想法到快速實作的捷徑:Rails系列 第 11

[ Day 11 ][ Dev ] 更進一步認識Rails - rspec

之前看到rails 101還有rails 101 s裡面都寫到想要熟悉restful,resource是什麼,

最好的方法就是多寫幾次然後背起來。

坦白說的確是這樣沒錯,

就像你要去做最大概似估計值,你一定要先背起來每個分配的pdf長怎樣才能過關斬將。

不過單純的背誦其實是很無聊的,不如借rspec寫test來順便練習一下。

首先要明白為什麼要寫測試:

  1. 因為手動去測試所有頁面,是很麻煩的事情

  2. 這樣在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是如何進行測試的:

  1. 撰寫測試碼(在我們剛剛新建的spec檔案裡面,他們都位於spec/底下)

  2. bundle exec rspec spec/ 這一行就會執行所有的測試

  3. 如果想要只執行某隻檔案也行bundle exec rspec spec/models/post\_spec.rb,也可以指定想要執行的行數n :n

接著就可以來寫測試啦!

require 'rails_helper'

RSpec.describe PostsController, :type => :controller do

end
  • 最上方會把rails_helper給require進來,裡面包含一些設定檔案還有其他要一起require的東西XD

有興趣的話可以自己看看。

  • 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去測試是比較符合實際情形的。


上一篇
[ Day 10 ][ Dev ] 從開發Po文功能認識MVC #3 完成post的CRUD
下一篇
[ Day 12 ][ Dev ] rspec - 用Factory造測試用的假物件
系列文
從想法到快速實作的捷徑:Rails30

尚未有邦友留言

立即登入留言