iT邦幫忙

2022 iThome 鐵人賽

DAY 22
0
Modern Web

Rails,我要進來囉系列 第 22

第二十二天:Rails 官方內建的測試框架 - Minitest

  • 分享至 

  • xImage
  •  

開場白

鼬~~哩賀,我是寫程式的山姆老弟,昨天跟大家一起看了 Rails 的 i18n 怎麼做,今天來看官方推薦的 Rails 測試方法怎麼說,夠夠~

https://raw.githubusercontent.com/shrimp509/my-img-host/master/relacs-studio/Rails%E6%88%91%E8%A6%81%E9%80%B2%E4%BE%86%E5%9B%89/day22-1.png

Rails 內建的測試

老實說我沒有用過 Rails 內建的測試,之前只有用過 Rspec 而已,先來看看預設的有什麼不一樣

Rails 內建的測試套件為 minitest,預設被包在 ActiveSupport 裡,在建立專案時就一起安裝了

內建的測試會自動建立以下資料夾在 test/ 底下,最主要分為

  • controllers:測試 controller 層各 API 或 頁面
  • models:測試 model 層商業邏輯
  • system:開瀏覽器,以使用者的角度來做測試,預設使用 selenium + chrome

次要的測試還有

  • helpers: 測試 controller 會用到的自訂 helpers 功能
  • integration: 跨 Controller 之間的測試
  • mailers: 測試 ActionMailer 的 mailer 運作
  • channels: 測試 ActionCable 的 websocket 運作

輔助的測試

  • fixtures: 事先定義好測試用的資料

簡易的 Controller Test

class ArticlesControllerTest < ActionDispatch::IntegrationTest
	# called before every single test
  setup do
    @article = articles(:one)
  end

  # called after every single test
  teardown do
    # when controller is using cache it may be a good idea to reset it afterwards
    Rails.cache.clear
  end

  test "should get index" do
    get articles_url
    assert_response :success
  end
	
	test "should create article" do
	  assert_difference("Article.count") do
	    post articles_url, params: { article: { body: "Rails is awesome!", title: "Hello Rails" } }
	  end
	
	  assert_redirected_to article_path(Article.last)
	end

	test "should show article" do
	  article = articles(:one)
	  get article_url(article)
	  assert_response :success
	end

	test "should destroy article" do
	  article = articles(:one)
	  assert_difference("Article.count", -1) do
	    delete article_url(article)
	  end
	
	  assert_redirected_to articles_path
	end

	test "should update article" do
	  article = articles(:one)
	
	  patch article_url(article), params: { article: { title: "updated" } }
	
	  assert_redirected_to article_path(article)
	  # Reload association to fetch updated data and assert that title is updated.
	  article.reload
	  assert_equal "updated", article.title
	end
end

簡易的 System Test

require "application_system_test_case"

class ArticlesTest < ApplicationSystemTestCase
  test "viewing the index" do
    visit articles_path
    assert_selector "h1", text: "Articles"
  end

	test "should create Article" do
	  visit articles_path
	
	  click_on "New Article"
	
	  fill_in "Title", with: "Creating an Article"
	  fill_in "Body", with: "Created this article successfully!"
	
	  click_on "Create Article"
	
	  assert_text "Creating an Article"
	end
end

模擬手機排版,測試 RWD

預設在 test/application_system_test_case.rb 有定義 screen_size 為 1400 x 1400

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
end

還可以自己新增一個 test/mobile_system_test_case.rb 定義出手機螢幕大小

require "test_helper"

class MobileSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :chrome, screen_size: [375, 667]
end

其他 system test 再去繼承 MobileSystemTestCase 就可以測試 RWD

require "mobile_system_test_case"

class PostsTest < MobileSystemTestCase

  test "visiting the index" do
    visit posts_url
    assert_selector "h1", text: "Posts"
  end
end

簡易的 Integration Test

require "test_helper"

class BlogFlowTest < ActionDispatch::IntegrationTest
  test "can see the welcome page" do
    get "/"
    assert_select "h1", "Welcome#index"
  end

	test "can create an article" do
	  get "/articles/new"
	  assert_response :success
	
	  post "/articles",
	    params: { article: { title: "can create", body: "article successfully." } }
	  assert_response :redirect
	  follow_redirect!
	  assert_response :success
		assert_select "p", "Article was successfully created."
	end
end

什麼時候要選 System Test、什麼時候選 Integration Test

從上面的例子看起來,System TestIntegration Test 好像很相似,他們之間最大的差別是,System Test 是實際開瀏覽器起來執行的測試,而 Integration Test 沒有開瀏覽器

這個有開瀏覽器的差別,讓他們有了不同的使用情境,有開瀏覽器的 System Test,就可以像上面提到的調整視窗大小,來測試 RWD 元件是不是正常運作,還有,因為有瀏覽器就可以測試 Javascript 是否正常運作,例如 bootstrap 的 navbar 元件,在小視窗時會需要 jquery 來展開 navbar,這就是 System Test 才能做到的測試

Integration Test 雖然不是透過開啟瀏覽器來做測試,但還是能跟靜態的 HTML 元件互動測試,因為 Rails 是 Server Side Render 麻,呼叫完 Controller Action 之後就會得到 HTML 了,瀏覽器並非必要的

Integration Test 主要是用來測試,重要的 Controller Actions 之間的運作邏輯是不是正常、API 之間的互動是否正常,像是能不能依照我們需要的欄位填入,來建立一篇部落格文章

