D2.小侃測試驅動

分享第二章,關於測試驅動。這裏的測試主要針對Web後端的測試 —— 你爲何要寫測試用例(即測試用例的完善是不是浪費時間),如何完善你的測試用例,代碼設計如何簡化測試用例的書寫,以及一些後期的構想。javascript

1. 你爲何要寫測試用例

這個習慣一般會被認爲是一種耽誤開發進度的行爲,你須要花費幾乎和開發代碼相同的時間來逐步完善你的測試用例。可是在開發過程當中,在開發完成一段代碼後若是負責任而不是說徹底把問題交給測試人員去發現的話,這個時候一般都會去作一些手動的測試。例如:前端

  1. 在代碼中執行某些方法,查看輸出的值是否符合預期。
  2. 修改數據庫/緩存,而後執行某些方法,看數據庫的變化是否符合預期。
  3. 使用工具模擬請求某些接口,查看接口的返回值/數據庫的變化值是否會符合預期。
  4. 若是有前端頁面的話,還會涉及到先後端聯調,即要在前端頁面上經過前端交互,查看前端的反饋是否符合預期,來間接驗證後端代碼的正確性。

現代化的測試工具都在儘量的將這些人工的手動測試行爲抽象成代碼塊,當你有意識去進行手動測試的時候,其實已經開始在嘗試測試用例的行爲了。既然能夠經過手動的方式進行測試,那爲何還須要用代碼來實現測試?java

  1. 代碼是能夠複用或者在簡單重構後能夠實現更多的功能的,可是當你選擇手動的時候,每次你都須要重頭開始。
  2. 成熟的工做流中應當包括代碼審覈流程,代碼審覈的方式有不少,逐句閱讀你的代碼,或者檢查你測試代碼的完善性以及正確性,而後運行你的測試用例。後者更加簡單。
  3. 當代碼改動,例如修復 Bug 時候,很難保證你的改動是否會影響其餘依賴你代碼的部分。在人工測試的時代有一個叫作迴歸測試,即在你修復 Bug 將你的系統從新測試一遍。可是若是你已經有了完善的測試用例了呢,直接執行命令搞定。
  4. 當你重構代碼的時候,同上。

2. 如何完善你的測試用例

在進入完善階段前,先說說你將如何實現測試用例。laravel

rubydescribe Meme do

  before do
    @meme = Meme.new
  end

  describe "when asked about cheeseburgers" do
    it "must respond positively" do
      @meme.i_can_has_cheezburger?.must_equal "OHAI!"
    end
  end

  describe "when asked about blending possibilities" do
    it "won't say no" do
      @meme.will_it_blend?.wont_match /^no/i
    end
  end
end

上面的代碼來自於 Ruby 的 minitestbefore 包含的代碼塊是在執行下面的測試用例前要作的事情,一般還會支持一個相對應的方法,在測試用例執行完執行。每一個用例裏面都進行一些很小的判斷。git

第一段中提到了一些手動測試裏面常常會涉及到的測試內容,這裏拿其中的 23 進行說明。在進行數據庫相關的測試時,須要在 before 中插入一條測試數據,而且在 after 中刪除測試數據。中間的測試用例中,經過執行相應的方法,執行完畢後:檢查數據變化狀況/檢查是否有預期的異常/是否返回預期結果 來確認代碼的正確性。若是是接口的話,就是經過代碼發起對應的請求,而後檢查返回的內容是否返回預期,有須要的話再去查看數據庫裏面的數據是否符合預期變化。github

如今已經有了測試用例,可是任然須要考慮一種特殊狀況。我如今爲一個函數寫了相對完善的測試用例了,跑完都 PASS 了,結果發現線上的日誌裏面仍是有那個函數的報錯。檢查下發現函數的某個分支以前在測試的時候沒有測試到,恰好線上的某種狀況運行到了這個分支,結果有一個很不明顯的語法錯誤報錯了,有沒有辦法能確保全部的代碼都測試過了?這裏須要引入的是一個叫作 測試用例覆蓋率 的概念,基本上每一個語言都會有響應的實現。經過測試用例覆蓋率,量化的告訴你你的測試用例有沒有跑完某某文件裏的全部代碼,而你須要作的,就是儘量保證你的覆蓋率保持在 100%。web

