Python 項目提速技巧:連接複用

???? Python貓” ,一個值得加星標的公衆號

花下貓語:我平時有比較好的文章閱讀/搜索習慣,所以偶爾會發現一些優質但傳播範圍很小的文章,有時候順藤摸瓜,還會發現一些很低調但富有真誠的原創公衆號。今天分享的文章及公衆號就是如此。作者打算寫一個“自動化用例開發過程中的常見技巧”系列,但我覺得這些技巧適用於大多數實戰項目,讀了總會有所啓發。所以,好文一起來學習吧!

劇照 | 《霸王別姬》

來源:質量價值@質量價值公衆號

爲什麼需要連接複用

接口、UI的測試用例中都會有大量的IO操作,比如HTTP、RPC調用、數據庫查詢等,這是典型的IO密集型任務,對自動化效率有追求的測試工程師應該思考一個問題:如何讓用例執行更加地有效率(快)?

拋出的這個問題其實很大,從驗證策略、用例設計、IO優化、用例分發方式等角度都可以講,我不準備在這篇文章裏完整的闡述,只挑出一個點:連接複用

這裏的連接可以存在於以下地方:

  • HTTP連接

  • RPC連接(http、socket都可能)

  • 中間件連接(數據庫、緩存服務等連接,可簡化爲TCP)

  • UI自動化的Appium、Selenium對象(webdriver協議)

連接複用(以TCP爲例)的好處可以大幅度降低TCP三次握手、四次揮手的次數以實現對用例消耗時間的降低,舉一個很簡單的例子:比如一個mysql client的建鏈跟關閉連接各需要10ms,當你存在10000多條用例,並且平均每個用例需要2次mysql查詢操作,那整個用例執行時間可以降低400秒。對於做慣了UI自動化測試的童鞋而言,UI自動化執行時間往往以分鐘、小時爲計量單位,這400秒時間的減少似乎並不明顯。這點我承認,但是對於下沉至接口層的自動化,完全可以相信一個業務場景用例能在一秒內驗證完成,能壓榨出400秒時間就是非常大的優化。

而且我相信,你在每一點上都比別人多想一點多做一點,這些點點滴滴的積累、沉澱就會變成你的絕對優勢。

不經意來了碗雞湯,回到正題:連接複用。

一般操作

對於測試人員而言,要實現『連接複用』最簡單的辦法對高度抽象的應用對象的複用,你不用過多去考慮實現層面的細節,比如連接池等。比如我之前在接口封裝的基石:requests.Session介紹過通過requests.Session來實現HTTP連接的複用,當你所有的HTTP接口調用都基於同一個requests.Session來調用的話,那其實就實現了全局的『HTTP連接複用』能力。

HTTP調用是有狀態的,所以是否應該使用同一個requests.Session來調用,要視實際情況來判斷,本文不多展開。

下文我以mysql的連接複用(使用pymysql庫)來作介紹。

先看一個簡單的例子:

import pymysql

conn = pymysql.Connect(host="your_host", user="root", password="your_password", database="your_db")
with conn.cursor() as curosr:
    curosr.execute("select * from user limit 1")
    ret = curosr.fetchone()
conn.close()

當你在測試用例裏需要進行SQL查詢時,可以copy上面的代碼去做相關的操作,一個兩個用例還好,但是用例成千上百時,我就算不講『連接複用』概念,我也相信你也覺得這樣的代碼很臃腫,需要優化。

大部分測試人員會使用這個辦法:在測試啓動時,連接一次數據庫(pymysql.Connect),然後把返回的pymysql.Connection作爲一個全局對象供其他用例使用,這就是連接複用的思路。

現實問題

但往往我們實際的應用場景可能更加豐富、複雜,比如:

  1. 需要訪問同一數據庫實例的不同database

  2. 需要不同賬號訪問同一數據庫實例(權限問題)

  3. 需要訪問不同數據庫實例

第一種情況還好,訪問不同database可以共用一個連接,只需要使用use <db>來切換。另外兩種呢?如果按照上面提到的思路也有辦法:在測試啓動時,建立不同賬號建立對不同數據庫實例的連接,都是作爲『全局的數據連接』,而在使用時(用例邏輯層)去挑選適合你當前用例的連接對象。

按照上面辦法的需要注意:因爲需要用例設計者人工去選擇合適的pymysql.Connection對象,當對象較多時,用例設計者很可能選錯,導致用例失敗。

我這裏更推薦另外種做法——懶加載,你不需要測試一開始就建立所有的mysql連接,而是在你的用例裏需要去查詢數據庫時,顯式地傳入連接信息(地址、用戶名等)去建立連接,這樣就可以避免使用了錯誤的數據庫連接信息了,如:

def test_user():
    conn = pymysql.Connect(host="host1", user="root", password="pwd", database="ddd")
    with conn.cursor() as curosr:
        curosr.execute("select * from user limit 1")
        ret = curosr.fetchone()
    assert ret


def test_tag():
    conn = pymysql.Connect(host="host1", user="root", password="pwd", database="ddd")
    with conn.cursor() as curosr:
        curosr.execute("select * from tag")
        ret = curosr.fetchone()
    assert ret

但這樣就帶出來問題了:明明要講連接複用,爲什麼還要在每一個用例裏去初始化數據庫連接?

單例模式

上面一大段其實就爲了引出設計模式裏非常重要的一種——單例模式:單例模式確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。

也就是說會存在以下的邏輯:

單例模式的實現辦法有很多種,比如:

def singleton(cls):
    instances = dict()

    @functools.wraps(cls)
    def _singleton(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return _singleton


@singleton
class MySQLConnectionProxy:

    def __init__(self, *args, **kwargs):
        self._conn = pymysql.Connect(*args, **kwargs)

    def __getattr__(self, item):
        return getattr(self._conn, item)

上面的例子還用到了代理模式,之後會有更詳細的講解

對應的測試用例可以改成這種方式:

def test_user():
    conn = MySQLConnectionProxy(host="host1", user="root", password="pwd", database="ddd")
    with conn.cursor() as curosr:
        curosr.execute("select * from user limit 1")
        ret = curosr.fetchone()
    assert ret

再結合我們上一講的如何讓用例支持多環境?,我們可以把數據庫連接信息抽象出來,從而變成:

def test_user():
    conn = MySQLConnectionProxy(**entrypoints.mysql)
    with conn.cursor() as curosr:
        curosr.execute("select * from user limit 1")
        ret = curosr.fetchone()
    assert ret

單例模式的變種

但上面單例模式的代碼其實並沒有解決多用戶、多數據庫連接的問題,該怎麼解決呢?思路稍微變通下不難發現:應該只對使用相同連接信息的調用使用單例模式

這話說點有點抽象,具象一點就是:當數據庫host、端口、用戶名、密碼相同時,返回一個已建立的pymysql.Connection,也可以用下圖來加深理解:

所以可以進一步優化上面的代碼:

def singleton_mysql_instance(cls):
    instances = dict()

    @functools.wraps(cls)
    def _singleton(*args, **kwargs):
        conn_params = (kwargs.get("host"), kwargs.get("port"), kwargs.get("user"), kwargs.get("password"))
        p = hash(conn_params)
        if p not in instances:
            instances[p] = cls(*args, **kwargs)
        return instances[p]
    return _singleton

爲了方便理解,我簡化了實現,也儘量少去使用inspect、magic method這些能力

連接複用的注意點

單例模式下全局只維護了一個實例,這個時候一定要慎重考慮一個問題:如果該對象被執行了析構函數或者像mysql的連接被關閉了(不管是主動還是被動),如何能夠發現或者重新構造?

另外還有一個問題,全局只維護了一個實例,在多線程模型下,是否能夠保證對它的操作是線程安全的?(thread safety)

受限於篇幅,這兩個問題這邊不展開討論了,感興趣的可以留言一起討論。

優質文章,推薦閱讀:

Python對象的空間邊界:獨善其身與開放包容

Python 工匠:讓函數返回結果的技巧

將安卓手機打造成 Python 全棧開發利器

聽說你是程序員,請問你知道龍書、虎書、鯨書、魔法書、犀牛書...指的是哪些書麼?

感謝創作者的好文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章