在分散式系統中,服務間的依賴關係錯綜複雜,任何單點故障都可能引發連鎖反應,導致整個系統癱瘓。斷路器模式作為一種有效的容錯機制,可以隔離故障服務,防止錯誤蔓延。同時,在軟體測試過程中,模擬物件模式則能有效隔離待測元件,提升測試效率和可靠性。本文將深入探討這兩種模式的原理、應用場景以及 Python 實作方式,並進一步說明依賴注入模式如何與模擬物件模式結合,提升程式碼的可測試性和可維護性。 透過斷路器機制,系統可以避免對已知故障服務的持續請求,從而保障核心服務的穩定執行。而模擬物件的使用則讓開發者得以在受控環境下進行測試,驗證程式碼邏輯的正確性,並提升程式碼的可測試性。

分散式系統模式中的斷路器模式

在現代軟體開發中,斷路器(Circuit Breaker)模式是一種重要的設計模式,用於提高系統的容錯性和穩定性。尤其是在分散式系統中,當系統依賴外部服務或資源時,斷路器模式可以有效防止系統因外部故障而導致的連鎖反應。

斷路器模式的應用場景

斷路器模式主要應用於以下場景:

  1. 電子商務結帳流程:當支付閘道發生故障時,斷路器可以暫停進一步的支付嘗試,防止系統過載。
  2. 速率限制的API:當API達到其速率限制時,斷路器可以停止額外的請求,以避免受到懲罰。

斷路器模式的實作

以下是一個使用Python實作斷路器模式的範例。我們將使用pybreaker函式庫來實作斷路器。

import pybreaker
from datetime import datetime
import random
from time import sleep

# 定義斷路器,在連續失敗2次後開啟斷路器,5秒後重置
breaker = pybreaker.CircuitBreaker(fail_max=2, reset_timeout=5)

@breaker
def fragile_function():
    if not random.choice([True, False]):
        print(" / OK", end="")
    else:
        print(" / FAIL", end="")
        raise Exception("這是一個範例例外")

def main():
    while True:
        print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), end="")
        try:
            fragile_function()
        except Exception as e:
            print(" / {} {}".format(type(e), e), end="")
        finally:
            print("")
        sleep(1)

if __name__ == "__main__":
    main()

程式碼解析:

此範例程式碼展示瞭如何使用pybreaker函式庫實作斷路器模式。我們定義了一個名為fragile_function的函式,該函式模擬了一個可能失敗的外部呼叫。透過使用@breaker裝飾器,我們將斷路器應用於該函式。當連續失敗達到設定的閾值(此例中為2次)時,斷路器將開啟,後續的呼叫將立即失敗,直到重置超時(此例中為5秒)後才會再次嘗試。

斷路器模式的工作原理

當斷路器開啟時,所有對fragile_function的呼叫將立即失敗並引發CircuitBreakerError例外,而不會實際執行函式。這樣可以防止系統對已經故障的服務進行不必要的呼叫,從而提高系統的穩定性。

其他分散式系統模式

除了斷路器模式外,還有許多其他分散式系統模式可用於提高系統的可靠性和可擴充套件性,例如:

  • 命令查詢責任分離(CQRS):將讀取和寫入資料的責任分離,以最佳化資料存取和可擴充套件性。
  • 兩階段提交:一種分散式交易協定,確保跨多個參與資源的原子性和一致性。
  • Saga模式:一系列本地交易,形成一個分散式交易,提供補償機制以維護部分失敗或中止交易的一致性。
  • Sidecar模式:在主要服務旁邊佈署額外的輔助服務,以增強功能,如監控、記錄或安全功能,而無需直接修改主應用程式。
  • 服務註冊:集中管理分散式系統中的服務註冊和發現,允許服務動態註冊和發現彼此,以促進通訊和可擴充套件性。
  • Bulkhead模式:將資源或元件分割槽,以隔離故障並防止連鎖故障影響系統的其他部分。

測試模式

