4. 用戶驗收測試
4-1 目標
單元測試是針對單一類別和方法進行測試,但是對一個系統來說,單一組件運作正常,不代表整個系統運作正常。就像這張 GIF 圖一樣,鎖是正常運作的,門也是正常運作的,但是組起來不OK啊。
剛才我們只測試了 Parking model,但是整個 Rails 還包括 Router、Controller 和 View。
這時候可以撰寫所謂的用戶驗收測試,在 Rspec 中叫做 Feature Spec。這會模擬用戶操作瀏覽器的行為,來對 Rails 進行整合性的測試。
在稍後的補充我們會提到也可以針對 Router、Controller 和 View 進行做單元測試,但是效益不大,因為用 Model Spec 和 Feature Spec 就可以大致涵蓋我們的測試需要了。
4-2 Capybara 安裝
Capybara 這個 gem 會用來搭配 Rspec 進行 Feature Spec 測試,請先安裝:
group :development, :test do
gem 'rspec-rails'
+ gem 'capybara'
gem 'byebug', platform: :mri
end
執行 bundle
編輯 spec/rails_helper.rb
# (略)
require 'spec_helper'
require 'rspec/rails'
+ require 'capybara/rails'
+ require 'capybara/rspec'
4-3 測試「一般費率」繳費流程
執行 mkdir spec/features
建立驗收測試的目錄
新增 spec/features/guest_spec.rb
require 'rails_helper'
feature "parking", :type => :feature do
scenario "guest parking" do
# Step 1
visit "/" # 瀏覽首頁
# save_and_open_page # 這會存下測試當時的 HTML 頁面
expect(page).to have_content("一般費率") # 檢查 HTML 中要出現 "一般費率" 文字
# Step 2
click_button "開始計費" # 按這個按鈕
# Step 3:
click_button "結束計費" # 按這個按鈕
# Step 4: 看到費用畫面
expect(page).to have_content("¥2.00") # 檢查 HTML 中要出現 ¥2.00 文字
end
end
執行 rspec spec/features/guest_spec.rb
測試會通過。
你可以試試看稍微更動畫面的文字,就會發現測試失敗。例如拿掉開始計費按鈕之類的。
說明一下:
feature
的作用等同於describe
,scenario
的作用等同於it
- 這裡用了
have_content
來檢查指定文字有沒有出現在 HTML 裡面。Web 應用的測試,沒辦法去完整比對 HTML 字串,因為字串比對差一個空白就不一樣。如果說設計師稍微多加一個<br>
就要改測試,這樣就太累了,測試會太敏感。所以我們只能檢查說 HTML 裡面有出現我們希望要有的關鍵字。 - 注意到我們不需要再重復測試金額對不對了,在前幾章單元測試中我們已經確定這部分的元件正常。Feature Spec 的重點在於檢查東西(Model + Controller + View)接起來有沒有正常運作。
- 被註解掉的
save_and_open_page
會存下測試當時的 HTML 頁面,除錯的時候可以使用。如果打開的話,跑測試會出現:
你會找到存下來的 HTML 檔名,執行 open tmp/capybara/capybara-xxxxxx.html
就可以直接用預設瀏覽器打開看看。
4-4 測試註冊流程
測試用戶註冊的流程:
require 'rails_helper'
feature "register and login", :type => :feature do
scenario "register" do
visit "/users/sign_up" # 瀏覽註冊頁面
expect(page).to have_content("Sign up")
within("#new_user") do # 填表單
fill_in "Email", with: "[email protected]"
fill_in "Password", with: "12345678"
fill_in "Password confirmation", with: "12345678"
end
click_button "Sign up"
# 檢查文字。這文字是 Devise 預設會放在 flash[:notice] 上的
expect(page).to have_content("Welcome! You have signed up successfully!")
# 檢查資料庫裡面最後一筆真的有剛剛填的資料
user = User.last
expect(user.email).to eq("[email protected]")
end
end
這裡我們模擬用戶填寫表單送出的情況,用fill_in
可以填入值。最後除了檢查畫面上有沒有出現想要的關鍵字,也可以檢查資料有沒有正確存進資料庫。
4-5 測試登入登出流程
測試用戶登入登出的流程:
feature "register and login", :type => :feature do
+ scenario "login and logout" do
+ # 先建立一個測試用的用戶在資料庫
+ user = User.create!( :email => "[email protected]", :password => "12345678")
+
+ visit "/users/sign_in"
+
+ within("#new_user") do
+ fill_in "Email", with: "[email protected]"
+ fill_in "Password", with: "12345678"
+ end
+
+ click_button "Log in" # 點擊登入按鈕
+ expect(page).to have_content("Signed in successfully")
+
+ click_link "登出" # 點擊主選單的登出超連結
+ expect(page).to have_content("Signed out successfully")
+ end
end
4-6 測試「短期費率」流程
要操作「短期費率」必須登入。但是登入剛剛已經測試過了,不需要再用 capybara 再走一次。Devise 有提供測試用的 sign_in
方法,請修改 spec/rails_helper.rb
:
RSpec.configure do |config|
+ config.include Devise::Test::ControllerHelpers, type: :controller
+ config.include Devise::Test::ControllerHelpers, type: :view
+ config.include Devise::Test::IntegrationHelpers, type: :feature
# (略)
新增 spec/features/short_term_spec.rb
require 'rails_helper'
feature "parking", :type => :feature do
scenario "short-term parking" do
user = User.create!( :email => "[email protected]", :password => "12345678")
sign_in(user) # 這樣就可以登入了
visit "/"
choose "短期費率" # 選 radio button
click_button "開始計費"
click_button "結束計費"
expect(page).to have_content("¥2.00")
end
end
執行 rspec spec/features/short_term_spec.rb
測試通過。
這裡用了 choose
來對 Radio 按鈕做選擇,在 capybara 中還有提供其他方法針對不同表單元件做操作,例如:
- check “核選方塊名稱”
- uncheck “核選方塊名稱”
- select “選項名稱”, :from => “下拉選單名稱”
- attach_file 上傳檔案
詳細用法請參考 capybara 文件。
4-7 故意修改 Model API
做到這裡,假設我們想回來修改一下 Model,這個 calculate_amount
其實可以放在回呼(callback)裡面,這樣只要 save
存進資料庫時就會自動計算,不需要手動呼叫calculate_amount
,也不需要檢查 amount
必填了。然後我們有點故意地順便改個方法名稱:
+ before_validation :setup_amount
- def calculate_amount
+ def setup_amount
# (略)
def validate_end_at_with_amount
- if ( end_at.present? && amount.blank? )
- errors.add(:amount, "有結束時間就必須有金額")
- end
修改 spec/models/parking_spec.rb
中把所有 calculate_amount
改成 save
(有多處請都修改到):
- @parking.calculate_amount
+ @parking.save
之前提過測試案例跟案例之間是互相獨立、不會互相影響的(這樣測試失敗時,才不用懷疑是不是被別的測試影響到)。每個測試案例,裡面需要的物件會重新建立。跑自動化測試用的資料庫跟開發用的不一樣(在config/database.yml裡面會設定
test
環境用的資料庫,並用db/schema.rb
的定義建立測試用資料庫)。另外,如果你有save
存進資料庫的話,Rails 也會在跑完測試案例後,自動復原砍掉該測試案例新增的資料。
刪除檢查 amount 必填的測試:
- it "is invalid without amount" do
- parking = Parking.new( :parking_type => "guest",
- :start_at => Time.now - 6.hours,
- :end_at => Time.now)
- expect( parking ).to_not be_valid
- end
然後再跑一次 rspec spec/models/parking_spec.rb
測試全部通過。
但是呢,這個 Parking model 元件是好的。但是其實跑驗收其實是爛的,因為我們改了 Parking model 的 API。
執行 rspec spec/features/guest_spec.rb
:
這告訴我們單元測試的局限性,驗收測試幫助我們檢查了整個系統整合起來是正常的。
修改 app/controllers/parkings_controller.rb
,砍掉 @parking.calculate_amount
:
def update
@parking = Parking.find(params[:id])
@parking.end_at = Time.now
- @parking.calculate_amount
@parking.save!
redirect_to parking_path(@parking)
end
再跑一次 rspec spec/features/guest_spec.rb
就通過了。
剛剛都是跑單個測試檔案,如果要跑全部測試,可以執行 rake spec
4-8 小結
剛剛都是跑單個測試檔案,要一次跑全部的測試的話,請執行 rake spec
我們會在 git push 前,盡量跑過一次全部的測試進行檢查。實際部署上 production 伺服器前,也一定會執行 rake spec
檢查所有測試都必須通過。一個專案如果有良好的測試涵蓋,那麽透過執行自動化測試可以大幅減少人工測試的時間,確保這次的修改一切功能正常,增加成功上線的把握。
至於驗收測試什麽時候寫? 要寫多少呢?
- 相對於單元測試,驗收測試通常是在功能完成之後才撰寫,主要的目的其實是做「回歸測試(Regression Testing」,旨在檢驗軟體原有功能在修改後是否保持正常,每次部署新版本上線前,會跑全部的測試做回歸測試檢查,看看有沒有東西被弄壞了,是一種投資未來的測試。
- 單元測試通常是跟該功能一起由開發者完成,驗收測試則會另外開任務,並可能由另一個開發者(或專門的測試工程師)來完成會更好。
- 撰寫驗收測試會花額外的時間,而且比較脆弱。因為頁面流程一改、或是改個文案,測試就得跟著改。因此通常我們只會針對網站最常用的功能(Happy Path)來撰寫驗收測試,這樣投資報酬率最高。
- 驗收測試很難完全取代人工驗證,因為有太多東西是很難驗證的,例如 CSS、畫面顏色、按鈕位置等等。如果要做到自動檢查畫面完全一模一樣,會耗費很大的維護成本。
- 因此相對於新創公司還在時常變動功能的軟體,成熟期的軟體比較會投資在完整的驗收測試,特別是 B2B 領域,因為軟體出錯對客戶造成的損失是很大的。