Unittest 之 DDT 的原理解析


引言

  前面的文章介紹了如何在 Python 的 Unittest 框架中來使用 ddt 實現數據驅動的自動化測試。


在了解了 ddt 的使用后,你是否有過如下疑問:

  • ddt 是如何把你的測試數據轉換傳給你的測試用例?

  • 當你的一組數據有多個參數時,ddt 是如何 unpack 的?

  • 當你有多組數據時,ddt 拆分測試用例是如何命名的?

 

主題:今天分享的內容是--探索 ddt 實現數據驅動的秘密。

通過閱讀ddt 源碼,我們不難發現其實 ddt 的實現核心就是@ddt(cls)這個裝飾器,而這個裝飾器的核心代碼是 wrapper這個類函數,下面我直接把 wrapper 的源碼貼上來,大家一起看看:

def wrapper(cls):
    # 先遍歷被裝飾類的name, 和func
    # 對於func,先看被裝飾的是DATA_ATTR還是FILE_ATTR
    for name, func in list(cls.__dict__.items()):
        # 如果被裝飾的是DATA_ATTR
        if hasattr(func, DATA_ATTR):
            #獲取@data提供數據的index和內容並且遍歷它們
            for i, v in enumerate(getattr(func, DATA_ATTR)):
                # 重新生成新的測試函數名,這個函數名會展示在測試報告中
                test_name = mk_test_name(
                    name,
                    getattr(v, "__name__", v),
                    i,
                    fmt_test_name
                )
                test_data_docstring = _get_test_data_docstring(func, v)
                # 如果類函數被@unpack裝飾
                if hasattr(func, UNPACK_ATTR):
                    # 如果提供的數據是tuple或者list
                    if isinstance(v, tuple) or isinstance(v, list):
                        # 則添加一個case到測試類中
                        # list或tuple傳不定數目的值, 用*v即可。
                        add_test(
                            cls,
                            test_name,
                            test_data_docstring,
                            func,
                            *v
                        )
                    else:
                        # unpack dictionary
                        # 添加一個case到測試類中
                        # dict中傳不定數目的值,用**v
                        add_test(
                            cls,
                            test_name,
                            test_data_docstring,
                            func,
                            **v
                        )
                else:
                    # 如不需要unpack,則直接添加一個case到測試類
                    add_test(cls, test_name, test_data_docstring, func, v)
            # 刪除原來的測試類
            delattr(cls, name)
        # 如果被裝飾的是file_data
        elif hasattr(func, FILE_ATTR):
            # 獲取file的名稱
            file_attr = getattr(func, FILE_ATTR)
            # 根據process_file_data解析這個文件
            # 在解析的最后,會調用mk_test_name生成多個測試用例
            process_file_data(cls, name, func, file_attr)
            # 測試用例生成后,會刪除原來的測試用例
            delattr(cls, name)
    return cls

 

 來分析下這段代碼, 對於每一個被 @ddt 裝飾的測試類,ddt 首先去遍歷測試類的自有屬性,從而得出這個測試類有哪些測試方法,這部分主要靠這條語句:

# wrapper源碼第4行
for name, func in list(cls.__dict__.items()):

 

然后,ddt 去判斷所有的 func(即類函數)里,有沒有裝飾器 @data 或者 @file_data,主要靠這兩條語句:

# 被@data裝飾, wrapper源碼第6行
if hasattr(func, DATA_ATTR):
# 被file_data 裝飾,wrapper源碼第47行
elif hasattr(func, FILE_ATTR):

 接着程序會進入兩條分支:被 @data 裝飾,即由 ddt 直接提供數據;被 @file_data 裝飾,即數據由外部文件提供。

 

1.被 @data 裝飾,即由 ddt 直接提供數據
如果數據是直接通過 @data 提供的,那么為每一組數據新生成一個測試用例名稱。

# 在本例中, i, v的第一次循環,值為 
# i:0 v:['Testing', 'Testing']
# wrapper源碼第8行
for i, v in enumerate(getattr(func, DATA_ATTR)):
    test_name = mk_test_name(
        name,
        getattr(v, "__name__", v),
        i,
        fmt_test_name
    )

test_name 生成使用的是函數 mk_test_name。

注意:ddt 在此時實現了把你的測試數據轉換傳給你的測試用例。其實不是通過傳遞,而是通過把測試數據拆分,並且生成新測試用例的方式來達成的。

 

而在函數 mk_test_name 里,ddt 更是把原來的測試函數通過特定的規則,拆分成不同的測試函數。

test_name = mk_test_name(name,getattr(v, "__name__", v),i,fmt_test_name)

