Python 的 nose2 測試框架在標準 unittest 模組基礎上提供更豐富的功能。首先,安裝 nose2 套件並將測試檔案更名為以 test 開頭。執行測試時,可透過指令指定單一檔案或單一測試案例,有效管理測試流程,尤其在大型專案中能節省時間和資源。整合 geoip2 函式庫時,使用 Mocking 模擬外部 API 呼叫,避免實際連線,確保測試的穩定性和效率。

為了評估測試完整性,使用 Coverage 函式庫計算測試覆寫率,並可生成 HTML 格式的報告以便於檢視。將覆寫率評估整合到測試檔案中,能自動產生報告,方便追蹤程式碼的測試覆寫情況。此外,使用 cProfile 和 Werkzeug 的 ProfilerMiddleware 進行效能分析,找出應用程式中的效能瓶頸,例如 GeoIP 資料函式庫查詢,有助於最佳化程式碼效能。最後,文章說明瞭使用 Apache 搭配 mod_wsgi,以及 uWSGI 和 Nginx 佈署 Flask 應用程式的步驟,並提供設定檔範例,方便讀者參考。

使用nose2進行單元測試

nose2是一個Python測試框架,它擴充套件了標準的unittest模組,提供了更多的功能和彈性。在本章中,我們將探討如何使用nose2來執行測試。

準備工作

首先,我們需要安裝nose2函式庫:

$ pip install nose2

nose2有一個測試檔案探索機制,要求測試檔案必須以test開頭命名。由於我們的測試檔案命名為app_tests.py,我們需要將其重新命名為test_app.py。在終端機中,可以執行以下命令:

$ mv app_tests.py test_app.py

執行測試

我們可以使用以下命令執行應用程式中的所有測試:

$ nose2 -v

這將找出應用程式中的所有測試並執行它們,即使我們有多個測試檔案。

若要執行單一測試檔案,可以執行以下命令:

$ nose2 test_app

若要執行單一測試,可以執行以下命令:

$ nose2 test_app.CatalogTestCase.test_home

這在我們有大量測試案例且應用程式佔用大量記憶體時非常重要。如此一來,我們可以只執行與變更相關的測試,或是執行因特定變更而失敗的測試。

使用mocking避免外部API存取

在某些情況下,我們的應用程式會與第三方服務整合,這些服務可能需要付費或是影響統計資料。Mocking在這種情況下扮演著非常重要的角色。在本文中,我們將整合geoip2函式庫並使用mocking進行測試。

準備工作

首先,我們需要安裝geoip2函式庫和對應的資料函式庫:

$ pip install geoip2

我們還需要從MaxMind網站下載免費的geoip資料函式庫,並將其解壓縮到專案資料夾中。

修改程式碼

我們需要對my_app/catalog/models.pymy_app/catalog/views.pytemplates/product.html進行一些小修改。

my_app/catalog/models.py中,我們新增了一個名為user_timezone的欄位:

class Product(db.Model):
    # ... 其他欄位 ...
    user_timezone = db.Column(db.String(255))

    def __init__(self, name, price, category, image_path, user_timezone=''):
        # ... 其他欄位初始化 ...
        self.user_timezone = user_timezone

my_app/catalog/views.py中,我們修改了create_product()方法以包含時區資訊:

import geoip2.database, geoip2.errors

@catalog.route('/<lang>/product-create', methods=['GET', 'POST'])
def create_product():
    form = ProductForm()
    if form.validate_on_submit():
        # ... 未修改的程式碼 ...
        reader = geoip2.database.Reader('GeoLite2-City_20230113/GeoLite2-City.mmdb')
        try:
            match = reader.city(request.remote_addr)
        except geoip2.errors.AddressNotFoundError:
            match = None
        product = Product(name, price, category, filename, match and match.location.time_zone or 'Localhost')
        # ... 未修改的程式碼 ...

使用mocking進行測試

我們需要修改test_app.py以適應geoip查詢的mocking:

from unittest import mock
import geoip2.records

class CatalogTestCase(unittest.TestCase):
    def setUp(self):
        # ... 未修改的程式碼 ...
        self.geoip_city_patcher = mock.patch('geoip2.models.City', location=geoip2.records.Location(time_zone='America/Los_Angeles'))
        PatchedGeoipCity = self.geoip_city_patcher.start()
        self.geoip_reader_patcher = mock.patch('geoip2.database.Reader')
        PatchedGeoipReader = self.geoip_reader_patcher.start()
        PatchedGeoipReader().city.return_value = PatchedGeoipCity
        with self.app.app_context():
            db.create_all()
        from my_app.catalog.views import catalog
        self.app.register_blueprint(catalog)
        self.client = self.app.test_client()

    def tearDown(self):
        self.geoip_city_patcher.stop()
        self.geoip_reader_patcher.stop()
        os.remove(self.test_db_file)

    def test_create_product(self):
        "測試建立新產品"
        # ... 未修改的程式碼 ...
        rv = self.client.post('/en/product-create', data={'name': 'iPhone 5', 'price': 549.49, 'company': 'Apple', 'category': 1, 'image': tempfile.NamedTemporaryFile()})
        self.assertEqual(rv.status_code, 302)
        rv = self.client.get('/en/product/1')
        self.assertEqual(rv.status_code, 200)
        self.assertTrue('iPhone 5' in rv.data.decode("utf-8"))
        self.assertTrue('America/Los_Angeles' in rv.data.decode("utf-8"))

