JT's Blog

Do the right things and do the things right.

RSpec

| Comments

介紹

  • 用來測試 Ruby / Rails 的測試工具
  • 程式開發過程,可以確保程式的正確性,也可以讓開發人員有效的重構
  • 當成一種程式文件

慣例

  1. 一個 .rb 要搭配同名的 _spec.rb
  2. #method 表示 instance method, .method 表示class method
  3. 在 Rails 寫 RSpec 建議順序 Model > Controller > View

安裝

1
2
3
4
5
6
# Gemfile
group :development, :test do
  gem 'rspec-rails'
end

$ rails generate rspec:install

設定取消自動產生 rspec

1
2
3
4
5
6
7
8
9
10
11
12
# config/application.rb

class Application < Rails::Application
  config.generators do |g|
    g.controller_specs false
    g.model_specs false
    g.view_specs false
    g.helper_specs false
    g.routing_specs false
    g.request_specs false
  end
end

測試指令

  • rspec 全部測試
  • rspec spec/controllers 整個資料夾測試
  • rspec filename_spec.rb 單一檔案測試
  • rspec filename_spec.rb:linenumber 執行某行之後的測試

輸出

預設不產出文件

  • 產生document 並輸出 specdoc 文件
1
2
  $ rspec spec --format documentation --out rspec_result.txt
  $ rspec spec -fd -o rspec_result.txt
  • 輸出 html 文件
1
2
  $ rspec spec --format html --out rspec_result.html
  $ rspec spec -fh -o rspec_result.html

Describe / Context / it

RSpec 的主要區塊,所有的測試都寫在 describe 裡面,裡面則包含多個 context、it 測試

descirbe 超過 40 個字,就需要使用 context、it 分段,讓你的測試更明確、有條理,保持可讀性

Before & After

同unit test 的setup 與teardown

  • before(:each) 每段it之前執行
  • before(:all) 整段describe前只執行一次
  • after(:each) 每段it之後執行
  • after(:all) 整段describe後只執行一次

let & let!

  • let 用到的時候再會被執行
  • let! 每個測試前都會執行

建議用 let 代替 before 來建立 instance variable

因為 let 有 lazy load 特性,只在測項第一次用到該 variable 時被執行,並且會 cache 直到該測項結束

這個技巧在產生 database 內容以測試 query 和 scope 時十分好用

Controller Specs

Examples

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require 'rails_helper'

describe TeamsController, :type => :controller do
  describe "GET index" do
    it "assigns @teams" do
      team = Team.create
      get :index
      expect(assigns(:teams)).to eq([team])
    end

    it "renders the index template" do
      get :index
      expect(response).to render_template("index")
    end
  end
end

Matchers

  • render_template
1
expect(response).to render_template(:new)
  • redirect_to
1
expect(response).to redirect_to(location)
  • have_http_status
1
2
3
4
5
6
7
8
9
10
expect(response).to have_http_status(:created)

# success 就有四種寫法 XD
expect(response).to have_http_status(200)
expect(response.status).to eq(200)
expect(response).to have_http_status(:ok)
expect(response).to have_http_status(:success)

# page not found
expect(response).to have_http_status(404)
  • be_a_new
1
2
3
# assigns(:widget) 表示 action 要回傳的 @widget
# be_a_new 只是否等同 Widget.new
expect(assigns(:widget)).to be_a_new(Widget)

Model Specs

Examples

使用 reload 重新載入資料

1
2
3
4
5
6
7
8
9
10
11
12
require "rails_helper"

describe Post, :type => :model do
  context "with 2 or more comments" do
    it "orders them in reverse chronologically" do
      post = Post.create!
      comment1 = post.comments.create!(:body => "first comment")
      comment2 = post.comments.create!(:body => "second comment")
      expect(post.reload.comments).to eq([comment2, comment1])
    end
  end
end

取消 transactions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require "rails_helper"

RSpec.configure do |c|
  c.use_transactional_examples = false
  c.order = "defined"
end

describe Widget, :type => :model do
  it "has none to begin with" do
    expect(Widget.count).to eq 0
  end

  it "has one after adding one" do
    Widget.create
    expect(Widget.count).to eq 1
  end

  # 關閉 transactions,剛剛建立的 Widget 就不會消失
  it "has one after one was created in a previous example" do
    expect(Widget.count).to eq 1
  end

  after(:all) { Widget.destroy_all }
end

Routing Specs

1
2
3
4
5
6
7
8
9
10
11
describe "posts", :type => :routing do

  expect(:get => "/events").to route_to("events#index")
  expect(:get => "/widgets/1/edit").not_to be_routable

  expect(:get => "/posts/1").to route_to(
        :controller => "posts",
        :action => "show",
        :id => "1"
        )
end

:type => ?

設定 type 為 controller / model / routing 是 rspec metadata,可以讓 rspec 知道目前的 type

1
2
3
desc "posts", :type => :controller do
  # ...
end

當然也可在 rails_helper 設定 type 參考檔案目錄

1
2
# spec/rails_helper
config.infer_spec_type_from_file_location!

只要把檔案放在對應的資料夾,即代表該檔案的 type

1
2
3
4
# spec/controllers/
desc "posts" do
  # ...
end

進階

RSpec Double / Stubs / Mocks

Reference

Comments