軟體測試在 DevOps 流程中扮演關鍵角色,測試金字塔的應用能有效提升軟體品質。隨著自動化程度提高,維運團隊也需理解開發團隊的測試方法,確保團隊協作順暢。測試金字塔的結構能提供快速且準確的品質反饋,從單元測試、整合測試到端對端測試,層層把關。開發者編寫單元測試能確保程式碼的品質,整合測試則驗證系統間的連線與互動,而端對端測試則從使用者角度驗證整個系統流程。此外,合約測試能確保服務介面的一致性,避免因服務變更導致程式碼錯誤。

儀錶板設計的藝術:從資料到洞察

在設計儀錶板的過程中,我們經常會遇到一個挑戰:如何將大量的資料轉化為有用的資訊。一個好的儀錶板設計應該能夠幫助使用者快速理解系統的運作狀態,並找出潛在的問題。

建立儀錶板的基礎

首先,我們需要確定儀錶板的第一行應該顯示什麼內容。這通常是最重要的資訊,能夠讓使用者一目瞭然地瞭解系統的整體狀態。我們應該避免將太多不相關的資料混雜在一起,否則使用者會感到困惑。

範例:磁碟效能指標

舉例來說,如果我們要監控磁碟效能,我們可能會想要將以下指標放在一起:

  • 磁碟讀取次數
  • 磁碟寫入次數
  • 磁碟寫入延遲
  • 磁碟佇列深度

這些指標能夠提供對磁碟子系統健康狀態的全面瞭解。透過將這些相關的指標分組在一起,使用者可以快速地鑽取到問題的根源。

內容解密:

此圖示展示了磁碟效能指標之間的關係。透過將這些指標分組在一起,我們可以更輕鬆地分析磁碟子系統的健康狀態。

引導讀者

一旦我們定義了儀錶板的結構,並按照重要性進行了排序,我們就可以新增一些額外的元素來引導讀者。我們可以使用註解小工具來描述儀錶板、元件和元件分組。這樣可以幫助使用者更好地理解資料的意義。

範例:註解的使用

例如,我們可以在圖表旁邊新增一個註解來說明某個指標在特定條件下的異常行為。這樣,使用者就不會對突然的變化感到困惑。

為儀錶板命名

為儀錶板命名是一個重要的步驟。我們可以將儀錶板分成三個部分:目標受眾、被檢查的系統和系統的檢視。透過這種方式命名儀錶板,使用者可以快速找到他們需要的資訊。

範例:儀錶板命名

例如,我們可以將一個儀錶板命名為“行銷 – 平台 – 網路流量報告”。這樣,行銷人員就可以快速找到他們需要的資訊,而不必花時間瀏覽其他無關的儀錶板。

軟體開發中的品質保證:超越「調味料」思維

在軟體開發的世界裡,品質並非只是最後新增的「調味料」。無論是在餐廳還是軟體開發中,若不檢查原料的品質,最終產品的品質也好不到哪裡去。要持續交付高品質的產品,必須在每個元件和原料中個別建立品質,並在它們完全融入最終產品之前單獨驗證其品質。

測試的重要性

在開發生命週期的末端附加測試,可能會導致災難性的結果。當你測試產品的所有組成部分時,通常會進行更多的測試。這意味著你如何構建測試工作變得非常重要。這些測試結果的品質同樣重要。你需要能夠信任這些測試的輸出結果,否則人們會質疑它們的必要性。

自動化測試的陷阱

如果你正在進行任何自動化測試,你可能會密切關注測試案例的數量。事實上,你可能會為自己擁有大量的自動化測試而感到自豪。儘管有1500個自動化測試案例和一個完整的QA團隊在每次發布時進行迴歸測試,你仍然會在軟體中釋放出錯誤。更糟糕的是,你有時會釋放出令人尷尬的錯誤,這些錯誤凸顯了某些領域根本沒有被測試過的事實。

接受錯誤的存在

是否有辦法避免將錯誤釋放到生產環境中?我的看法是沒有。只要你仍在編寫軟體,你就會寫出未被捕捉並進入生產環境的錯誤。這是你生命中的一部分,也是你簽約接受的內容。

