原始碼教學:教你 30 行程式碼實現 ddt 模組
前言
用 python 做過自動化的小夥伴,大多數都應該使用過 ddt 這個模組,不可否認 ddt 這個模組確實挺好用,可以自動根據用例資料,來生成測試用例,能夠很方便的將測試資料和測試用例執行的邏輯進行分離。接下來就帶大家一起自己,手把手擼出一個 ddt。
1、DDT 的實現原理
首先我們來看一下 ddt 的基本使用:
ddt 在使用時非常簡潔,也就是兩個裝飾器,@ddt 這個裝飾器裝飾測試類,@data 這個裝飾器裝飾器用例方法並傳入測試資料。這兩個裝飾器實現的效果就是根據傳入的用例資料自動生成用例。具體是怎麼實現的呢?其實實現的思路也特別的簡單,也就兩個步驟:
第一步:把傳進來的用例資料儲存起來
第二步:遍歷用例資料,每遍歷一條資料 就動態的給測試類新增一個用例方法。
ddt 中的兩個裝飾器其實實現的就是這麼兩個步驟:
@data:做的是第一步將傳入測試資料儲存起來;
@ddt 做的是第二步,遍歷用例資料,給測試類動態新增用例方法。
2、data 裝飾器的實現
前面我們說到 data 這個裝飾器,做的事情是將用例資料儲存起來。那麼如何儲存呢?其實最簡單的方式就是 儲存被裝飾的這個用例方法的屬性。接下來我們來具體實現:
先看一個 ddt 使用的案例
@ddt
class TestLogin(unittest.TestCase):
@data(11,22)
def test_login(self, item):
pass
瞭解過裝飾器裝飾器原理的小夥伴,應該都知道上面@data(11,22) 這行程式碼執行的效果等同於
test_login = data(11,22)(test_login)
接下來我們來分析一下上面這行程式碼,首先是呼叫 data 這個裝飾器函式,把用例資料 11,22 當成引數傳入進去,然後返回一個可呼叫物件(函式),再次呼叫返回的函式並把用例方法傳入進去。明確了呼叫的流程那麼我們就可以結合之前的需求去定義 data 這個裝飾器函數了。具體實現如下:
def data(*args):
def wrapper(func):
setattr(func, "PARAMS", args)
return func
return wrapper
程式碼解讀:
前面的案例在使用 data 時,執行的 test_login = data(11,22)(test_login) 先呼叫 data 傳入的 11,22 通過不定長引數 args 接收,然後返回巢狀的函式 wrapper 然後呼叫返回的 wrapper 函式,傳入被裝飾的 test_login 方法 在 wrapper 函式中我們把用例資料儲存為 test_login 這個方法的 PARAMS 屬性,再把 test_login 返回 到此為止,data 這個裝飾器我們就實現用例資料的儲存
3、ddt 裝飾器的實現
通過 data 這個裝飾器我們實現了用例資料儲存之後,我們接下來實現 ddt 這個裝飾器,根據用例資料生成測試用例。前面的案例 @ddt 裝飾測試類的時候,實際上執行的效果等同於下面的程式碼
TestLogin = ddt(TestLogin)
這行程式碼就是把被裝飾器的類傳入到 ddt 這個裝飾器函式中,再把返回值賦值給 TestLogin。之前我們分析的時候說了 ddt 這個裝飾器做的事情是遍歷用例資料,動態的給測試類新增用例方法,接下來我們就來實現 ddt 這個裝飾器內部的邏輯。
def ddt(cls):
for name, func in list(cls.__dict__.items()):
if hasattr(func, "PARAMS"):
for index, case_data in enumerate(getattr(func, "PARAMS")):
new_test_name ="{}_{}".format(name,index)
setattr(cls, new_test_name, func)
else:
delattr(cls, name)
return cls
程式碼解讀:
ddt 函式內部邏輯說明: 1、呼叫 ddt 這個函式時會把測試類當成引數傳入進來, 2、然後通過 cls.__dict__ 獲取測試的所有屬性和方法,進行遍歷 3、判斷變量出來的屬性或方法 有沒有 PARAMS 這個屬性, 4、如果有,則說明這個方法用 data 裝飾器裝飾過並傳入了用例資料。 5、通過 getattr(func, "PARAMS")獲取所有的用例資料,進行遍歷。 6、每遍歷出來一組用例資料,生產一個用例方法名, 再動態的給測試類新增一個用例方法。 7、遍歷完所有用例資料之後,刪除測試類原來定義的測試方法 8、最後返回測試類
當目前為止 ddt 和 data 這兩個裝飾器函式的基本功能實現了,可以自動根據用例資料生成測試用例了,接下來我們寫個測試類來檢查一下
# 定義裝飾器函式data
def data(*args):
def wrapper(func):
setattr(func, "PARAMS", args)
return func
return wrapper
# 定義裝飾器函式ddt
def ddt(cls):
for name, func in list(cls.__dict__.items()):
if hasattr(func, "PARAMS"):
for index, case_data in enumerate(getattr(func, "PARAMS")):
new_test_name = "{}_{}".format(name, index)
setattr(cls, new_test_name, func)
else:
delattr(cls, name)
return cls
import unittest
# 編寫測試類
@ddt
class TestDome(unittest.TestCase):
@data(11, 22, 33, 44)
def test_demo(self):
pass
執行上述用例,我們就會發現執行了四條用例,根據用例資料生成用例的功能就已經實現了。
4、解決用例引數傳遞的問題
雖然上面基本的功能已經實現了,但是還存在一個問題。用例的資料沒有傳遞到用例方法中。那麼用例資料傳遞怎麼實現了,我們可以通過一個閉包函式對用例方法進行修,從而實現在呼叫用例方法的時候,把用例測試當成引數傳遞進去。修改原有用例方法的函式程式碼如下
from functools import wraps
def update_test_func(test_func,case_data):
@wraps(test_func)
def wrapper(self):
return test_func(self, case_data)
return wrapper
程式碼解讀:
上面我們定義了一個叫做 update_test_func 的閉包函式 閉包函式接收兩個引數:test_func(接收用例方法),case_data(接收用例資料) 閉包函式返回一個巢狀函式,巢狀函式內部呼叫原來的用例方法,並傳入測試資料 巢狀函式在定義時,使用了 functools 模組中的裝飾器 wraps 來裝飾,它可以讓 wrapper 這個巢狀函式具有 test_func 這個用例函式的相關屬性。
下面我們回到前面寫的 ddt 這個函式中,在給測試類新增用例之前,呼叫 update_test_func 方法對用例方法進行修改。
def ddt(cls):
for name, func in list(cls.__dict__.items()):
if hasattr(func, "PARAMS"):
for index, case_data in enumerate(getattr(func, "PARAMS")):
# 生成一個用例方法名
new_test_name = "{}_{}".format(name, index)
# 修改原有的測試方法,設定用例資料為測試方法的引數
test_func = update_test_func(func,case_data)
setattr(cls, new_test_name, test_func)
else:
delattr(cls, name)
return cls
通過加上這一步之後,我們在測試類中 動態給測試類新增的測試方法,其實指向的全部是 update_test_func 裡面定義的 wrapper 函式,在執行測試用的時候實際上也是執行的 wrapper 函式,而在 wrapper 函式內部,我們呼叫了原來定義的測試方法,並將用例資料傳入了進去,到此為止 ddt 的功能我們就完全實現了。
下面是一個完整的案例,大家可以複製過去執行,也可以自己去寫一遍,還可以根據自己的一些需求進行自定義的擴充套件。
完整案例
from functools import wraps
import unittest
# --------ddt的實現--------
def data(*args):
def wrapper(func):
setattr(func, "PARAMS", args)
return func
return wrapper
def update_test_func(test_func, case_data):
@wraps(test_func)
def wrapper(self):
return test_func(self, case_data)
return wrapper
def ddt(cls):
for name, func in list(cls.__dict__.items()):
if hasattr(func, "PARAMS"):
for index, case_data in enumerate(getattr(func, "PARAMS")):
# 生成一個用例方法名
new_test_name = "{}_{}".format(name, index)
# 修改原有的測試方法,設定用例資料為測試方法的引數
test_func = update_test_func(func, case_data)
setattr(cls, new_test_name, test_func)
else:
delattr(cls, name)
return cls
# --------測試用例編寫--------
@ddt
class TestDome(unittest.TestCase):
@data(11, 22, 33, 44)
def test_demo(self, data):
assert data < 40
#---------用例執行-----------
unittest.main()