mk_test_name 的參數里:

  • name 是原測試函數的名字

  • v 是我們的一組測試數據

  • i 是這組數據的 index

     

fmt_test_name 指定新的 test 函數的名字的格式,這個格式是按照原來測試函數名 index 第一個測試數據_第二個測試數據這樣的格式。

 

例如,我們的測試數據 ['Testing','Testing'] 會被轉換成test_baidu_search_1_['Testing', 'Testing']',但是由於符號 '[' 和 '' 以及 ',' 是不合法的字符,故會被 '_' 替換,故最終新生成的測試用例名是test_baidu_search_1___Testing____Testing__ 這塊的邏輯在函數 mk_test_name 的最后兩行:

# ddt內容函數mk_test_name,test_name處理邏輯如下
test_name = "{0}_{1}_{2}".format(name, index, value)
return re.sub(r'\W|^(?=\d)', '_', test_name)

 

緊接着,ddt 又去查找你的測試類函數,看它有沒有被 @unpack 裝飾。如果有,就意味着我們的測試類函數有多個參數,這個時候就需要把我們的測試數據 unpack,這樣我們的測試類函數的各個參數才能接收到傳入的值。

這樣,ddt 把上一步生成的 test_name 和剛剛 unpack 的值(數據是 list、tuple,還是 dictionary,決定了 unpack 采用 *v 還是 **v),通過 add_test 來新生成一個測試用例,注冊到我們的測試類下面,所有這些動作是在下面這段代碼里完成的。

# wrapper源碼里的18行到43行
if hasattr(func, UNPACK_ATTR):
    if isinstance(v, tuple) or isinstance(v, list):
        add_test(
            cls,
            test_name,
            test_data_docstring,
            func,
            *v
        )
    else:
        # unpack dictionary
        add_test(
            cls,
            test_name,
            test_data_docstring,
            func,
            **v
        )
else:
    add_test(cls, test_name, test_data_docstring, func, v)

 

注意:
  這個時候測試類中是多了測試函數的,多了多少個,要取決於 ddt 提供的測試數據的組數,有幾組就生成幾個測試用例,並且都注冊到原測試類中去;

  unpack 其實就是為了把一個測試用例的多個測試數據全部傳入新生成的測試函數中去,這些測試數據和測試函數的參數一一對應。

 

最后,ddt 會把最初的那個原始測試類方法給刪除(因為原測試函數已經根據各組數據變成了新的測試函數)。

# wrapper源碼45行
delattr(cls, name)

通過這樣的方式,ddt 根據測試數據的組數,通過函數 mk_test_name 生成多組測試用例,並通過 add_test 函數注冊到 unittest的TestSuite 里去。

 

2.被 @file_data 裝飾,即數據由外部文件提供
如果測試函數被 @file_data 裝飾,ddt 則會先獲取 file_data 里的數據文件名稱,然后通過函數 process_file_data 里進行下一步處理。

# wrapper源碼的第49到52行
file_attr = getattr(func, FILE_ATTR)
process_file_data(cls, name, func, file_attr)

 

看起來只有短短的兩行,其實 ddt 在函數 process_file_data 內部做了很多操作。

首先 ddt 會先拿到我們提供的數據文件的絕對地址,並通過后綴名判斷它是 yaml 文件還是 json 文件,然后分別調用 yaml 或者 json 的 load 方法拿到文件里提供的數據。

拿到數據后,最終也是通過 mk_test_name 函數和 add_test 函數,生成多條測試用例,並且注冊到 unittest 的 TestSuite 里去。

 

最后一樣是刪除原來的測試函數:

# wrapper源碼54行
delattr(cls, name)

這就是 ddt 的整個實現邏輯了。

 

總結

  DDT 的源代碼非常經典,代碼行數不多,值得我們深讀。仔細琢磨並研究透 DDT 的源碼,有助於你的測試開發技術提升。建議用單步調試的方式,結合今天分享的內容,邊執行測試代碼邊走讀 DDT 代碼,這樣將更有助於你加深對 DDT 原理的理解。

 

歡迎關注【無量測試之道】公眾號,回復【領取資源】
Python編程學習資源干貨、
Python+Appium框架APP的UI自動化、
Python+Selenium框架Web的UI自動化、
Python+Unittest框架API自動化、

資源和代碼 免費送啦~
文章下方有公眾號二維碼,可直接微信掃一掃關注即可。

備注:我的個人公眾號已正式開通,致力於測試技術的分享,包含:大數據測試、功能測試,測試開發,API接口自動化、測試運維、UI自動化測試等,微信搜索公眾號:“無量測試之道”,或掃描下方二維碼:

 添加關注,讓我們一起共同成長!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM