聊聊如何寫單元測(cè)試

這篇文章主要討論一下幾點(diǎn)問題

  • 如何開始寫一個(gè)單元測(cè)試
  • 單元測(cè)試有我自己的一些實(shí)踐

這篇文章的假設(shè)為你明確自己要寫單元測(cè)試了,如果您不符合這個(gè)假設(shè),可以參看這篇文章 先解決思想:為何要寫單元測(cè)試

當(dāng)打算開始寫單元測(cè)試時(shí)。你調(diào)整了下坐姿,氣運(yùn)丹田,感覺到冥冥之中又向高質(zhì)量代碼邁進(jìn)了一步,但當(dāng)你的手下意識(shí)的敲擊鍵盤的時(shí)候,又覺得似乎哪里不太對(duì)勁:“恩,應(yīng)該怎樣開始寫一個(gè)單元測(cè)試呢?”

如何寫單元測(cè)試

首先我們需要明確,什么叫做單元測(cè)試。

在計(jì)算機(jī)編程中,單元測(cè)試(英語:Unit Testing)又稱為模塊測(cè)試, 是針對(duì)程序模塊(軟件設(shè)計(jì)的最小單位)來進(jìn)行正確性檢驗(yàn)的測(cè)試工作。 程序單元是應(yīng)用的最小可測(cè)試部件。

我的理解是:測(cè)試某個(gè)具體的函數(shù),是否符合編寫者的預(yù)期。

其實(shí)也很好理解,就是將你編寫某個(gè)函數(shù)的功能與你的預(yù)期做一個(gè)比較,如果函數(shù)運(yùn)行的結(jié)果與你的預(yù)期相符,則說明測(cè)試通過,反之則失敗。

舉個(gè)很簡(jiǎn)單的例子

class Person
  attr_accessor :name, :gender

  def initialize(name, gender)
    self.name = name
    self.gender = gender
  end
  
  def say_hello
      puts "#{self.title} #{name} said hello."
  end
  
  def title
     self.gender == "male" ? "Mr" : "Ms"
  end
end

我想測(cè)試一下 title 這個(gè)函數(shù)是否符合我預(yù)期,于是我會(huì)這樣寫測(cè)試(假設(shè)使用Rails 原生的 test框架)

require 'test_helper'

class PersonTest < ActiveSupport::TestCase
  test "should return title correctly" do
    person = Person.new("Ji Cheng", "Male")
    assert_equal "Mr", person.title

    person = Person.new "Han Meimei", "Female"
    assert_equal "Ms", person.title
  end
end

語言不同,測(cè)試框架不同都會(huì)導(dǎo)致代碼不同,但是思想都是一樣的,都是去 assert 一個(gè)值,與運(yùn)行后的函數(shù)值保持一致。

也許有人會(huì)說你這個(gè)函數(shù)太簡(jiǎn)單了,簡(jiǎn)直不用看就知道會(huì)發(fā)生什么,為什么還要寫個(gè)測(cè)試? 其實(shí)想想,寫出這個(gè)測(cè)試也花多長(zhǎng)時(shí)間,更重要的是,你在寫這個(gè)測(cè)試的時(shí)候,會(huì)更加清楚這個(gè)函數(shù)輸入與輸出,是否滿足預(yù)期,以及多一次使用自己寫的函數(shù)的機(jī)會(huì),體諒下調(diào)用你寫的函數(shù)的人。

細(xì)心的朋友肯定已經(jīng)看出來了,這個(gè)測(cè)試是會(huì)報(bào)錯(cuò)的,如果你真沒看出來,就更加說明單元測(cè)試的重要性。實(shí)際情況中太多函數(shù)是看不出來的,但是例如下面的 analysis_message

# encoding: utf-8

class JobStepService
  attr_accessor :project, :job, :flow

  # Some functions ...

  class << self
    def analysis_message(hash)
      job_id = hash[:job_id]
      index = hash[:index]
      job_step = JobStep.find_by(job_id: job_id, index: index)
      return if job_step.nil? || job_step.status == "stopped"

      try_mark_last_job_status(job_id, index)
      where = JobStep.where(job_id: job_id, index: index)
      safe_update_job_hash(where, hash)
      job_step.reload
    end

    private

    def try_mark_last_job_status(job_id, index)
      return if index.to_i.zero?
      # 必須得找到, 不找到肯定是哪里出錯(cuò)了,應(yīng)該拋異常
      JobStep.find_by(job_id: job_id, index: index.to_i - 1).update_attribute(:status, "success")
    end
  end
end

當(dāng)不是那么容易看出的時(shí)候,去寫一個(gè)單元測(cè)試是跟你在命令端調(diào)試所花的時(shí)間是差不多的。