從錯誤中學習和改進

你可以努力減少某些型別的錯誤發生的可能性,並開發出一種流程,當你識別出一個錯誤時,你確切知道該如何測試這種情況,並確保它不會再次發生。透過足夠的練習,你將能夠識別錯誤類別,也許能夠一舉消除整個類別的錯誤。但這個過程需要迭代和承諾。

測試金字塔

許多當今的測試套件都圍繞著測試金字塔的概念。測試金字塔是一個隱喻,用於描述你應該為應用程式進行的測試型別。它對在測試生命週期中應該投入最多精力和努力的地方給出了意見。

單元測試

測試金字塔強調單元測試是測試套件的基礎,也是最大的組成部分。單元測試旨在測試程式碼的特定部分(稱為單元),並確保其功能和行為符合預期。

def add(a, b):
    return a + b

#### 內容解密:
這段程式碼定義了一個名為 `add` 的函式該函式接受兩個引數 `a``b`,並傳回它們的和單元測試將驗證這個函式是否正確地執行了加法運算

整合測試

整合測試是金字塔中的下一層,它們關注的是將系統的各個單元組合起來並對它們進行分組測試。這些測試檢查單元之間的互動。

def test_add_function_integration():
    result = add(2, 3)
    assert result == 5
    
#### 內容解密:
這段程式碼展示了一個整合測試驗證 `add` 函式在實際使用中的行為它檢查當輸入2和3時函式是否傳回5確保函式在更大系統中的正確性

端對端測試

端對端測試從最終使用者的角度來檢查系統。

def test_end_to_end():
    # 模擬使用者操作
    result = perform_end_to_end_test()
    assert result == expected_output
    
#### 內容解密:
這段程式碼代表了一個端對端測試模擬了使用者的操作並驗證整個系統是否按預期工作這種測試關注的是整個應用程式的工作流程

圖示:測試金字塔結構

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title 軟體測試金字塔實踐

package "軟體測試架構" {
    package "測試層級" {
        component [單元測試
Unit Test] as unit
        component [整合測試
Integration Test] as integration
        component [端對端測試
E2E Test] as e2e
    }

    package "測試類型" {
        component [功能測試] as functional
        component [效能測試] as performance
        component [安全測試] as security
    }

    package "工具框架" {
        component [pytest] as pytest
        component [unittest] as unittest
        component [Selenium] as selenium
        component [JMeter] as jmeter
    }
}

unit --> pytest : 撰寫測試
unit --> integration : 組合模組
integration --> e2e : 完整流程
functional --> selenium : UI 自動化
performance --> jmeter : 負載測試

note right of unit
  測試金字塔基礎
  快速回饋
  高覆蓋率
end note

@enduml

軟體品質與測試金字塔在DevOps中的重要性

軟體測試是DevOps流程中的關鍵環節,而測試金字塔(Testing Pyramid)則是確保軟體品質的重要指導原則。測試金字塔的概念最初由開發團隊所採用,但隨著DevOps運動的推進,維運團隊也開始接觸並應用這一概念。

為何測試金字塔對DevOps至關重要

  1. 跨團隊協作與一致性:隨著DevOps運動的推進,越來越多的自動化被引入開發流程。維運團隊需要了解開發團隊目前採用的方法和實踐,以確保跨團隊協作的順暢和產生協同效應。

  2. 自動化與品質訊號:DevOps中的自動化需要與軟體開發生命週期中的其他流程互動。這些流程需要能夠發出訊號,這些訊號轉化為量化指標,用於評估軟體的品質屬性。例如,自動化佈署流程需要知道程式碼是否達到一定的品質標準。這就要求開發者在設計測試套件時,將定性假設轉化為可被自動化理解的訊號。

測試金字塔的結構

測試金字塔提供了一個構建測試套件的框架,以實作最大化的速度和品質。按照測試金字塔的結構,可以快速提供準確的反饋。

單元測試(Unit Tests)

