TL;DR

從多種角度認識軟體測試

大家好,我是 Gunner(거너, 정재훈),目前在 ONDA 負責飯店管理解決方案的後端開發。從事後端開發多年,跨過許多不同領域後,我發現軟體測試的範圍不只廣,連「測試是不是必要」這件事本身也眾說紛紜。

因此我想用兩篇文章,談談軟體測試有哪幾種,以及開發者該用什麼標準來決定要寫什麼測試。

1. 軟體測試的種類

軟體測試,指的是驗證軟體是否正常運作、有沒有 bug 的過程。

軟體測試大致可分成黑箱(Black-Box)測試與白箱(White-Box)測試兩類。

  • Black-Box(Functional)測試: 把軟體當成「黑箱」來測試,看不見內部如何運作。從外部使用者的角度驗證功能,不檢驗原始碼。主要由 QA(Quality Assurance,驗證服務功能與品質的)團隊負責。

  • White-Box(Structural)測試: 查看程式碼並驗證軟體功能的手法。測試對象包括原始碼、程式結構、資料流等,主要由開發者負責。

若以_測試接近的層級_來看,還可以這樣分類:

  • Unit(單元): 測試程式的基本組成單位。也就是把程式的基本單位與其他部分隔離,獨立執行後確認結果是否符合預期。單位的標準會因程式語言或框架不同而異。比方說在函數式程式設計裡,一個函數就是一個單位。

  • Integration(整合): 系統由多個元件之間的互動組成。整合測試檢驗的是同一系統內、或不同系統之間的元件如何互動。

       **◦ Regression(迴歸):** 當我們新增功能或修正 bug 時,為了確認這些修改有沒有對既有功能產生負面影響而執行的測試。
    

    *整合測試有時會變成迴歸測試,有時則是不同的東西。

  • System(系統): 針對一個完整整合的系統,確認它是否滿足需求規格的測試。因為必須了解內部,所以和黑箱測試不同,可以開瀏覽器跑程式碼來測。又因為是從「一個整合系統」的觀點測功能,所以和整合測試也不同。不只進行功能性(Functional)測試,也包含非功能性(Non Functional)項目(例如穩定性、擴充性等)的驗證。

2. 物件導向系統裡的測試

目前許多軟體系統都用物件導向方法開發。物件導向典範最早在 1960 年代為了模擬需求而誕生的 Simula 語言中出現,現在已經用在各個領域。

物件導向系統裡做單元測試時,單位的基準是什麼?

物件導向系統的基本組成單位是物件(object),所以單位基準一般會看描述物件資訊的類別(class)。

類別由欄位(field)、方法(method)、繼承等多種元素交織而成。實際為類別寫單元測試時,通常是為方法寫測試,所以也有人會把方法當成基本單位。但方法本來就無法和類別分開來看,所以把方法當基本單位並不恰當。

以類別為單位基準時,單元測試大致可分成兩種形式:

  • Intra-class(類別內部): 只測試目標類別內部的案例
  • Inter-class(類別之間): 除了測試目標類別,還有其他類別參與的案例

稍等一下!Inter-class 形式的單元測試和整合測試有什麼不同?

物件導向系統的特性是一個測試案例常涉及多個類別,這時單元測試和整合測試之間的界線就變模糊了。因為整合測試的定義是「測試同一系統內或不同系統之間的元件互動」。

關鍵在於你怎麼定義元件的標準。如果把元件的標準定為一個類別,那單元測試和整合測試的界線確實會模糊。

雖然沒有標準答案,但一般可以用以下方式區分單元測試和整合測試。當然這不是絕對的標準,在物件導向系統裡要清楚區分兩者仍是個難題。

  • 單元測試重視獨立執行(在與其他系統元件隔離的狀態下快速執行的測試),因此建議使用 mock,但整合測試不用 mock,而是在測試中使用實際程式碼。
  • 單元測試涉及的類別數量通常比整合測試少。

3. BDD(Behaviour-Driven Development)與 TDD(Test-Driven Development)

這種模糊性在行為驅動開發(Behaviour-Driven Development, BDD)[1] 和測試驅動開發(Test-Driven Development, TDD)這類測試工具裡也看得到。TDD 聚焦於單元測試,而 BDD 是從 TDD 衍生出來的技法,把重點放在系統的行為,強調清楚表達並測試行為。

BDD 寫測試規格時用的 'Given/When/Then' 指引就很能體現這個特性:

  • Given: 設定觸發測試目標行為所需的初始狀態。
  • When: 描述引發測試目標行為的事件。
  • Then: 確認事件發生時預期的結果。

來看一段用 'Given/When/Then' 指引寫的 Scala 測試案例範例:

package com.acme.pizza

import org.scalatest.FunSpec
import org.scalatest.BeforeAndAfter
import org.scalatest.GivenWhenThen

class PizzaSpec extends FunSpec with GivenWhenThen {

  var pizza: Pizza = _

  describe("披薩") {

    it ("應該可以加配料") {
      Given("一個新披薩")
      pizza = new Pizza

      When("加上配料時")
      pizza.addTopping(Topping("green olives"))

      Then("配料數量增加")
      expectResult(1) {
        pizza.getToppings.size
      }

      And("加入的配料種類保留下來")
      val t = pizza.getToppings(0)
      assert(t === new Topping("green olives"))
    }
  }
}

BDD 為什麼強調 'Given/When/Then'?

物件導向系統的功能常透過物件之間的互動來運作,只靠單元測試會有極限。因此寫測試案例(Test Case, TC)時專注在互動上會比較好。也就是說,設計 TC 時不要把範圍限定在 Intra-class,最好用 Inter-class 的範圍思考。

如果你用 JUnit 這類單元測試工具,很容易跟著 JUnit「一個類別寫一個測試檔案」的潛規則,在設計 TC 時也只想到那一個類別。但如果一開始就考慮 'Given/When/Then' 指引,就會自然而然想到類別之間的互動。

換句話說,用 BDD 會讓你在寫 TC 時考慮類別之間的互動,自然就會寫出好的 Inter-class 測試。

當然,光遵守 'Given/When/Then' 指引並不保證就能寫出考慮互動的測試。'Given/When/Then' 指引可以用在單元測試和整合測試上,最重要的還是測試作者心裡想的 TC 層級是什麼。

那麼 TDD(測試驅動開發)是什麼,業界對它有哪些看法?

參考資料

1. Behavior-driven development - Wikipedia