[譯]我們?nèi)绾螠y(cè)試 Rails 應(yīng)用

Josh Steiner January 14, 2014

我常常被問(wèn)到哨查,怎樣開(kāi)始測(cè)試 Rails 程序江掩。其實(shí)最為一個(gè)測(cè)試新手镊掖,最難得地方在于你不知道一些專業(yè)術(shù)語(yǔ)或者該問(wèn)怎樣的問(wèn)題丘喻。下面所寫(xiě)的是一些概覽诵竭,關(guān)于我們使用什么工具话告,為什么使用這些工具,和一些需要牢記在心的建議卵慰。

RSpec

我們用 RSpec 而不是 Test::Unit沙郭,是因?yàn)檎Z(yǔ)法更友好蛮瞄,可讀性更高恕刘。當(dāng)然昔汉,你可以花幾天時(shí)間爭(zhēng)論到底該用哪個(gè)框架箱蝠,每個(gè)框架都有自己的優(yōu)點(diǎn)蒙谓。最主要的是你在用他們進(jìn)行測(cè)試钙畔。

Feature specs

Feature specs千元,可以測(cè)試你整個(gè)程序的高級(jí)測(cè)試工具哈误,保證每個(gè)部件都工作正常暖眼,挺贊的惕耕。他是從用戶的角度編寫(xiě)的,比如用戶點(diǎn)擊或者填寫(xiě)表單罢荡。我們用 RSpec 和 Capybara赡突,他們?cè)试S你寫(xiě)照這樣編寫(xiě)可以和網(wǎng)頁(yè)進(jìn)行交互的測(cè)試。

這是一個(gè) RSpec feature 測(cè)試的栗子:

# spec/features/user_creates_a_foobar_spec.rb

feature 'User creates a foobar' do
  scenario 'they see the foobar on the page' do
    visit new_foobar_path

    fill_in 'Name', with: 'My foobar'
    click_button 'Create Foobar'

    expect(page).to have_css '.foobar-name', 'My foobar'
  end
end

這個(gè)測(cè)試区赵,模擬了一個(gè)用戶打開(kāi)新建foobar的表單惭缰,填入信息,點(diǎn)擊“Create”笼才。這個(gè)測(cè)試然后假設(shè)頁(yè)面正如期待漱受,正確地顯示剛才創(chuàng)建的foobar

這些對(duì)于測(cè)試高級(jí)功能來(lái)說(shuō)很棒,但是記住昂羡,feature specs 跑起來(lái)很慢絮记。在用 Capybara 測(cè)試應(yīng)用所有的可能路徑的時(shí)候,把測(cè)試的邊緣情況留模型虐先,視圖怨愤,控制器的 sepecs。

我傾向于關(guān)于怎么區(qū)分 Rspec 和 Capybara 方法的不同點(diǎn)蛹批。Capybara 方法實(shí)際上是和頁(yè)面進(jìn)行交互撰洗,比如點(diǎn)擊,表單操作腐芍,或者在頁(yè)面上查找元素差导。你可以在 Capybara 的findersmatchers猪勇,和 actions 查看更多文檔设褐。

Model specs

Model specs 經(jīng)常被用來(lái)測(cè)試系統(tǒng)中較小的部件,比如類或方法泣刹,這點(diǎn)和單元測(cè)試挺相似的助析。有時(shí),他們也會(huì)和數(shù)據(jù)庫(kù)進(jìn)行交互项玛。他們運(yùn)行起來(lái)很快貌笨,也可以處理正在測(cè)試系統(tǒng)(system under test)中的一些邊緣情況。

在 RSpec 中襟沮,他們看起來(lái)像這樣:

# spec/models/user_spec.rb

# Prefix class methods with a '.'
describe User, '.active' do
  it 'returns only active users' do
    # setup
    active_user = create(:user, active: true)
    non_active_user = create(:user, active: false)

    # exercise
    result = User.active

    # verify
    expect(result).to eq [active_user]

    # teardown is handled for you by RSpec
  end