單元測試是測試金字塔的基礎。它們針對軟體中的個別單元或元件進行測試,例如方法、類別或函式。單元測試應該由編寫該元件的開發者撰寫,以確保在開發過程中能夠定期執行這些測試。

單元測試的重要性
  • 開發者擁有最多的上下文,能夠建立最合適的測試案例。
  • 自動化的單元測試幫助開發者在重構程式碼時進行驗證。
  • 有利於採用測試驅動開發(Test-Driven Development, TDD)等開發實踐。
  • 透過程式碼審查等流程,可以強制執行單元測試的要求。
def add(a, b):
    return a + b

# 單元測試範例
def test_add():
    assert add(1, 2) == 3
    assert add(-1, 1) == 0
    assert add(-1, -1) == -2

內容解密:

這段程式碼展示了一個簡單的加法函式及其對應的單元測試。首先定義了一個名為add的函式,該函式接收兩個引數ab,並傳回它們的和。接著,定義了一個名為test_add的測試函式,用於驗證add函式的正確性。測試案例涵蓋了正數、負數以及正負數相加的不同場景,以確保add函式在各種情況下都能正確運作。

為何DevOps書籍需要討論測試

  1. 自動化測試在DevOps方法中扮演著至關重要的角色。隨著系統中自動化的程度越來越高,自動化驗證變更是否成功變得越來越重要。
  2. 維運人員需要像開發人員一樣理解測試生命週期,因為他們將參與更多的自動化工作,特別是在基礎設施建立和管理方面。

單元測試的結構與實務應用

在軟體開發中,確保開發者是單元測試的主要編寫者是一項基本要求。如果組織內無法實作這一點,則需要仔細評估相關的利弊。至少,開發團隊應該對如何處理前述要點有明確的答案。

單元測試的結構

在結構上,單元測試應盡可能與系統的其他部分隔離。任何非被測試單元的部分都應該被模擬或替換。目標是確保測試執行快速,且不受其他系統可能失敗的影響。

程式碼範例與解析

import unittest
from unittest.mock import Mock

def calculate_rate_of_return(investment):
    api = get_api()
    original_price = api.get_original_purchase_price(investment)
    # 進行一些計算
    return rate_of_return

class TestCalculateRateOfReturn(unittest.TestCase):
    def test_calculate_rate_of_return(self):
        # 模擬 API 呼叫
        api = Mock()
        api.get_original_purchase_price.return_value = 1000
        # 將模擬的 API 傳入被測試函式
        rate_of_return = calculate_rate_of_return_with_api(investment, api)
        self.assertEqual(rate_of_return, expected_rate_of_return)

if __name__ == '__main__':
    unittest.main()

內容解密:

  1. 匯入必要的模組:使用 unittest 進行單元測試,並從 unittest.mock 匯入 Mock 以模擬外部依賴。
  2. 定義被測試函式calculate_rate_of_return 函式原本依賴於外部 API 取得原始購買價格。
  3. 建立測試類別TestCalculateRateOfReturn 繼承自 unittest.TestCase,用於包含多個測試案例。
  4. 模擬外部依賴:在 test_calculate_rate_of_return 方法中,使用 Mock 物件模擬 API 呼叫,避免實際呼叫外部 API。
  5. 執行測試:呼叫被測試函式並斷言其傳回值是否符合預期。

決定單元測試的內容

單元測試中最難的部分是確定要為哪些內容編寫測試案例。令人意外的是,測試所有內容可能會產生反效果。如果測試所有內容,重構將變得更加困難。當開發者嘗試為內部實作編寫測試案例時,這種情況經常發生。

重點分析

  • 區分公開與私有程式碼路徑:專注於公開程式碼路徑的單元測試,可以在不大量重構內部測試的情況下更改私有路徑。
  • 避免過度測試內部實作:除非必要,否則應避免對內部實作進行測試,以保持重構的靈活性。

單元測試的最佳實踐

單元測試位於測試金字塔的底層,應該佔據大部分的測試案例。它們通常是最快、最可靠和最細粒度的測試型別。在單元測試中,失敗的來源應該非常明顯,因為被測試的單元範圍很小。

自動化執行測試