在前面的章節中,我們討論了架構模式和特定使用案例的模式,如並發或效能。在本章中,我們將探討特別適用於測試的設計模式。這些模式有助於隔離元件,使測試更加可靠,並促程式式碼的可重用性。

Mock Object模式

Mock Object模式是一種強大的工具,用於在測試過程中隔離元件。Mock物件有助於建立受控的測試環境,並驗證元件之間的互動。

Mock Object模式的特點:

  1. 隔離:Mock物件隔離了被測試的程式碼單元,確保測試在可預測且無外部副作用的受控環境中執行。
  2. 行為驗證:透過Mock物件,您可以驗證測試過程中發生的特定行為,例如方法呼叫或屬性存取。
  3. 簡化:Mock物件簡化了測試的設定,避免了建立複雜的依賴物件。

Mock Object模式的實作

以下是一個使用Python中的unittest.mock模組實作Mock Object模式的範例:

import unittest
from unittest.mock import MagicMock

class PaymentGateway:
    def process_payment(self, amount):
        # 模擬支付閘道處理支付
        pass

class PaymentProcessor:
    def __init__(self, payment_gateway):
        self.payment_gateway = payment_gateway

    def process_payment(self, amount):
        return self.payment_gateway.process_payment(amount)

class TestPaymentProcessor(unittest.TestCase):
    def test_process_payment(self):
        # 建立Mock支付閘道
        mock_payment_gateway = MagicMock()
        mock_payment_gateway.process_payment.return_value = True

        # 建立支付處理器並使用Mock支付閘道
        payment_processor = PaymentProcessor(mock_payment_gateway)
        result = payment_processor.process_payment(100)

        # 驗證結果和Mock支付閘道的呼叫
        self.assertTrue(result)
        mock_payment_gateway.process_payment.assert_called_once_with(100)

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

程式碼解析:

此範例展示瞭如何使用unittest.mock模組建立Mock物件,以隔離被測試的PaymentProcessor類別。透過Mock支付閘道,我們可以控制其行為並驗證PaymentProcessor類別的正確性。

圖表翻譯:

此圖示展示了測試過程中使用Mock物件的流程。首先,決定是否使用Mock物件。如果使用,則建立並設定Mock物件的行為。接著,執行測試並驗證結果。如果不使用Mock物件,則直接使用真實物件進行測試。該圖清晰地說明瞭測試過程中Mock物件的使用方式,有助於理解測試邏輯。

模擬物件模式在測試中的應用

在軟體開發的測試階段,模擬物件(Mock Object)模式是一種非常實用的技術。它允許開發者模擬複雜或不可用的依賴項,從而專注於被測試單元的功能驗證。本文將深入探討模擬物件模式的原理、實作方式及其在單元測試和整合測試中的應用。

模擬物件與存根的比較

模擬物件和存根(Stub)都是用於替換真實實作的測試替身(Test Double)。然而,它們之間存在關鍵差異。存根主要用於提供間接輸入給被測試程式碼,而模擬物件除了提供輸入外,還能驗證與被測試程式碼的互動。這使得模擬物件在許多測試場景中更加靈活。

實際應用範例

在現實世界中,我們可以找到許多類別似模擬物件概念的工具或系統。例如:

  • 飛行模擬器:用於培訓飛行員,讓他們在安全可控的環境中學習處理各種飛行場景。
  • 心肺復甦(CPR)假人:用於教學,讓學生在模擬人體上練習CPR技術。
  • 碰撞測試假人:用於汽車安全測試,提供有關汽車碰撞的寶貴資料,而無需冒真人生命風險。

模擬物件模式的使用場景

單元測試

在單元測試中,模擬物件用於替換被測試程式碼的複雜、不可靠或不可用的依賴項。這使得開發者能夠專注於測試單元本身的功能,而不是其與外部系統的互動。例如,當測試一個從API取得資料的服務時,可以使用模擬物件來模擬API的回應,從而測試服務在不同資料場景下的行為,而無需實際呼叫API。

整合測試

在整合測試中,模擬物件同樣發揮重要作用,但焦點轉向元件之間的互動。模擬物件可以用於模擬尚未開發完成或測試成本過高的元件。例如,在微服務架構中,可以使用模擬物件來代表正在開發或暫時不可用的服務,從而測試其他服務與其互動的正確性。

行為驗證

模擬物件模式也常用於行為驗證,即驗證物件之間的互動是否符合預期。可以設定模擬物件預期特定的呼叫、引數和互動順序,從而實作對系統行為的精確測試。例如,在MVC架構中,可以使用模擬物件來驗證控制器是否正確呼叫身份驗證和日誌記錄服務。

模擬物件模式的實作

以下是一個使用Python unittest模組實作模擬物件模式的範例。假設我們有一個日誌記錄器類別,將訊息寫入檔案:

import unittest
from unittest.mock import mock_open, patch

class Logger:
 def __init__(self, filepath):
 self.filepath = filepath

 def log(self, message):
 with open(self.filepath, "a") as file:
 file.write(f"{message}\n")

class TestLogger(unittest.TestCase):
 def test_log(self):
 msg = "Hello, logging world!"
 m_open = mock_open()
 with patch("builtins.open", m_open):
 logger = Logger("dummy.log")
 logger.log(msg)
 
 m_open.assert_called_once_with("dummy.log", "a")
 m_open().write.assert_called_once_with(f"{msg}\n")

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

程式碼解析

此範例展示瞭如何使用unittest.mock.patch來模擬內建的open()函式,從而測試Logger類別的log()方法。測試過程中,我們驗證了open()函式是否以正確的引數被呼叫,以及寫入檔案的內容是否符合預期。

圖表翻譯

此圖示展示了測試Logger類別的流程。首先,我們嘗試模擬內建的open()函式。如果模擬成功,我們建立Logger物件並呼叫其log()方法。接著,我們驗證open()函式的呼叫引數是否正確,以及寫入檔案的內容是否符合預期。如果所有驗證都透過,測試即成功完成;否則,測試失敗。

依賴注入模式的原理與實作

依賴注入(Dependency Injection, DI)是一種軟體設計模式,旨在降低類別之間的耦合度,提高模組化和可測試性。其核心思想是將類別的依賴關係從內部建立轉變為外部傳遞,從而實作類別之間的鬆散耦合。

實際案例

在現實生活中,我們可以觀察到許多依賴注入模式的例子:

  • 電器裝置與電源插座:不同的電器可以插入不同的電源插座來使用電力,而無需直接和永久地連線線路。
  • 相機鏡頭:攝影師可以根據不同的環境和需求更換相機鏡頭,而無需更換相機本身。
  • 模組化火車系統:在模組化火車系統中,可以根據每趟旅程的需求新增或移除各個車廂(如臥鋪車、餐車或行李車)。

依賴注入模式的應用場景

在Web應用程式中,將資料函式庫連線物件注入到儲存函式庫或服務等元件中,可以增強模組化和可維護性。這種做法允許在不同的資料函式庫引擎或組態之間輕鬆切換,而無需直接更改元件的程式碼。同時,它也大大簡化了單元測試的過程。

另一個應用場景是管理跨不同環境(開發、測試、生產等)的組態設定。透過依賴注入,可以減少模組與其組態來源之間的耦合,從而更輕鬆地管理和切換環境。在單元測試中,可以注入特定的組態以測試模組在不同組態下的效能,確保其健全性和功能性。

使用模擬物件實作依賴注入模式

在這個例子中,我們將建立一個簡單的場景,其中WeatherService類別依賴WeatherApiClient介面來取得天氣資料。在單元測試中,我們將注入該API客戶端的模擬版本。

首先,我們使用Python的Protocol功能定義WeatherApiClient介面:

from typing import Protocol

class WeatherApiClient(Protocol):
    def fetch_weather(self, location: str) -> str:
        """取得指定地點的天氣資料"""
        ...