class JobStepTest < ActiveSupport::TestCase
  setup do
    # do some initialize work...
  end
  test "could analysis message correctly" do
    JobStepService.new(@job).generate_job_steps
    assert_equal false, JobStep.count == 1
    hash = { index: 0, status: "success", return_value: 0, log: "hello world\n", job_id: @job.id.to_s, category: "step" }
    JobStepService.analysis_message hash
    assert_equal "success", JobStep.asc(:index).first.status
    assert_equal "pending", JobStep.asc(:index).last.status

    hash = { index: 2, status: "failure", return_value: 0, log: "hello world\n", job_id: @job.id.to_s, category: "step" }
    JobStepService.analysis_message hash
    assert_equal "success", JobStep.asc(:index).to_a[1].status
    assert_equal "failure", JobStep.asc(:index).to_a[2].status
  end

想必大家也看出來了,測(cè)試甚至有些隨意不太友好,但是至少在跑了這段測(cè)試之后,我很信任之前寫的函數(shù)是沒有問題的(就算有,也不會(huì)是那些會(huì)被同事恥笑的低級(jí)錯(cuò)誤)。

寫單元測(cè)試一些實(shí)踐

大前提

所有的實(shí)踐前面都有一個(gè)大前提:首先你得寫單元測(cè)試。我非常喜歡寫一些顯而易見的單元測(cè)試當(dāng)做休息放松,當(dāng)別人問我為什么寫這種測(cè)試的時(shí)我通常是以“增加代碼測(cè)試覆蓋率”來忽悠他們。(但是實(shí)際上還是有30%左右的概率會(huì)測(cè)出各種問題,包含各種語法錯(cuò)誤,誤觸某回調(diào)等奇怪的錯(cuò)誤校驗(yàn)不過,也許我就是一個(gè)粗心的人),這樣做還有另外一個(gè)好處,培養(yǎng)自己對(duì)每個(gè)方法都寫測(cè)試的習(xí)慣:連很簡(jiǎn)單的方法都寫了,那稍微復(fù)雜點(diǎn)的,簡(jiǎn)直不能忍。

誰來寫

開發(fā)來寫。單測(cè)主要測(cè)試的是具體的函數(shù),沒有比開發(fā)人員更熟悉自己寫的函數(shù)了,同時(shí)本著“吃自己的狗糧”的原則,也可以反省下自己設(shè)計(jì)的函數(shù)是否合理。最重要的,當(dāng)自己寫完一個(gè)的時(shí)候,就可以把單元測(cè)試當(dāng)做自己手動(dòng)調(diào)試代碼,這樣就可以很自然的無縫的將單測(cè)寫上,而不用等測(cè)試人員排隊(duì)做。

關(guān)于測(cè)試覆蓋率

雖然這個(gè)東西聽起來很虛,但我覺得是個(gè)必需品,必須得上。當(dāng)有一個(gè)標(biāo)準(zhǔn)去衡量自己的工作進(jìn)度的時(shí)候,潛意識(shí)中大家會(huì)努力的提高這個(gè)指標(biāo)。同時(shí)絕大多數(shù)測(cè)試覆蓋率統(tǒng)計(jì)工具,都能通過界面顯示出你函數(shù)中未覆蓋的邏輯,避免自己漏測(cè)。我自己使用simplecov 這個(gè)gem 來統(tǒng)計(jì)我自己的Rails 項(xiàng)目的測(cè)試覆蓋。

寫的測(cè)試跑著要快

我非常贊同,寫的測(cè)試越慢,由于人的惰性,會(huì)導(dǎo)致自己因?yàn)椴幌氲忍枚慌軠y(cè)試。測(cè)試寫的再多,不跑全是白搭。

其實(shí)一個(gè)單元測(cè)試的內(nèi)容很少,那么一般慢是慢在哪里呢?

我覺得有以下方面

  1. IO
  2. sleep/wait 語句
  3. 數(shù)據(jù)庫(kù)的大量寫入

關(guān)于IO
目前我遇見比較多的是關(guān)于網(wǎng)絡(luò)的IO, 有些第三方組件會(huì)接入網(wǎng)絡(luò),這種一般都會(huì)帶來500ms左右的延時(shí),運(yùn)氣不好連國(guó)外(比如我們的項(xiàng)目連github API)沒準(zhǔn)就會(huì)變成假摔(一定概率的跑出錯(cuò),非必現(xiàn)的錯(cuò)誤)。常見的操作是 Stub 解決問題,各大語言都有很成熟的解決方案。比如我現(xiàn)在使用的 webmock 這個(gè) gem ,當(dāng)然以ruby 這種 “開放式” 語言的能力,就算不引入任何gem,寫個(gè)猴子補(bǔ)丁也會(huì)非常的輕松。

關(guān)于sleep/ wait
大多數(shù)使用sleep/ wait 的時(shí)候都是在等待某個(gè)異步方法的執(zhí)行完成,我的處理方式是將異步的處理以及等待后面的語句都抽成兩個(gè)獨(dú)立的函數(shù),分別測(cè)試這兩個(gè)函數(shù),從而避免走sleep 這種慢的操作