單元測試通常由持續整合伺服器(如 Jenkins、CircleCI、Harness 等)自動執行。持續整合伺服器已經變得非常流行,因為它們能夠在程式碼變更時自動執行測試套件。

軟體測試的金字塔結構與實踐

軟體測試是確保應用程式品質的關鍵步驟。測試金字塔是一種常見的測試策略,涵蓋了不同層級的測試,包括單元測試、整合測試和端對端測試。本篇文章將探討這些測試型別的特點、挑戰和最佳實踐。

整合測試:連線點的驗證

整合測試是測試金字塔中的第二層,主要目標是驗證系統之間的連線點以及應用程式對這些系統的回應處理。與單元測試不同,整合測試會連線到實際的資料函式庫伺服器,寫入資料並讀取以驗證操作是否成功。

整合測試的重要性

很少有兩個系統能夠無縫地協同工作。被整合的兩個元件可能是在不同的使用場景下構建的,但當它們被組合在一起時,卻可能以意想不到的方式失敗。例如,曾經有一個公司在建造新總公司時,建築物和停車場是分開設計的,但當開始施工時,卻發現停車場的樓層與建築物的樓層不對齊,需要額外的樓梯來整合兩者。

整合測試的挑戰

整合測試需要花費更多時間,因為需要在測試元件之間進行互動。此外,每個元件都需要經過設定和拆卸過程,以確保它們處於正確的測試狀態。例如,可能需要填充資料函式庫記錄或下載本地檔案進行處理。由於這些額外的開銷,整合測試通常更昂貴,但它們在測試策略中仍然扮演著至關重要的角色。

整合測試的最佳實踐

  • 絕不對生產環境執行整合測試。如果需要測試其他服務,例如資料函式庫,應嘗試在本地測試環境中啟動該服務。
  • 避免對生產環境進行唯讀測試,因為這可能會對生產伺服器造成不必要的壓力,影響真實使用者。
  • 如果無法在本地啟動相依服務,請考慮建立一個為測試套件提供服務的測試環境。

端對端測試:從使用者視角驗證系統

端對端測試位於測試金字塔的頂端,從終端使用者的角度來驗證系統。它們通常透過模擬瀏覽器或客戶端應用程式來驅動變更,並驗證結果,例如資料是否正確顯示、回應時間是否合理以及UI錯誤是否出現。

端對端測試的特點

端對端測試是最完整的測試,但也是最耗時且最容易出現問題的。它們應該是您的測試組閤中最小的一部分。如果過度依賴端對端測試,您可能會發現您的測試套件變得脆弱,容易在執行之間失敗。

合約測試:確保服務介面的一致性

合約測試是一種新興的測試形式,旨在檢測被模擬或存根的服務是否發生了變化。當您使用模擬或存根服務進行測試時,您需要確保該服務的輸入和輸出符合您的預期。如果真實服務的行為發生了變化,但您的測試沒有反映這種變化,那麼您可能會釋出無法與服務正確互動的程式碼。

合約測試的實踐

合約測試是一組獨立的測試,用於驗證服務的輸入和輸出是否仍然按照預期執行。由於合約可能會發生變化,因此以較低的頻率執行合約測試並不罕見(例如,每天執行一次)。

透過使用合約測試,您可以檢測到其他服務何時更改了其預期,並相應地更新您的存根和模擬。如果您想了解更多資訊,請參閱 Alex Soto Bueno、Andy Gumbrecht 和 Jason Porter 的《Testing Java Microservices》(Manning,2018)中關於合約測試的優秀章節。

此圖示說明瞭不同層級的軟體測試之間的關係,以及合約測試如何支援整合測試。

程式碼範例與解析

以下是一個簡單的整合測試範例,用於驗證資料函式庫操作:

@Test
public void testInsertData() {
    // 步驟1:讀取資料函式庫中的行數
    int initialCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM my_table", Integer.class);
    
    // 步驟2:執行插入操作
    jdbcTemplate.update("INSERT INTO my_table (data) VALUES (?)", "Test Data");
    
    // 步驟3:驗證行數是否增加
    int finalCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM my_table", Integer.class);
    assertEquals(initialCount + 1, finalCount);
}