Integration Test 能做到的,System Test 也能做到,所以如果你都要全部都是透過 System Test 來測也是可以,不過 System Test 因為要開瀏覽器,整個測試下來就會比較慢,所以就看個人囉

可以自定義 Testing Helper 減少重複

另外在 test 資料夾內,創一個 test_helper.rb 檔案,然後讓 ActionDispatch::IntegrationTest 去 include 我們擴充的 module

# test/test_helper.rb
module SignInHelper
  def sign_in_as(user)
    post sign_in_url(email: user.email, password: user.password)
  end
end

class ActionDispatch::IntegrationTest
  include SignInHelper
end

在需要使用 sign_in_as 的地方,去 require 我們剛建立的 test_helper,就可以直接使用

require "test_helper"

class ProfileControllerTest < ActionDispatch::IntegrationTest

  test "should show profile" do
    # helper is now reusable from any controller test case
    sign_in_as users(:david)

    get profile_url
    assert_response :success
  end
end

如果 test_helper 很多的話,也可以再把結構拉出來,test/test_helpers/xxx_helper.rb,然後直接在 test/test_helper.rb 去 eager requiring

# test/test_helper.rb
Dir[Rails.root.join("test", "test_helpers", "**", "*.rb")].each { |file| require file }

統整 assertions

Minitest 內建的 assertions

  • assert / assert_not
  • assert_equal / assert_not_equal
  • assert_same / assert_not_same
  • assert_nil / assert_not_nil
  • assert_empty / assert_not_empty
  • assert_match / assert_not_match
  • assert_includes / assert_not_includes
  • assert_in_delta / assert_not_in_delta
  • assert_in_epsilon / assert_not_in_epsilon
  • assert_throws
  • assert_raises
  • assert_instance_of / assert_not_instance_of
  • assert_kind_of / assert_not_kind_of
  • assert_respond_to / assert_not_respond_to
  • assert_operator / assert_not_operator
  • assert_predicate / assert_not_predicate

Rails 擴充的 assertions

  • assert_difference / assert_not_difference
  • assert_changes / assert_not_changes
  • assert_nothing_raised
  • assert_recognizes
  • assert_generates
  • assert_response
  • assert_redirected_to

Views

  • assert_selector
  • assert_text
  • assert_select_email
  • assert_select_encoded
  • css_select
  • assert_dom_equal

ActionMailer

  • assert_emails
  • assert_enqueued_emails

ActionJob

  • assert_enqueued_with

ActionCable

  • assert_reject_connection
  • assert_has_stream
  • assert_has_stream_for
  • assert_broadcast_on

其他值得注意的點

  • Fixture 的 yaml 檔支援 ERB

    <% 1000.times do |n| %>
    user_<%= n %>:
      username: <%= "user#{n}" %>
      email: <%= "user#{n}@example.com" %>
    <% end %>
    
  • 可自定義測試的 Worker 數量,加快測試速度

    • 主要是用在不同機器測試時的效率
    • 不指定的話,預設是會根據 CPU 核心數自動分配
    • 可用環境變數,e.g. PARALLEL_WORKERS**=**15 bin/rails test
  • 在測試中,可以透過 response@response 得到 response 的內容,適合用在 debugging

    • 其他可以用的還有 @request, @controller, flash, cookies, session, ,

Minitest vs. Rspec

Minitest

require "test_helper"

class ArticleTest < ActiveSupport::TestCase
 setup do
  @article = articles(:one)
 end

 test 'title cant be blank' do
  @article.title = nil
  assert @article.valid?
  assert_equal ["can't be blank"], @article.errors[:title]
 end

 test "body cant be blank" do
  @article.body = nil
  assert @article.valid?
  assert_equal ["can't be blank"], @article.errors[:body]
 end
end

Rspec

require 'rails_helper'

RSpec.describe Article, type: :model do

 context 'cant be blank' do
  article = Article.new
  article.valid?
  it 'must have a title' do
   expect(article.errors.messages[:title]).to include("can't be blank")
  end

  it 'must have a body' do
   expect(article.errors.messages[:body]).to include("can't be blank")
  end
 end
end

最後我從網路上文章找到範例,並改一下,讓他們變得有點像,然後來比較看看兩者的差異,Minitest 其實就是一般的 Ruby code,只是有包語法糖,讓 def test_title_cant_be_blank 可以寫成 test 'title cant be blank' do 而已

而 Rspec 比起 Minitest 來說,可以寫出更有結構的測試,可以指定不同的 context、每個 context 也可以有各個 unit test

總結

我還真的是第一次用 Minitest,覺得挺新奇的,不過從各個來源看來,Ruby 或 Rails 的開發者應該都比較偏好 Rspec,我也不清楚為何 Rails 不預設用 Rspec 作為測試的框架就好,似乎有一段糾葛?

Minitest 確實用起來就是跟寫 Ruby 一樣,差不多就是只要會寫 Ruby、就會寫 Minitest,而 Rspec 就是好像在寫新的語言的感覺,學習成本比較高一點

總之今天就是這樣拉,我們明天見~


上一篇
第二十一天:網頁如何支援多國語言?Rails 的 i18n
下一篇
第二十三天:Rails 的 Command Line 工具,發現好多酷東東
系列文
Rails,我要進來囉30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言