某種意義上來講,測試用例和測試覆蓋率是用來提升開發者對本身代碼自信心的工具。可是,他們也不是萬能的。測試用例裏面總可能會漏掉一些參數的可能性,固然你的代碼裏面也沒有爲這種可能性進行代碼的編寫,最終測試用例覆蓋率只能告訴你你寫的代碼咱們都幫你檢測過了測試過了,對於你沒有考慮到的可能性,表示無能爲力。因此儘量編寫嚴格的代碼,例如 javascript 裏面儘量都用 === 而不是 ==,使用強類型的編程規範等等,這些來下降這種由於接受的參數範圍過大帶來的潛在風險。數據庫

3. 代碼設計如何簡化測試用例的書寫

整個 Web (也不侷限於 web)一般包括三個層面的代碼 —— 單純數據處理與運算、涉及到數據庫、涉及到具體的網絡協議。其中單純的數據運算於處理主要爲普通的運算的函數或者是其餘代碼,涉及到數據庫就是傳統意義上 MVC 裏面的 M,涉及到具體的網絡協議就是對應的 C。這三塊的測試分別對應着第一節中常規的測試內容的前三條。編程

由於 C層面一般還可能涉及到頁面的渲染以及相應協議的模擬,因此一般把測試的重心放在函數以及數據庫相關的代碼裏面能夠減小測試用例代碼的複雜度,這個就要求 Controller 的代碼要儘量少。對於複雜度較高的應用的一些目前的一些建議:後端

  1. 將數據的基礎校驗都放在 M層,若是使用 Ruby 開發的話,ActiveRecord以及Mongoid都提供了很方便使用的 validation 功能。
  2. 嘗試在代碼中使用 Pub/Sub 模式配合一些 ORM中提供的鉤子(hook) 來實現 Model 之間的通訊。 例如在 A 建立的時候發佈某個消息,B監聽到消息以後修改他本身的某個屬性值。
  3. 使用 Command 模式將一些業務無關的功能從系統中抽離出來,例如郵件發送。

以上建議參考:Laravel wisper resque

4. 構想

以上的內容都避開了先後端須要聯調的測試用例,下面的內容主要是針對這塊。Ruby 在這個方向已經有一些比較優雅的實現,感興趣的能夠直接先去欣賞一下 Capybara

隨着包括 Selenium Phantomjs 以及基於前者的 Watir 等一系列瀏覽器驅動的普及,使用代碼控制瀏覽器已經再也不是一件很複雜的事情。在這個能力的基礎上,能夠嘗試把基於前端的測試分爲四步:

  1. 等待某標誌性元素出現(例如等待頁面載入玩,或者某個內容異步加載出現)
  2. 模擬用戶操做,這裏的操做包括且不侷限於用戶點擊、用戶輸入
  3. 等待反饋中標誌性元素出現(例如某某輸入框出現)
  4. 判斷內容,是否符合預期

基於這個流程,能夠解決絕大多數的前端測試。可是單純依靠這個流程任然不夠,由於頁面中可能出現例如驗證碼這樣的阻礙元素,在不修改代碼的前提下,能夠嘗試經過數據庫/緩存來取到這些內容。一樣,和測試接口相同,這裏也涉及到在測試前數據庫中插入測試數據,測試用例執行後嚴重數據庫裏面數據變化,以及所有測試完畢後刪除測試數據的內容。最終致使這塊測試用例代碼的實現須要同時對前端後端有必定的瞭解。目前還在考慮在借鑑 Capybara 的基礎上,設計出更加通用的方案。

最後貼一段 Capybara 的代碼結束這段內容:

rubyfeature "Signing in" do
  background do
    User.make(:email => 'user@example.com', :password => 'caplin')
  end

  scenario "Signing in with correct credentials" do
    visit '/sessions/new'
    within("#session") do
      fill_in 'Email', :with => 'user@example.com'
      fill_in 'Password', :with => 'caplin'
    end
    click_button 'Sign in'
    expect(page).to have_content 'Success'
  end

  given(:other_user) { User.make(:email => 'other@example.com', :password => 'rous') }

  scenario "Signing in as another user" do
    visit '/sessions/new'
    within("#session") do
      fill_in 'Email', :with => other_user.email
      fill_in 'Password', :with => other_user.password
    end
    click_button 'Sign in'
    expect(page).to have_content 'Invalid email or password'
  end
end

原文地址: https://github.com/vincenting/note/issues/2

相關文章
相關標籤/搜索