end

# Prefix instance methods with a '#'
describe User, '#name' do
  it 'returns the concatenated first and last name' do
    # setup
    user = build(:user, first_name: 'Josh', last_name: 'Steiner')

    # excercise and verify
    expect(user.name).to eq 'Josh Steiner'
  end
end

為了維護(hù)的可讀性锥惋,確保你寫(xiě)的測(cè)試是 Four Phase Test

The Four-Phase Test is a testing pattern, applicable to all programming languages and unit tests (not so much integration tests).
It takes the following general form:

test do
  setup
  exercise
  verify
  teardown
end

比如:

it 'encrypts the password' do
  user = User.new(password: 'password')
  user.save
  user.encrypted_password.should_not be_nil
end

Controller specs

當(dāng)通過(guò)一個(gè)控制器測(cè)試多個(gè)路徑的時(shí)候,我們喜歡用 controller specs 而不是 feature specs开伏,因?yàn)樗芸彀虻覍?xiě)起來(lái)容易。

一個(gè)好的測(cè)試驗(yàn)證的 use case:

# spec/controllers/sessions_controller_spec.rb

describe 'POST #create' do
  context 'when password is invalid' do
    it 'renders the page with error' do
      user = create(:user)

      post :create, session: { email: user.email, password: 'invalid' }

      expect(response).to render_template(:new)
      expect(flash[:notice]).to match(/^Email and password do not match/)
    end
  end

  context 'when password is valid' do
    it 'sets the user in the session and redirects them to their dashboard' do
      user = create(:user)

      post :create, session: { email: user.email, password: user.password }

      expect(response).to redirect_to '/dashboard'
      expect(controller.current_user).to eq user
    end
  end
end

View specs

View specs 對(duì)于測(cè)試在模板中根據(jù)條件顯示不同信息來(lái)說(shuō)非常有用固灵,但是很多開(kāi)發(fā)者卻忘了這個(gè)捅伤,而用 feature specs。然后絞盡腦汁為什么他們花了那么長(zhǎng)時(shí)間跑測(cè)試巫玻。當(dāng)然丛忆,你可以用 feature spec 覆蓋每一個(gè)條件視圖,但我更喜歡像這樣使用 view specs :

# spec/views/products/_product.html.erb_spec.rb

describe 'products/_product.html.erb' do
  context 'when the product has a url' do
    it 'displays the url' do
      assign(:product, build(:product, url: 'http://example.com')

      render

      expect(rendered).to have_link 'Product', href: 'http://example.com'
    end
  end

  context 'when the product url is nil' do
    it "displays 'None'" do
      assign(:product, build(:product, url: nil)

      render

      expect(rendered).to have_content 'None'
    end
  end
end

FactoryGirl

當(dāng)編寫(xiě)測(cè)試代碼的時(shí)候仍秤,你會(huì)需要在不同場(chǎng)景往數(shù)據(jù)庫(kù)里灌數(shù)據(jù)熄诡。你可以使用內(nèi)建的User.create,但是當(dāng) model 中有很多 validations 時(shí)候诗力,這樣好乏味啊凰浮。通過(guò)User.create,即便你的測(cè)試代碼和這些驗(yàn)證無(wú)關(guān),你也不得不指定屬性來(lái)滿足 validations袜茧。最重要的是菜拓,如果后來(lái)你修改了 validations,你還要重新修改測(cè)試套件的代碼笛厦。解決方案就是用 factories(工廠纳鼎,數(shù)據(jù)生成器) 或者 fixtures(夾具)來(lái)創(chuàng)建模型。

我們更喜歡 factories(和 FactoryGirl)勝過(guò) Rails 的 fixgures递递,因?yàn)?fixtures 是神秘嘉賓喷橙。夾具讓人很難看到內(nèi)因和效果,因?yàn)椴糠诌壿嬙谀闶褂盟臅r(shí)候已經(jīng)在文件中被定義好了登舞。因?yàn)閵A具遠(yuǎn)在在測(cè)試之前就已經(jīng)被生成,他們變得很難控制悬荣。