執行測試

執行以下命令以檢視測試是否透過:

$ nose2 test_app.CatalogTestCase.test_create_product -v

詳細解析

  1. 設定 Mocking:在 setUp 方法中,我們使用 mock.patchgeoip2.models.Citygeoip2.database.Reader 進行了 Mocking,以模擬 geoip 查詢的結果。

    • geoip2.models.City 的 Mocking 設定了 location 屬性中的 time_zone'America/Los_Angeles'
    • geoip2.database.Reader 的 Mocking 設定了其 city 方法的傳回值為我們之前 Mock 的 PatchedGeoipCity 例項。
  2. 停止 Mocking:在 tearDown 方法中,我們停止了之前啟動的 Mock patchers,以確保實際呼叫不受影響。

  3. 測試建立產品:在 test_create_product 方法中,我們驗證了產品建立後,時區資訊是否正確地被渲染在產品頁面中。

相關資源
  • nose2 檔案:https://docs.nose2.io/en/latest/index.html
  • unittest.mock 檔案:https://docs.python.org/3/library/unittest.mock.html

測試覆寫率的評估

在之前的章節中,我們已經介紹瞭如何撰寫測試案例,但評估測試的完整程度是一個重要的課題,這就涉及到所謂的「覆寫率」。覆寫率是指我們的程式碼有多少部分被測試所涵蓋。覆寫率的百分比越高,表示測試越全面(儘管高覆寫率並不是衡量測試品質的唯一標準)。在本章節中,我們將介紹如何評估應用程式的程式碼覆寫率。

準備工作

為了評估測試覆寫率,我們將使用一個名為 coverage 的函式庫。安裝指令如下:

$ pip install coverage

操作步驟

  1. 使用命令列評估覆寫率:最簡單的方式是直接透過命令列執行以下指令:

$ coverage run –source=<應用程式資料夾名稱> –omit=test_app.py,run.py test_app.py


   這裡的 `--source` 引數指定了需要被評估覆寫率的目錄,而 `--omit` 引數則用於指定在評估過程中需要被忽略的檔案。

2. **輸出報告到終端機**:執行以下指令可以在終端機上列印出報告:

   ```bash
$ coverage report
  1. 輸出HTML格式的報告:執行以下指令可以生成HTML格式的報告:

$ coverage html


   這將在目前的工作目錄下建立一個名為 `htmlcov` 的新資料夾。在該資料夾中開啟 `index.html` 檔案,即可在瀏覽器中檢視完整的報告。

### 將覆寫率評估整合到測試檔案中

我們也可以在測試檔案中加入一些程式碼,以便每次執行測試時都能自動生成覆寫率報告。以下是需要在 `test_app.py` 中加入的程式碼片段:

1. **開始評估覆寫率**:在檔案的最前面加入以下程式碼以啟動覆寫率評估:

   ```python
import coverage
cov = coverage.coverage(
    omit = [
        '/Users/apple/workspace/flask-cookbook-3/Chapter-10/lib/python3.10/site-packages/*',
        'test_app.py'
    ]
)
cov.start()

這段程式碼匯入了 coverage 函式庫並建立了一個物件,指定了需要忽略的檔案(如第三方套件和測試檔案本身),然後啟動了覆寫率評估。

  1. 結束評估並輸出報告:在檔案的最後,將 if __name__ == '__main__': 下的程式碼修改如下:

if name == ‘main’: try: unittest.main() finally: cov.stop() cov.save() cov.report() cov.html_report(directory = ‘coverage’) cov.erase()


   這段程式碼確保在 `unittest.main()` 執行完成後,會停止覆寫率評估、儲存結果、輸出報告(包括終端機報告和HTML報告),最後刪除臨時檔案。

### 執行測試並檢視報告

執行以下指令即可執行測試並檢視覆寫率報告:

