前言
用 python 做過自動化的小夥伴,大多數都應該使用過 ddt 這個子產品,不可否認 ddt 這個子產品确實挺好用,可以自動根據用例資料,來生成測試用例,能夠很友善的将測試資料和測試用例執行的邏輯進行分離。接下來就帶大家一起自己,手把手撸出一個 ddt。
1、DDT 的實作原理
首先我們來看一下 ddt 的基本使用:
ddt 在使用時非常簡潔,也就是兩個裝飾器,@ddt 這個裝飾器裝飾測試類,@data 這個裝飾器裝飾器用例方法并傳入測試資料。這兩個裝飾器實作的效果就是根據傳入的用例資料自動生成用例。具體是怎麼實作的呢?其實實作的思路也特别的簡單,也就兩個步驟:
整理了一份大廠軟體測試面試寶典pdf第一步:把傳進來的用例資料儲存起來
第二步:周遊用例資料,每周遊一條資料 就動态的給測試類添加一個用例方法。
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()