Factories菠秒,另一方面來(lái)說(shuō),把邏輯正確地放到測(cè)試中氯迂。這讓我們很容易地看到正在發(fā)生什么践叠,而且對(duì)于不同的場(chǎng)景更加靈活。Factories 比夾具更慢嚼蚀,但是從靈活性和可讀性上來(lái)說(shuō)禁灼,這點(diǎn)犧牲是值得的!

把數(shù)據(jù)固化到數(shù)據(jù)庫(kù)也會(huì)減慢測(cè)試速度轿曙。無(wú)論合適弄捕,優(yōu)先使用 FactoryGirl 的 build_stubbed,而不是create导帝。build_stubbed 會(huì)在內(nèi)存中生成守谓,避免寫(xiě)入磁盤(pán)。如果你測(cè)試一些查詢操作(User.where(admin: true))您单,你會(huì)希望從數(shù)據(jù)庫(kù)里進(jìn)行查找斋荞,這就意味著你必須使用create

跑帶有 JavaScript 的 specs

你會(huì)最終碰到一種場(chǎng)景虐秦,你需要測(cè)試一些依賴 JavaScript 代碼的功能平酿。用默認(rèn)的驅(qū)動(dòng)跑 specs 不會(huì)執(zhí)行頁(yè)面中任何 JavaScript 代碼。

你需要兩個(gè)法器悦陋,來(lái)跑帶有 JavaScript 代碼的功能 specs

  1. 安裝一個(gè) JavaScript 驅(qū)動(dòng)

有兩種類型的 JavaScript 驅(qū)動(dòng)蜈彼。比如像 Selenium,它會(huì)打開(kāi)一個(gè) GUI 窗口瀏覽器叨恨,然后在你看著它的時(shí)候柳刮,在頁(yè)面上點(diǎn)擊。這是一個(gè)很有用的工具在測(cè)試中用來(lái)視圖化。但是不幸的是秉颗,啟動(dòng)一整個(gè) GUI 窗口瀏覽器很慢痢毒。由于這個(gè)原因,我們傾向于使用 headless 瀏覽器蚕甥。對(duì)于 Rails 來(lái)說(shuō)哪替,你可能會(huì)使用 Poltergeist 或者 [Capybara] Webkit(https://github.com/thoughtbot/capybara-webkit)。

  1. 對(duì)于特定的測(cè)試使用 JavaScript 元數(shù)據(jù)關(guān)鍵字
feature 'User creates a foobar' do
   scenario 'they see the foobar on the page', js: true do
     ...
   end
 end

用合適的關(guān)鍵字菇怀,RSpec 會(huì)根據(jù)需要運(yùn)行任何 JavaScript凭舶。

清理數(shù)據(jù)庫(kù)

默認(rèn)情況下,跑 Rails 測(cè)試的時(shí)候爱沟,Rails 會(huì)把每個(gè)場(chǎng)景都存在數(shù)據(jù)庫(kù)事務(wù)中帅霜。這就說(shuō)明,在每個(gè)測(cè)試結(jié)束的時(shí)候呼伸,Rails 會(huì)回滾所有在測(cè)試中的修改身冀。這點(diǎn)非常好,因?yàn)槲覀儾幌肴魏螠y(cè)試數(shù)據(jù)會(huì)對(duì)別的測(cè)試產(chǎn)生副作用括享。

不幸的是搂根,當(dāng)我們使用 JavaScript 驅(qū)動(dòng)的時(shí)候,這個(gè)測(cè)試實(shí)在另一個(gè)線程進(jìn)行的铃辖。這就意味著剩愧,它并沒(méi)有和程序共用同一個(gè)數(shù)據(jù)庫(kù)連接,而且為了運(yùn)行程序看到測(cè)試結(jié)果娇斩,你的測(cè)試會(huì)提交這個(gè)事務(wù)仁卷。為了解決這個(gè)問(wèn)題,我們可以允許數(shù)據(jù)庫(kù)提交這些數(shù)據(jù)成洗,然后接著五督,在每個(gè) spec 后 Truncate 數(shù)據(jù)庫(kù)。這樣會(huì)比事務(wù)慢一點(diǎn)瓶殃,所以充包,我們只在需要的時(shí)候使用 truncation

這就是 Database Cleaner 的用處遥椿。Database Cleaner 允許你配置策略基矮。我建議閱讀下 Avdi 的文章 看看血淋淋的細(xì)節(jié)。這是個(gè)極(喪)其(心)詳(补诔 )細(xì)(狂)的配置過(guò)程家浇,所以我通常在項(xiàng)目中來(lái)回拷貝這個(gè)文件,或者使用 Suspenders碴裙,這樣就可以輕易搞定了钢悲。

doubles 和 stubs 方法

double 方法可以模擬系統(tǒng)中另外一個(gè)對(duì)象点额。經(jīng)常的,你會(huì)需要一個(gè)替身莺琳,并且只測(cè)試一個(gè)屬性还棱,所以不值得加載整個(gè) ActiveRecord 對(duì)象。

car = double(:car)

當(dāng)你使用 stubs惭等,你是在告訴一個(gè)對(duì)象去響應(yīng)一個(gè)已給出的方法珍手。如果 stub 之前的 double

car.stub(:max_speed).and_return(120)

我們現(xiàn)在可以期待當(dāng)訪問(wèn) max_speed 我們的 car 對(duì)象總是返回120。臨時(shí)產(chǎn)生可以響應(yīng)一個(gè)方法的對(duì)象而且?guī)в型瑯拥囊蕾囮P(guān)系辞做,而不用系統(tǒng)中真的存在的對(duì)象琳要,這是很棒的方法!在這個(gè)栗子中秤茅,我們?cè)谝粋€(gè) double 出來(lái)的對(duì)象上進(jìn)行了 stub稚补,實(shí)際上你還可以在別的對(duì)象 stub 任何方法。

我們可以把上邊的代碼簡(jiǎn)化為這樣:

car = double(:car, max_spped: 120)

測(cè)試間諜(Test Spies)

當(dāng)測(cè)試你的程序時(shí)嫂伞,你會(huì)碰到你想驗(yàn)證當(dāng)一個(gè)對(duì)象接收到一個(gè)指定的方法的場(chǎng)景孔厉。為了遵照 Four Phase Test 的最佳實(shí)踐,我們用測(cè)試間諜帖努,這樣我們的期待會(huì)進(jìn)入最佳的 verify 狀態(tài)。之前粪般,我們用 Bourne 做到這點(diǎn)拼余,但是 RSpec 已經(jīng)在 RSpec Mocks 中包括了這項(xiàng)功能∧洞酰看看文檔中的這個(gè)栗子:

invitation = double('invitation', accept: true)

user.accept_invitation(invitation)

expect(invitation).to have_received(:accept)

用 Webmock stub 外部請(qǐng)求

基于三方服務(wù)的測(cè)試套件跑起來(lái)很慢的匙监,會(huì)因?yàn)榫W(wǎng)絡(luò)連接的斷開(kāi)導(dǎo)致失敗,而且也可能會(huì)因?yàn)榉?wù)頻率限制或者缺少沙盒環(huán)境導(dǎo)致失敗小作。

確保你的測(cè)試套件在用 Webmock stub 外部請(qǐng)求的時(shí)候亭姥,不會(huì)和三方服務(wù)交互。這個(gè)可以配置在 spec/spec_helper.rb

require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)