內容解密:

  1. 讀取初始行數:首先,我們查詢資料函式庫中的初始行數,以此作為基準。
  2. 執行插入操作:然後,我們執行一個插入操作,將新的資料新增到資料函式庫中。
  3. 驗證結果:最後,我們再次查詢資料函式庫中的行數,並斷言行數是否如預期般增加。 這個範例展示瞭如何在整合測試中驗證資料函式庫操作的有效性。然而,在分享的測試環境中,這種簡單的行數比較可能不足夠,因為其他平行的測試也可能正在修改資料函式庫。因此,更嚴格的驗證方法,例如檢查特定插入資料的存在,是必要的。
@Test
public void testInsertDataWithVerification() {
    // 插入資料
    String testData = "Unique Test Data";
    jdbcTemplate.update("INSERT INTO my_table (data) VALUES (?)", testData);
    
    // 驗證插入的資料是否存在
    String retrievedData = jdbcTemplate.queryForObject("SELECT data FROM my_table WHERE data = ?", String.class, testData);
    assertEquals(testData, retrievedData);
}

內容解密:

  1. 插入特定資料:我們插入一條具有唯一性的資料,以便後續驗證。
  2. 查詢並驗證資料:透過查詢剛剛插入的資料,並比較查詢結果與原始資料的一致性,來驗證插入操作的正確性。 這種方法更為嚴謹,因為它直接驗證了所插入資料的存在和正確性,而不是僅僅依賴行數的變化。

軟體測試的品質與挑戰

軟體測試是確保應用程式品質的關鍵步驟,但測試過程中常會遇到許多挑戰。特別是在端對端(end-to-end)測試中,測試的脆弱性(brittleness)是一個常見問題。當網頁佈局或元素名稱發生變化時,測試可能會失敗。此外,底層測試驅動程式(web driver)的問題也可能導致測試失敗,這類別問題的除錯過程相當耗時且令人沮喪。

端對端測試的脆弱性

端對端測試之所以脆弱,是因為它們依賴於對網頁佈局的瞭解。當網頁佈局發生變化時,這些測試很容易就會被破壞。此外,頁面載入速度慢、驅動引擎解析問題、第三方外掛載入失敗等,都可能導致測試失敗。這類別問題並非由被測系統本身引起,而是由測試環境或基礎設施所導致。

過度依賴端對端測試的原因

許多QA團隊傾向於使用端對端測試,是因為這是他們與應用程式互動的慣用方式。然而,這種做法可能導致對生產資料(production data)的過度依賴。生產資料中可能包含錯誤或特例,這使得測試結果可能無法反映真實的正確性。隨著單元測試(unit tests)的減少和端對端測試的增加,測試套件可能會從檢查正確性轉變為檢查一致性,而這兩者並不總是一致。

測試結構與端對端測試的範圍

端對端測試位於測試金字塔(testing pyramid)的頂端,數量較少但覆寫的功能卻很廣泛。以一個電子商務網站的訂單生成流程為例,一個端對端測試可能涵蓋以下步驟:

  1. 登入網站
  2. 搜尋商品目錄
  3. 將商品加入購物車
  4. 執行結帳流程(付款)
  5. 驗證確認郵件或收據是否已傳送

這些步驟看似簡單,但實際上卻涵蓋了多個底層功能,例如資料函式庫連線、搜尋功能、購物車功能、支付處理、郵件通知等。這種廣泛的覆寫使得單個端對端測試能夠驗證多個業務邏輯。

最佳實踐:限制端對端測試的數量

為了避免過多的維護成本和隨機失敗,應該限制端對端測試的數量,專注於核心業務功能的測試。對於電子商務網站來說,核心功能可能包括:

內容解密:

上述提到限制端對端測試數量的原因在於其維護成本高且容易失敗,因此我們需要專注在最重要的業務流程上,像是電子商務網站的核心功能包含使用者登入、商品搜尋、購物車操作、付款流程等,這些都需要透過嚴格的測試來確保其穩定性和正確性。