返回文章列表

Python依賴注入模式與單元測試實踐

本文探討 Python 依賴注入模式的應用,包含建構子注入和裝飾器注入兩種方式,並結合 Mock 和 Stub 技術,示範如何在單元測試中隔離依賴,提升程式碼可測性。同時,文章也涵蓋了 Python 程式碼風格的最佳實踐、常見反模式以及相關修正工具,幫助開發者編寫更乾淨、更易維護的程式碼。

軟體開發 單元測試

依賴注入模式已成為現代軟體開發中不可或缺的一部分,它能有效降低程式碼耦合度,提升程式碼的可測試性和可維護性。本文除了介紹建構子注入和裝飾器注入兩種常見的依賴注入方式外,更著重於如何在單元測試中運用 Mock 和 Stub 技術來驗證程式碼的正確性。此外,良好的程式碼風格也是確保程式碼品質的關鍵,因此本文也提供了一些 Python 程式碼風格的最佳實踐和常見反模式的修正建議,並推薦了一些常用的程式碼風格檢查工具,幫助開發者編寫更符合規範且易於維護的程式碼。

依賴注入(Dependency Injection)模式詳解

依賴注入是一種軟體設計模式,用於實作控制反轉(Inversion of Control),從而提高程式碼的模組化程度和可測試性。本篇文章將探討依賴注入模式,並透過例項演示其在單元測試中的應用。

使用模擬物件進行依賴注入

在單元測試中,我們經常需要隔離被測試的類別與其依賴的其他類別或服務。以下是一個天氣服務(WeatherService)的例子,展示瞭如何使用依賴注入和模擬物件(Mock Object)來進行單元測試。

首先,我們定義一個天氣API客戶端介面及其真實實作:

# di_with_mock.py
class WeatherApiClient:
    def fetch_weather(self, location):
        # 真實的天氣API呼叫
        return f"Real weather data for {location}"

class WeatherService:
    def __init__(self, api_client):
        self.api_client = api_client

    def get_weather(self, location):
        return self.api_client.fetch_weather(location)

內容解密:

  • WeatherApiClient 類別代表了一個真實的天氣API客戶端。
  • WeatherService 類別依賴於 WeatherApiClient,並透過建構子注入該依賴。

接下來,我們建立一個模擬的天氣API客戶端,用於單元測試:

# test_di_with_mock.py
import unittest
from di_with_mock import WeatherService

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

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()

內容解密:

  • MockWeatherApiClient 類別模擬了 WeatherApiClient 的行為,但傳回假資料。
  • TestWeatherService 中,我們透過將 MockWeatherApiClient 的例項注入到 WeatherService 中,測試了 get_weather 方法的功能。

使用裝飾器實作依賴注入

除了直接透過建構子注入依賴外,我們還可以使用裝飾器來簡化依賴注入的過程。以下是一個通知服務(NotificationService)的例子,展示瞭如何使用裝飾器實作依賴注入。

首先,我們定義通知傳送者的介面和具體實作:

# di_with_decorator.py
from typing import Protocol

class NotificationSender(Protocol):
    def send(self, message: str):
        """傳送通知"""
        ...

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

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

內容解密:

  • NotificationSender 是一個通訊協定,定義了傳送通知的方法。
  • EmailSenderSMSSender 分別實作了透過電子郵件和簡訊傳送通知的功能。

然後,我們定義了一個 NotificationService 類別,並使用裝飾器注入通知傳送者:

def inject_sender(sender_cls):
    def decorator(cls):
        cls.sender = sender_cls()
        return cls
    return decorator

@inject_sender(EmailSender)
class NotificationService:
    def notify(self, message):
        self.sender.send(message)

內容解密:

  • inject_sender 裝飾器用於將指定的通知傳送者注入到 NotificationService 中。
  • 透過使用 @inject_sender(EmailSender),我們指定了使用 EmailSender 作為通知傳送者。

最後,我們可以進行單元測試,使用存根(Stub)類別來驗證通知是否正確傳送:

# test_di_with_decorator.py
import unittest
from di_with_decorator import NotificationService, inject_sender

class EmailSenderStub:
    def __init__(self):
        self.messages_sent = []

    def send(self, message: str):
        self.messages_sent.append(message)

class TestNotifService(unittest.TestCase):
    @inject_sender(EmailSenderStub)
    class TestNotificationService(NotificationService):
        pass

    def test_notify_with_email(self):
        service = self.TestNotificationService()
        service.notify("Hello")
        self.assertEqual(service.sender.messages_sent, ["Hello"])

內容解密:

  • EmailSenderStub 用於記錄傳送的通知訊息,以便進行驗證。
  • TestNotifService 中,我們使用 EmailSenderStub 作為通知傳送者,測試了 notify 方法的功能。

依賴注入模式的測試實踐與程式碼最佳化

依賴注入(Dependency Injection, DI)是一種重要的軟體設計模式,它能夠提升程式碼的彈性、可測試性和可維護性。在本章中,我們將探討如何使用依賴注入模式來最佳化程式碼,並結合單元測試確保其正確性。

使用裝飾器管理依賴

在前面的章節中,我們已經瞭解了依賴注入的基本概念。現在,我們將進一步探討如何使用裝飾器(decorator)來簡化依賴的管理。以下是一個具體的範例:

def inject_sender(sender_type):
    def decorator(cls):
        class Wrapper(cls):
            def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                self.sender = sender_type()
        return Wrapper
    return decorator

程式碼解析:

  1. inject_sender 函式接受一個 sender_type 引數,並傳回一個裝飾器。
  2. 裝飾器內部定義了一個 Wrapper 類別,繼承自原始類別 cls
  3. Wrapper__init__ 方法中,初始化了 sender 屬性為指定的 sender_type 例項。

單元測試範例

為了驗證依賴注入的正確性,我們需要撰寫單元測試。以下是一個使用 stub 技術進行測試的範例:

import unittest
from unittest.mock import MagicMock

class NotificationSender:
    def send(self, message):
        pass

class EmailSenderStub(NotificationSender):
    def __init__(self):
        self.messages_sent = []

    def send(self, message):
        self.messages_sent.append(message)

@inject_sender(EmailSenderStub)
class NotificationService:
    def __init__(self):
        self.sender = None

    def notify(self, message):
        self.sender.send(message)

class TestNotificationService(unittest.TestCase):
    def test_notify_with_email(self):
        email_stub = EmailSenderStub()
        service = NotificationService()
        service.sender = email_stub
        service.notify("Test Email Message")
        self.assertIn("Test Email Message", email_stub.messages_sent)

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

測試解析:

  1. 我們定義了一個 EmailSenderStub 類別,用於類別比電子郵件傳送的功能。
  2. 使用 @inject_sender(EmailSenderStub) 裝飾 NotificationService 類別,使其依賴於 EmailSenderStub
  3. 在測試案例中,我們建立了一個 NotificationService 例項,並將 email_stub 注入其中。
  4. 呼叫 notify 方法後,驗證訊息是否正確傳送。

程式碼風格與最佳實踐

在 Python 程式設計中,遵循 PEP 8 風格是非常重要的。以下是一些常見的程式碼風格問題及其解決方案:

  1. 縮排:使用四個空格進行縮排,避免混合使用 tab 和空格。
  2. 行長度:限制每行程式碼的最大長度為 79 個字元,以提高可讀性。
  3. 空白行:頂層函式和類別定義之間使用兩個空白行,類別內的方法定義之間使用一個空白行。

常見的程式碼風格違規與修正工具

為了保持程式碼的一致性和可讀性,我們可以使用一些自動化工具來檢查和修正程式碼風格違規,例如:

  • Black:用於自動格式化 Python 程式碼。
  • isort:用於排序和格式化 import 陳述式。
  • Ruff:一個快速的 Python linter 和程式碼格式化工具。

這些工具可以幫助開發者快速識別和修正程式碼中的風格問題,從而提高整體程式碼品質。

修正程式碼風格違規的工具與最佳實踐

在軟體開發過程中,保持一致的程式碼風格對於團隊協作和維護至關重要。Python 提供了一系列的工具和來幫助開發者遵循特定的程式碼風格。

匯入模組的最佳實踐