避免使用三方請(qǐng)求顾稀,學(xué)習(xí)怎樣 stub 外部服務(wù)請(qǐng)求达罗。

下一步

這僅僅是個(gè)如何開(kāi)始測(cè)試 Rails 應(yīng)用的概覽。為了促進(jìn)學(xué)習(xí)静秆,我非常推薦你上我們的 TDD workshop粮揉,在這里,你可以通過(guò)從零開(kāi)始建立兩個(gè) Rails 應(yīng)用抚笔,來(lái)深度學(xué)這些課程扶认。課程覆蓋到重構(gòu),為確保應(yīng)用和測(cè)試代碼的可維護(hù)性殊橙。TDDworkshop 的學(xué)生也可以介入我們的工作時(shí)間辐宾,你實(shí)時(shí)地可以問(wèn)我們攻城獅任何問(wèn)題狱从。

當(dāng)我是新手的時(shí)候我學(xué)習(xí)了這個(gè)課程,我墻裂推薦叠纹!

source: http://www.tuicool.com/articles/JrQzyi

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末季研,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子吊洼,更是在濱河造成了極大的恐慌训貌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冒窍,死亡現(xiàn)場(chǎng)離奇詭異递沪,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)综液,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)款慨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人谬莹,你說(shuō)我怎么就攤上這事檩奠。” “怎么了附帽?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵埠戳,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我蕉扮,道長(zhǎng)整胃,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任喳钟,我火速辦了婚禮屁使,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘奔则。我一直安慰自己蛮寂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布易茬。 她就那樣靜靜地躺著酬蹋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪疾呻。 梳的紋絲不亂的頭發(fā)上除嘹,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音岸蜗,去河邊找鬼尉咕。 笑死,一個(gè)胖子當(dāng)著我的面吹牛璃岳,可吹牛的內(nèi)容都是我干的年缎。 我是一名探鬼主播悔捶,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼单芜!你這毒婦竟也來(lái)了蜕该?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤洲鸠,失蹤者是張志新(化名)和其女友劉穎堂淡,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體扒腕,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绢淀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瘾腰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片皆的。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蹋盆,靈堂內(nèi)的尸體忽然破棺而出费薄,到底是詐尸還是另有隱情,我是刑警寧澤栖雾,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布楞抡,位于F島的核電站,受9級(jí)特大地震影響析藕,放射性物質(zhì)發(fā)生泄漏拌倍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一噪径、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧数初,春花似錦找爱、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至仑鸥,卻和暖如春吮播,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背眼俊。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工意狠, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疮胖。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓环戈,卻偏偏與公主長(zhǎng)得像闷板,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子院塞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理遮晚,服務(wù)發(fā)現(xiàn),斷路器拦止,智...
    卡卡羅2017閱讀 134,654評(píng)論 18 139
  • 加速測(cè)試的方法 這里所說(shuō)的“速度”有兩層含義县遣。 其一,當(dāng)然是測(cè)試運(yùn)行所用的時(shí)間汹族。我們這個(gè)小程序的測(cè)試已經(jīng)開(kāi)始出現(xiàn)慢...
    AQ王浩閱讀 952評(píng)論 0 1
  • 加速測(cè)試的方法 這里所說(shuō)的“速度”有兩層含義萧求。 其一,當(dāng)然是測(cè)試運(yùn)行所用的時(shí)間鞠抑。我們這個(gè)小程序的測(cè)試已經(jīng)開(kāi)始出現(xiàn)慢...
    AQ王浩閱讀 2,525評(píng)論 1 9
  • Awesome Ruby Toolbox Awesome A collection of awesome Ruby...
    debbbbie閱讀 2,867評(píng)論 0 3
  • 八月桂花次第香 九月菊花處處黃 為是二君秋情重 夜來(lái)惹得魂夢(mèng)揚(yáng)
    半個(gè)讀書(shū)人閱讀 291評(píng)論 13 30