接下來,我們實作RealWeatherApiClient類別,該類別實作了WeatherApiClient介面。在真實場景中,fetch_weather()方法將呼叫天氣服務,但為了簡化範例,我們僅傳回一個表示天氣資料結果的字串:

class RealWeatherApiClient:
    def fetch_weather(self, location: str) -> str:
        return f"Real weather data for {location}"

然後,我們建立WeatherService類別,該類別使用實作WeatherApiClient介面的物件來取得天氣資料:

class WeatherService:
    def __init__(self, weather_api: WeatherApiClient):
        self.weather_api = weather_api

    def get_weather(self, location: str) -> str:
        return self.weather_api.fetch_weather(location)

程式碼解析:

此程式碼定義了一個名為WeatherService的類別,用於取得天氣資料。建構子接收一個實作WeatherApiClient介面的物件作為引數,並將其儲存在例項變數weather_api中。get_weather()方法呼叫weather_apifetch_weather()方法來取得指定地點的天氣資料。這種設計允許在不修改WeatherService類別的情況下,更換不同的天氣資料來源。

手動測試範例

為了手動測試範例,我們建立一個WeatherService例項,並傳入RealWeatherApiClient()

if __name__ == "__main__":
    ws = WeatherService(RealWeatherApiClient())
    print(ws.get_weather("Paris"))

執行此程式碼後,輸出結果為:

Real weather data for Paris

使用模擬物件進行單元測試

在單元測試中,我們建立一個MockWeatherApiClient類別來模擬天氣API客戶端的行為:

class MockWeatherApiClient:
    def fetch_weather(self, location: str) -> str:
        return f"Mock weather data for {location}"

然後,我們編寫測試案例類別TestWeatherService,並在測試函式中注入模擬API客戶端:

import unittest
from di_with_mock import WeatherService

class TestWeatherService(unittest.TestCase):
    def test_get_weather(self):
        mock_api = MockWeatherApiClient()
        weather_service = WeatherService(mock_api)
        self.assertEqual(
            weather_service.get_weather("Anywhere"),
            "Mock weather data for Anywhere",
        )

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

執行此測試後,輸出結果為:

.
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
-
---
--
Ran 1 test in 0.000s
OK

圖表說明:依賴注入模式的流程

@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

圖表翻譯:

此圖示展示了依賴注入模式的流程。首先,系統會判斷是否使用模擬物件。如果使用模擬物件,則建立模擬API客戶端;否則,建立真實API客戶端。接著,建立WeatherService例項,並呼叫其get_weather()方法。最後,傳回天氣資料。這個流程清晰地說明瞭依賴注入模式如何在不同的API客戶端之間進行切換,從而提高了系統的靈活性和可測試性。

使用裝飾器實作依賴注入模式

除了使用模擬物件外,我們還可以使用裝飾器來簡化依賴注入的過程。下面是一個簡單的範例,示範如何使用裝飾器來實作依賴注入。

首先,我們定義一個NotificationSender介面,概述了任何通知傳送者應具備的方法:

from typing import Protocol

class NotificationSender(Protocol):
    def send(self, message: str) -> None:
        """傳送指定訊息的通知"""
        ...

然後,我們實作兩個特定的通知傳送者:EmailSender類別實作透過電子郵件傳送通知,而SMSSender類別實作透過簡訊傳送通知:

class EmailSender:
    def send(self, message: str) -> None:
        print(f"Sending Email: {message}")

class SMSSender:
    def send(self, message: str) -> None:
        print(f"Sending SMS: {message}")

接下來,我們定義一個通知服務類別NotificationService,並使用裝飾器來注入通知傳送者:

class NotificationService:
    sender: NotificationSender = None

    def notify(self, message: str) -> None:
        self.sender.send(message)

程式碼解析:

此程式碼定義了一個名為NotificationService的類別,用於傳送通知。類別屬性sender儲存了一個實作NotificationSender介面的物件。notify()方法呼叫sendersend()方法來傳送指定訊息的通知。透過使用裝飾器注入通知傳送者,可以在不修改NotificationService類別的情況下,更換不同的通知傳送方式。