正確地組織和排序匯入陳述式對於程式碼的可讀性和維護性非常重要。根據 Python 的風格,匯入陳述式應該分開寫並分成三類別:標準函式庫匯入、相關的第三方匯入和本地應用程式或函式庫內的匯入。每組之間應該用空行分隔。

不推薦的寫法

import os, sys
import numpy as np
from mymodule import myfunction

推薦的寫法

import os
import sys
import numpy as np
from mymodule import myfunction

命名慣例

使用描述性的名稱對於變數、函式、類別和模組非常重要。以下是不同型別名稱的具體命名慣例:

  • 函式和變數(包括類別屬性和方法):使用 lower_case_with_underscores
  • 類別:使用 CapWords
  • 常數:使用 ALL_CAPS_WITH_UNDERSCORES

不推薦的寫法

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

class my_class:
    pass

maxValue = 100

推薦的寫法

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

class MyClass:
    pass

MAX_VALUE = 100

註解的最佳實踐

註解應該是完整的句子,首字母大寫,並且清晰簡潔。對於區塊註解和行內註解有特定的建議:

  • 區塊註解通常適用於其後的程式碼,並與該程式碼保持相同的縮排層級。每行區塊註解以 # 和一個空格開始。
  • 行內註解應該謹慎使用。行內註解與陳述式在同一行,並與陳述式之間至少隔兩個空格。

不推薦的寫法

#This is a poorly formatted block comment.
def foo(): #This is a poorly formatted inline comment.
    pass

推薦的寫法

# This is a block comment.
# It spans multiple lines.

def foo():
    pass  # This is an inline comment.

表示式和陳述式中的空白字元

應該避免在以下情況下使用多餘的空白字元:

  • 緊接在括號、方括號或大括號內
  • 緊接在逗號、分號或冒號之前
  • 在指定運算元周圍使用多於一個空格以對齊其他陳述式

正確性反模式

這些反模式如果不加以解決,可能會導致錯誤或非預期的行為。我們將討論最常見的反模式及其替代方案。

使用 type() 函式比較型別

有時,我們需要透過比較來識別值的型別。常見的做法是使用 type() 函式。但是,使用 type() 比較物件型別並不考慮子類別,因此不夠靈活。更好的方法是使用 isinstance() 函式。

示範程式碼

from collections import UserList

class CustomListA(UserList):
    pass

class CustomListB(UserList):
    pass

def compare(obj):
    if type(obj) in (CustomListA, CustomListB):
        print("It's a custom list!")
    else:
        print("It's something else!")

def better_compare(obj):
    if isinstance(obj, UserList):
        print("It's a custom list!")
    else:
        print("It's something else!")

obj1 = CustomListA([1, 2, 3])
obj2 = CustomListB(["a", "b", "c"])

compare(obj1)
compare(obj2)
better_compare(obj1)
better_compare(obj2)

輸出結果

It's a custom list!
It's a custom list!
It's a custom list!
It's a custom list!

使用 isinstance() 的方法更簡單、更靈活,因為它考慮了子類別。

可變預設引數

當定義一個函式,其引數預期為可變值(如列表或字典)時,可能會想提供一個預設引數(分別為 []{})。但是,這樣的函式會保留呼叫之間的變更,導致非預期的行為。

不推薦的寫法

def append_to_list(value, lst=[]):
    lst.append(value)
    return lst

print(append_to_list(1))  # [1]
print(append_to_list(2))  # [1, 2],非預期的結果

推薦的寫法

def append_to_list(value, lst=None):
    if lst is None:
        lst = []
    lst.append(value)
    return lst

print(append_to_list(1))  # [1]
print(append_to_list(2))  # [2],符合預期

圖表示例:類別關係圖

@startuml
note
  無法自動轉換的 Plantuml 圖表
  請手動檢查和調整
@enduml

圖表翻譯: 此圖示展示了 UserListCustomListACustomListB 之間的繼承關係。CustomListACustomListBUserList 的子類別。

遵循這些最佳實踐和,可以幫助開發者編寫更乾淨、更可維護的 Python 程式碼。利用工具如 Flake8 和 IDE(如 Visual Studio Code 或 PyCharm)可以幫助發現並修正常見的程式碼風格違規和正確性反模式,從而提高開發效率和程式碼品質。