關(guān)于數(shù)據(jù)庫(kù)
很多測(cè)試相關(guān)的文章和書籍都強(qiáng)調(diào) 數(shù)據(jù)庫(kù)太慢了,所以不能使用數(shù)據(jù)庫(kù)。我不太認(rèn)同,因?yàn)槠鋵?shí)很多時(shí)候?qū)懙拇a都需要依賴數(shù)據(jù)庫(kù)的一些特性,或者離開數(shù)據(jù)庫(kù)而存在內(nèi)存中會(huì)很麻煩(比如查詢語句,脫離數(shù)據(jù)庫(kù)mock 個(gè) where 很麻煩)。我的策略(當(dāng)然是Rails 的策略)是使用專門用于測(cè)試的數(shù)據(jù)庫(kù),每當(dāng)運(yùn)行一個(gè)單側(cè)的時(shí)候就會(huì)把它清除掉。這樣,測(cè)試數(shù)據(jù)庫(kù)的數(shù)據(jù)會(huì)非常的少,查詢、新增起來大多數(shù)情況下其實(shí)也在20ms以內(nèi)。

我是非常反對(duì)當(dāng)一個(gè)單元測(cè)試跑完后,不清除數(shù)據(jù)庫(kù)的,可能這些數(shù)據(jù)會(huì)影響到其他單元測(cè)試,進(jìn)一步造成了測(cè)試的假摔,假摔是大忌,應(yīng)該盡量避免。當(dāng)然清數(shù)據(jù)庫(kù)也不是絕對(duì)的,需要自己靈活判別,比如下面的情況。

我運(yùn)行測(cè)試之前會(huì)生成100條左右的模板數(shù)據(jù),這些數(shù)據(jù)是我在進(jìn)行單元測(cè)試的時(shí)候絕對(duì)不會(huì)操作的,所以沒必要每次執(zhí)行一個(gè)單元測(cè)試刪除再新建。但是為了防止我自己有時(shí)候沒想清楚改掉模板數(shù)據(jù)從而有可能造成假摔,所以我在執(zhí)行每個(gè)單元測(cè)試之前會(huì)判斷下這些模板數(shù)據(jù)的行數(shù)是否是我最初的生成的行數(shù)。

單元測(cè)試不是萬能的

會(huì)有人覺得我花了那么大的功夫,覆蓋率90%了,上 jenkins 或者 flow.ci 了,那我的程序就很穩(wěn)定了。這種觀念當(dāng)然是不對(duì)的,就如同你買了一把200塊的鎖就指望自己的自行車永遠(yuǎn)不會(huì)被偷一樣。良好的單元測(cè)試會(huì)極大的提高程序穩(wěn)定性,但是不會(huì)百分百的保證程序一定ok,畢竟人無完人。

從入門到放棄?

相信很多朋友其實(shí)也寫過單測(cè),但或因需求變更過快,或因一次次的失敗無力解決,導(dǎo)致了最終沒有堅(jiān)持下來。這當(dāng)中其實(shí)是有一定技巧的,使用良好的技巧會(huì)在保證測(cè)試覆蓋率的同時(shí),降低測(cè)試失敗的頻率。下次就來說說 如何使用一些技巧,讓我們?nèi)菀讏?jiān)持執(zhí)行這個(gè)應(yīng)該堅(jiān)持的單元測(cè)試

最后,有興趣的朋友可以關(guān)注一下“持續(xù)集成慢慢來”這個(gè)公眾號(hào)與我交流,如果對(duì)持續(xù)集成感興趣,也可以試試我司基于SaaS的持續(xù)集成產(chǎn)品 flow.ci。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評(píng)論 6 540
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,275評(píng)論 3 428
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,368評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,736評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,919評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,481評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,235評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,427評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,656評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評(píng)論 1 294
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,160評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,380評(píng)論 2 379

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,712評(píng)論 25 708
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,818評(píng)論 18 139
  • 接下來談?wù)剢卧獪y(cè)試如何堅(jiān)持下來的問題 相信大家或因?yàn)樯鐓^(qū)影響、或因?yàn)樯霞?jí)領(lǐng)導(dǎo)的要求、抑或純粹的想挑戰(zhàn)自身的編碼水平...
    atpking閱讀 1,193評(píng)論 2 5
  • “我們熱愛這個(gè)世界,才真正活在這個(gè)世界上”——泰戈?duì)?這次旅行,盼了一月,或者可以說盼了有半年了,其實(shí)每次旅行結(jié)束...
    生涯花匠閱讀 454評(píng)論 1 2
  • 假如有人問我的煩憂 我不敢說出你的名字 我們就像兩根鐵軌 靠的是那么近 卻總是不會(huì)有交集 身旁沒有你 卻彌漫著你來...
    七月的巨蟹座閱讀 149評(píng)論 0 1