```bash
$ python test_app.py

輸出結果將與直接使用 coverage report 指令相似。

使用效能分析找出效能瓶頸

效能分析是衡量應用程式效能的重要工具,尤其是在決定擴充套件應用程式之前。Python內建了一個名為 cProfile 的效能分析工具,而 Werkzeug 提供了 ProfilerMiddleware,它是根據 cProfile 的封裝,讓使用更加方便。在本章節中,我們將使用 ProfilerMiddleware 來找出可能影響效能的瓶頸。

操作步驟

  1. 建立新的執行檔案:建立一個名為 generate_profile.py 的新檔案,與 run.py 類別似,但加入了 ProfilerMiddleware

from werkzeug.middleware.profiler import ProfilerMiddleware from my_app import app app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions = [10]) app.run(debug=True)


   這段程式碼匯入了 `ProfilerMiddleware`,並將其套用到 Flask 應用程式上,限制輸出前10個最耗時的呼叫。

2. **執行應用程式**:使用 `generate_profile.py` 執行應用程式,並進行一些操作(如新增產品),即可在輸出中看到效能分析報告。

### 檢視效能分析報告

從報告中可以看出哪個呼叫最耗時間。例如,在建立新產品的操作中,報告顯示對 GeoIP 資料函式庫的呼叫最為耗時。因此,若要改善效能,這是一個值得關注的點。

## 佈署與佈署後監控

佈署應用程式和管理佈署後的應用程式與開發同樣重要。選擇適當的佈署方式取決於具體需求。正確的佈署對於安全性和效能至關重要。本章將介紹各種佈署技術和一些佈署後使用的監控工具。

### 選擇合適的工具

每個工具都有其特點。例如,過度的監控可能會增加應用程式和開發者的負擔,而監控不足則可能導致未被發現的使用者錯誤和整體使用者滿意度下降。因此,選擇合適的工具對於簡化工作流程至關重要。

### 佈署後監控工具

本章將討論 New Relic 和 Sentry 等工具。Sentry 在第10章已經介紹過,它對於開發者來說非常有益。這些工具可以幫助監控應用程式,並根據需求提供相應的功能。

## 佈署與佈署後的管理

本章節將涵蓋多個獨立的佈署與管理主題,您可以根據需求選擇實施其中一個或多個主題。作為軟體開發者,您可以根據具體情況選擇最合適的工具或函式庫。

### 使用Apache佈署

在本文中,我們將學習如何使用Apache HTTP伺服器佈署Flask應用程式。對於Python網頁應用程式,我們將使用`mod_wsgi`,它實作了一個簡單的Apache模組,可以託管任何支援WSGI介面的Python應用程式。

#### 準備工作

首先,請確保您的系統上安裝了最新版本的Apache HTTP伺服器。對於macOS,可以使用Homebrew安裝:
```bash
$ brew install httpd

對於Ubuntu,可以升級現有的Apache版本:

$ sudo apt update
$ sudo apt install python3-dev
$ sudo apt install apache2 apache2-dev

接下來,在虛擬環境中安裝mod_wsgi

$ pip install mod_wsgi

佈署步驟

  1. 建立一個名為wsgi.py的檔案,內容如下:
from my_app import app as application

這是因為mod_wsgi期望應用程式物件被命名為application

  1. 使用以下命令啟動伺服器:
$ mod_wsgi-express start-server wsgi.py --processes 4

現在,您可以存取http://localhost:8000/來檢視應用程式。

使用uWSGI和Nginx佈署

對於已經熟悉uWSGI和Nginx的人來說,不需要太多解釋。uWSGI是一個協定和應用程式伺服器,提供了一個完整的堆積疊來建立主機服務。Nginx是一個反向代理和HTTP伺服器,非常輕量且能夠處理幾乎無限的請求。在本文中,我們將使用uWSGI和Nginx一起佈署我們的應用程式。

準備工作

我們將使用上一節中的應用程式,並使用相同的wsgi.py檔案。

安裝Nginx和uWSGI:

$ sudo apt-get install nginx
$ pip install pyuwsgi

佈署步驟

  1. 建立一個名為uwsgi.ini的檔案,內容如下:
[uwsgi]
http-socket = :9090
wsgi-file = /home/ubuntu/cookbook3/Chapter-11/wsgi.py
processes = 3

這裡組態了uWSGI執行在指定的HTTP地址上,並指定了工作程式的數量。

  1. 使用以下命令測試uWSGI:
$ uwsgi --ini uwsgi.ini

現在,您可以存取http://127.0.0.1:9090/來檢視應用程式。

詳細解說:

  • uwsgi.ini 檔案中的 http-socket 指定了 uWSGI 監聽的 HTTP 地址和埠。
  • wsgi-file 指定了包含 WSGI 應用程式物件的檔案路徑。
  • processes 指定了 uWSGI 工作程式的數量。

修改組態以使用uWSGI協定

http-socket修改為socket,以使用uWSGI協定而不是HTTP協定。

[uwsgi]
socket = :9090
wsgi-file = /home/ubuntu/cookbook3/Chapter-11/wsgi.py
processes = 3

圖表說明

以下是 uWSGI 和 Nginx 協同工作的流程圖:

@startuml
skinparam backgroundColor #FEFEFE
skinparam sequenceArrowThickness 2

title Nose2單元測試與佈署效能分析

actor "客戶端" as client
participant "API Gateway" as gateway
participant "認證服務" as auth
participant "業務服務" as service
database "資料庫" as db
queue "訊息佇列" as mq

client -> gateway : HTTP 請求
gateway -> auth : 驗證 Token
auth --> gateway : 認證結果

alt 認證成功
    gateway -> service : 轉發請求
    service -> db : 查詢/更新資料
    db --> service : 回傳結果
    service -> mq : 發送事件
    service --> gateway : 回應資料
    gateway --> client : HTTP 200 OK
else 認證失敗
    gateway --> client : HTTP 401 Unauthorized
end

@enduml

此圖示說明瞭客戶端請求透過Nginx反向代理到uWSGI,然後由uWSGI傳遞給Flask應用程式的流程。