什么是數(shù)據(jù)驅(qū)動?
數(shù)據(jù)驅(qū)動,指在自動化測試中處理測試數(shù)據(jù)的方式。
通常測試數(shù)據(jù)與功能函數(shù)分離,存儲在功能函數(shù)的外部位置。在自動化測試運行時,數(shù)據(jù)驅(qū)動框架會讀取數(shù)據(jù)源中的數(shù)據(jù),把數(shù)據(jù)作為參數(shù)傳遞到功能函數(shù)中,并會根據(jù)數(shù)據(jù)的條數(shù)多次運行同一個功能函數(shù)。
數(shù)據(jù)驅(qū)動的數(shù)據(jù)源可以是函數(shù)外的數(shù)據(jù)集合、CSV 文件、Excel 表格、TXT 文件,以及數(shù)據(jù)庫等。
數(shù)據(jù)驅(qū)動的好處有哪些?
1.數(shù)據(jù)驅(qū)動能夠減少重復(fù)代碼
下面我們通過一個例子來看下數(shù)據(jù)驅(qū)動是如何減少代碼重復(fù)的。
# 偽代碼,僅供演示
def book_order(user, product, num):
? ? # 你的函數(shù)邏輯
? ? pass
# 如果沒有數(shù)據(jù)驅(qū)動,你的代碼是這樣的:
book_order('張三', '前端自動化測試框架Cypress從入門到精通', 1)
book_order('李四', '測試開發(fā)入門與實戰(zhàn)', 1)
book_order('王五', '[測試開發(fā)入門與實戰(zhàn),前端自動化測試框架Cypress從入門到精通]', 50)
沒有數(shù)據(jù)驅(qū)動時,并且同一個功能函數(shù)存在多個測試數(shù)據(jù),你只能多次調(diào)用這個功能函數(shù);另外一旦某一個測試數(shù)據(jù)有更改/刪除,你需要在函數(shù)調(diào)用里去更改相應(yīng)的測試數(shù)據(jù),非常不方便。
但有了測試驅(qū)動時,你的代碼可能是下面這個樣子。
# data_book指向一個文件,這個文件里存儲有你所有的測數(shù)據(jù)。
data_book = './tests/data/testdata.csv'
# dataDrivenDecorator是你實現(xiàn)數(shù)據(jù)驅(qū)動的裝飾器
@dataDrivenDecorator(data_book)
def book_order(user, product, num):
? ? # 你的函數(shù)邏輯
? ? pass
這種情況下, 你無須進行多次調(diào)用,而且當(dāng)你的測試數(shù)據(jù)發(fā)生改變時, 你僅需要更改數(shù)據(jù)源文件的數(shù)據(jù)就可以了。
2.數(shù)據(jù)所屬的測試用例失敗,不會影響到其他測試數(shù)據(jù)對應(yīng)的測試用例
同樣舉一個例子,沒有數(shù)據(jù)驅(qū)動之前,假設(shè)我們有這樣的一個函數(shù):
test_data = [0, 1, 0, 1]
def test_without_data_driven(records):
? ? for x in records:
? ? ? ? assert x > 0
test_without_data_driven(test_data)
當(dāng)你運行這段代碼時,因為 test_data 的第一個值是 0, 它不大于 0。所以斷言失敗,所有 test_data 這個函數(shù) 0 后面的測試數(shù)據(jù)都沒有執(zhí)行。
如果有了數(shù)據(jù)驅(qū)動,則數(shù)據(jù)驅(qū)動會把這一個測試按照測試數(shù)據(jù)分解成多個測試,所有第一個測試數(shù)據(jù)失敗不也會影響到后面的測試結(jié)果。
了解了數(shù)據(jù)驅(qū)動的眾多好處,我們來看下在 Python 中,應(yīng)用比較廣泛的兩個數(shù)據(jù)驅(qū)動的框架。一個是 DDT(Data-Driven Tests),它是 unittest 框架中實現(xiàn)數(shù)據(jù)驅(qū)動的不二之選;另外一個是 parameterized,它是 pytest 能夠?qū)崿F(xiàn)數(shù)據(jù)驅(qū)動的秘訣。
DDT 含有哪些裝飾器
1.一個類裝飾器
ddt 這個類裝飾器必須裝飾在 TestCase 的子類上,TestCase 是 unittest 框架中的一個基類,它實現(xiàn)了 Test Runner 驅(qū)動測試運行所需的接口(interface)。
2.兩個方法裝飾器
分別是 data 和 file_data。其中 data 裝飾器,直接提供測試數(shù)據(jù);file_data 裝飾器則從 JSON 或 YAML 文件加載測試數(shù)據(jù)。
DDT 的使用步驟如下:
1、使用 @ddt 裝飾你的測試類;
2、使用 @data 或者 @file_data 裝飾你需要數(shù)據(jù)驅(qū)動的測試方法;
3、如一組測試數(shù)據(jù)有多個參數(shù),則需 unpack,使用 @unpack 裝飾你的測試方法。
DDT 使用詳解
先安裝 DDT:
pip install ddt
然后我以 lagouAPITest 框架里,tests 文件夾下的 test_baidu.py 這個文件為例,來講解下 ddt 的使用。
1.ddt 直接提供數(shù)據(jù)
# coding=utf-8
from ddt import ddt, data, file_data, unpack
from selenium import webdriver
import unittest
import time
@ddt????????# ddt一定是裝飾在TestCase的子類上
class Baidu(unittest.TestCase):
? ? def setUp(self):
? ? ? ? self.driver = webdriver.Chrome()
? ? ? ? self.driver.implicitly_wait(30)
? ? ? ? self.base_url = "http://www.baidu.com/"
? ? @data(['iTesting', 'iTesting'], ['helloqa.com', 'iTesting'])????????????? ? # data表示data是直接提供的。
? ? @unpack????????? ????? # unpack表示,對于每一組數(shù)據(jù),如果它的值是list或者tuple,那么就分拆成獨立的參數(shù)。
? ? def test_baidu_search(self, search_string, expect_string):
? ? ? ? driver = self.driver
? ? ? ? driver.get(self.base_url + "/")
? ? ? ? driver.find_element_by_id("kw").send_keys(search_string)
? ? ? ? driver.find_element_by_id("su").click()
? ? ? ? time.sleep(2)
? ? ? ? search_results = driver.find_element_by_xpath('//*[@id="1"]/h3/a').get_attribute('innerHTML')
? ? ? ? print(search_results)
? ? ? ? self.assertEqual(expect_string in search_results, True)
? ? def tearDown(self):
? ? ? ? self.driver.quit()
if __name__ == "__main__":
? ? unittest.main(verbosity=2)
在這個例子中,我直接使用了 @data 裝飾器。在這個裝飾器中,我給出了測試的 2 組數(shù)據(jù),分別是 ['iTesting', 'iTesting'] 和 ['helloqa.com', 'iTesting'];然后我使用 @unpack 裝飾器把每一組數(shù)據(jù)的數(shù)據(jù) unpack 成一個個的參數(shù)傳給我的函數(shù) test_baidu_search。
直接運行這個文件,結(jié)果如下:

你注意下,雖然我們只有一個測試用例 test_baidu_search。但在生成的測試報告里,顯示“Run 2 tests in 17.172s”,也就是 test_baidu_search 運行了 2 次,這就是 DDT 在起作用。
這是多組參數(shù),每組多個數(shù)據(jù)的情況,如果每組僅有一個數(shù)據(jù)呢?你僅需要更改如下:
# 如僅有一個參數(shù),那么直接在data里寫參數(shù)就好。
# 僅有一個參數(shù)的情況下,無須再用@unpack裝飾測試方法。
@data('data1', 'data2')
2.ddt 使用函數(shù)提供數(shù)據(jù)
ddt 直接提供數(shù)據(jù),除去上述的直接把數(shù)據(jù)寫在 @data() 的參數(shù)中外,還有一個情況,即數(shù)據(jù)先從函數(shù)獲取,然后再寫入 @data() 的參數(shù)中。
# coding=utf-8
from ddt import ddt, data, file_data, unpack
from selenium import webdriver
import unittest
import time
def get_test_data():
? ? # 這里寫你獲取測試數(shù)據(jù)的業(yè)務(wù)邏輯。
? ? # 獲取到后,把數(shù)據(jù)返回即可。
? ? # 注意,如果多組數(shù)據(jù),需要返回類似([數(shù)據(jù)1-參數(shù)1, 數(shù)據(jù)1-參數(shù)2],[數(shù)據(jù)2-參數(shù)1, 數(shù)據(jù)2-參數(shù)2])這樣的格式,方便ddt.data()解析
? ? results = ['iTesting', 'iTesting'], ['helloqa.com', 'iTesting']
? ? return results
@ddt
class Baidu(unittest.TestCase):
? ? def setUp(self):
? ? ? ? self.driver = webdriver.Chrome()
? ? ? ? self.driver.implicitly_wait(30)
? ? ? ? self.base_url = "http://www.baidu.com/"
? ? # data表示data是直接提供的。注意data里的參數(shù)我寫了函數(shù)get_test_data()的返回值,并且以*為前綴,代表返回的是可變參數(shù)。
? ? @data(*get_test_data())
? ? @unpack
? ? def test_baidu_search(self, search_string, expect_string):
? ? ? ? driver = self.driver
? ? ? ? driver.get(self.base_url + "/")
? ? ? ? driver.find_element_by_id("kw").send_keys(search_string)
? ? ? ? driver.find_element_by_id("su").click()
? ? ? ? time.sleep(2)
? ? ? ? search_results = driver.find_element_by_xpath('//*[@id="1"]/h3/a').get_attribute('innerHTML')
? ? ? ? print(search_results)
? ? ? ? self.assertEqual(expect_string in search_results, True)
? ? def tearDown(self):
? ? ? ? self.driver.quit()
if __name__ == "__main__":
? ? unittest.main(verbosity=2)
在本例中,我創(chuàng)建了一個函數(shù) get_test_data() 用于獲取我的測試數(shù)據(jù)。這個函數(shù)可以帶參數(shù),也可以不帶參數(shù),具體需要根據(jù)你的業(yè)務(wù)邏輯來。
注意:get_test_data() 的返回值,一定需要遵守 ddt.data() 可接受的數(shù)據(jù)格式。即:
一組數(shù)據(jù),每個數(shù)據(jù)為單個的值;多組數(shù)據(jù),每組數(shù)據(jù)為一個列表或者一個字典。
3.ddt 使用文件提供數(shù)據(jù) — JSON 和 YAML
除了使用 @ddt 直接提供數(shù)據(jù),DDT 還支持通過文件加載數(shù)據(jù)。
不過默認(rèn)只支持兩種格式 YAML 和 JSON,只有以“.yml” 或者“.yaml” 結(jié)尾的會被認(rèn)作 YAML 文件,其他格式都將被認(rèn)為是 JSON 文件。
使用 JSON 文件
如果把上述用例改成使用 JSON 文件,則我們的用例看起來是這樣的:
|--lagouAPITest
? ? |-- .....
? ? |--tests
? ? ? ? |--test_baidu.py
? ? ? ? |--test_baidu.json
? ? ? ? |--__init__.py
首先,我們創(chuàng)建一個跟 test_baidu.py 同名的文件 test_baidu.json,內(nèi)容如下:
{ "case1": {
? "search_string": "itesting",
? "expect_string": "iTesting"
? },
? "case2": {
? "search_string": "itesting",
? "expect_string": "iTesting"
? }
}
然后更新 test_baidu.py,更新后的代碼如下所示:
# -*- coding: utf-8 -*-
from ddt import ddt, data, file_data, unpack
from selenium import webdriver
import unittest
import time
@ddt
class Baidu(unittest.TestCase):
? ? def setUp(self):
? ? ? ? self.driver = webdriver.Chrome()
? ? ? ? self.driver.implicitly_wait(30)
? ? ? ? self.base_url = "http://www.baidu.com/"
? ? # 此處測試數(shù)據(jù)從文件讀取,使用@file_data裝飾器
? ? # 文件路徑是相對于Baidu這個測試類的相對路徑
? ? # 使用外部文件方式Load數(shù)據(jù)無須使用unpack
? ? @file_data('test_baidu.json')
? ? def test_baidu_search(self, search_string, expect_string):
? ? ? ? driver = self.driver
? ? ? ? driver.get(self.base_url + "/")
? ? ? ? driver.find_element_by_id("kw").send_keys(search_string)
? ? ? ? driver.find_element_by_id("su").click()
? ? ? ? time.sleep(2)
? ? ? ? search_results = driver.find_element_by_xpath('//*[@id="1"]/h3/a').get_attribute('innerHTML')
? ? ? ? print(search_results)
? ? ? ? self.assertEqual(expect_string in search_results, True)
? ? def tearDown(self):
? ? ? ? self.driver.quit()
if __name__ == "__main__":
? ? unittest.main(verbosity=2)
可以看到,使用 @file_data 這個裝飾器,與使用 @data 的裝飾器有一點不同:
(1)@file_data 這個裝飾器里,文件的路徑是相對于這個測試類本身來說的。在本例中為 Baidu 這個測試類所處的文件的相對位置;
(2)使用 @file_data 無須使用 unpack,即使同一組數(shù)據(jù)的參數(shù)有多個。
使用 YAML 文件:
如果想在 python 中使用 yaml 文件,則需要安裝 PyYAML。
pip install pyyaml
安裝好后,我們在test_baidu.json的同級目錄下,創(chuàng)建一個文件test_baidu.yaml,內(nèi)容如下:
"case1":
? "search_string": "itesting"
? "expect_string": "iTesting"
"case2":
? "search_string": "itesting"
? "expect_string": "iTesting"
然后,我們更改 test_baidu.py,更改后的內(nèi)容如下:
# -*- coding: utf-8 -*-
from ddt import ddt, data, file_data, unpack
from selenium import webdriver
import unittest
import time
# 使用yaml文件前先嘗試導(dǎo)入,導(dǎo)入失敗則將skip使用yaml數(shù)據(jù)驅(qū)動的測試用例
try:
? ? import yaml
except ImportError:
? ? have_yaml_support = False
else:
? ? have_yaml_support = True
needs_yaml = unittest.skipUnless(
? ? have_yaml_support, "Need YAML to run this test"
)
@ddt
class Baidu(unittest.TestCase):
? ? def setUp(self):
? ? ? ? self.driver = webdriver.Chrome()
? ? ? ? self.driver.implicitly_wait(30)
? ? ? ? self.base_url = "http://www.baidu.com/"
? ? # 使用yaml文件必須使用@needs_yaml裝飾
? ? @needs_yaml
? ? @file_data('test_baidu.yaml')
? ? def test_baidu_search(self, search_string, expect_string):
? ? ? ? driver = self.driver
? ? ? ? driver.get(self.base_url + "/")
? ? ? ? driver.find_element_by_id("kw").send_keys(search_string)
? ? ? ? driver.find_element_by_id("su").click()
? ? ? ? time.sleep(2)
? ? ? ? search_results = driver.find_element_by_xpath('//*[@id="1"]/h3/a').get_attribute('innerHTML')
? ? ? ? print(search_results)
? ? ? ? self.assertEqual(expect_string in search_results, True)
? ? def tearDown(self):
? ? ? ? self.driver.quit()
if __name__ == "__main__":
? ? unittest.main(verbosity=2)
你可以看到,與使用 JSON 文件不同, 使用 YAML 文件必須要先安裝 PyYaml。然后為了防止 yaml 導(dǎo)入失敗,我定義了 needs_yaml 這個裝飾器,用來給我的程序加個安全判斷。如果導(dǎo)入失敗,則所有以 needs_yaml 裝飾的測試用例將不會執(zhí)行。
4.ddt 使用文件提供數(shù)據(jù) — 其他格式數(shù)據(jù)文件
因為 ddt 默認(rèn)只支持 JSON 和 YAML 格式的數(shù)據(jù)。但是我想使用其他數(shù)據(jù)格式怎么辦?
常用的方式有如下兩種:
先讀取其他格式的文件(例如 Excel 格式),然后創(chuàng)建 ddt 支持的 JSON 或者 YAML 文件,最后把獲取到的數(shù)據(jù)寫入這個文件,再使用 @file_data() 即可;
創(chuàng)建一個函數(shù),在函數(shù)中讀取其他格式的文件并獲取數(shù)據(jù),將數(shù)據(jù)直接返回為 @ddt.data() 支持的格式調(diào)用即可。
DDT 的原理解析
了解了 ddt 的使用,不知你有沒有想過如下問題:
1、ddt 是如何把你的測試數(shù)據(jù)轉(zhuǎn)給你的測試用例的?
2、當(dāng)你的一組數(shù)據(jù)有多個參數(shù)時,ddt 是如何 unpack 的?
3、當(dāng)你有多組數(shù)據(jù)時,ddt 拆分測試用例是如何命名的?
下面我們就來一一揭曉 ddt 實現(xiàn)數(shù)據(jù)驅(qū)動的秘密。
其實 ddt 的實現(xiàn)核心就是**@ddt(cls)這個裝飾器,而這個裝飾器的核心代碼是 wrapper**這個內(nèi)函數(shù),下面我直接把 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提供數(shù)據(jù)的index和內(nèi)容并且遍歷它們
? ? ? ? ? ? for i, v in enumerate(getattr(func, DATA_ATTR)):
? ? ? ? ? ? ? ? # 重新生成新的測試函數(shù)名,這個函數(shù)名會展示在測試報告中
? ? ? ? ? ? ? ? test_name = mk_test_name(
? ? ? ? ? ? ? ? ? ? name,
? ? ? ? ? ? ? ? ? ? getattr(v, "__name__", v),
? ? ? ? ? ? ? ? ? ? i,
? ? ? ? ? ? ? ? ? ? fmt_test_name
? ? ? ? ? ? ? ? )
? ? ? ? ? ? ? ? test_data_docstring = _get_test_data_docstring(func, v)
? ? ? ? ? ? ? ? # 如果類函數(shù)被@unpack裝飾
? ? ? ? ? ? ? ? if hasattr(func, UNPACK_ATTR):
? ? ? ? ? ? ? ? ? ? # 如果提供的數(shù)據(jù)是tuple或者list
? ? ? ? ? ? ? ? ? ? if isinstance(v, tuple) or isinstance(v, list):
? ? ? ? ? ? ? ? ? ? ? ? # 則添加一個case到測試類中
? ? ? ? ? ? ? ? ? ? ? ? # list或tuple傳不定數(shù)目的值, 用*v即可。
? ? ? ? ? ? ? ? ? ? ? ? add_test(
? ? ? ? ? ? ? ? ? ? ? ? ? ? cls,
? ? ? ? ? ? ? ? ? ? ? ? ? ? test_name,
? ? ? ? ? ? ? ? ? ? ? ? ? ? test_data_docstring,
? ? ? ? ? ? ? ? ? ? ? ? ? ? func,
? ? ? ? ? ? ? ? ? ? ? ? ? ? *v
? ? ? ? ? ? ? ? ? ? ? ? )
? ? ? ? ? ? ? ? ? ? else:
? ? ? ? ? ? ? ? ? ? ? ? # unpack dictionary
? ? ? ? ? ? ? ? ? ? ? ? # 添加一個case到測試類中
? ? ? ? ? ? ? ? ? ? ? ? # dict中傳不定數(shù)目的值,用**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)
? ? ? ? ? ? # 根據(jù)process_file_data解析這個文件
? ? ? ? ? ? # 在解析的最后,會調(diào)用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(即類函數(shù))里,有沒有裝飾器 @data 或者 @file_data,主要靠這兩條語句:
# 被@data裝飾, wrapper源碼第6行
if hasattr(func, DATA_ATTR):
# 被file_data 裝飾,wrapper源碼第47行
elif hasattr(func, FILE_ATTR):
接著程序會進入兩條分支:被 @data 裝飾,即由 ddt 直接提供數(shù)據(jù);被 @file_data 裝飾,即數(shù)據(jù)由外部文件提供。
1.被 @data 裝飾,即由 ddt 直接提供數(shù)據(jù)
如果數(shù)據(jù)是直接通過 @data 提供的,那么為每一組數(shù)據(jù)新生成一個測試用例名稱。
# 在本例中, i, v的第一次循環(huán),值為
# i:0 v:['iTesting', 'iTesting']
# 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 生成使用的是函數(shù) mk_test_name。
注意:ddt 在此時實現(xiàn)了把你的測試數(shù)據(jù)轉(zhuǎn)給你的測試用例。 其實不是通過傳遞,而是通過把測試數(shù)據(jù)拆分,并且生成新測試用例的方式來達(dá)成的。
而在函數(shù) mk_test_name 里,ddt 更是把原來的測試函數(shù)通過特定的規(guī)則,拆分成不同的測試函數(shù)。
test_name = mk_test_name(name,getattr(v, "__name__", v),i,fmt_test_name)
mk_test_name 的參數(shù)里:
1、name 是原測試函數(shù)的名字
2、v 是、我們的一組測試數(shù)據(jù)
3、i 是這組數(shù)據(jù)的 index
4、fmt_test_name 指定新的 test 函數(shù)的名字的格式,這個格式是按照原來測試函數(shù)名 index 第一個測試數(shù)據(jù)_第二個測試數(shù)據(jù)這樣的格式。
例如,我們的測試數(shù)據(jù) ['iTesting','iTesting'] 會被轉(zhuǎn)換成test_baidu_search_1_['iTesting', 'iTesting']',但是由于符號 '[' 和 '' 以及 ',' 是不合法的字符,故會被 '_' 替換,故最終新生成的測試用例名是test_baidu_search_1___iTesting____iTesting__ 這塊的邏輯在函數(shù) mk_test_name 的最后兩行:
# ddt內(nèi)容函數(shù)mk_test_name,test_name處理邏輯如下
test_name = "{0}_{1}_{2}".format(name, index, value)
return re.sub(r'\W|^(?=\d)', '_', test_name)
緊接著,ddt 又去查找你的測試類函數(shù),看它有沒有被 @unpack 裝飾。如果有,就意味著我們的測試類函數(shù)有多個參數(shù),這個時候就需要把我們的測試數(shù)據(jù) unpack,這樣我們的測試類函數(shù)的各個參數(shù)才能接收到傳入的值。
這樣,ddt 把上一步生成的 test_name 和剛剛 unpack 的值(數(shù)據(jù)是 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)
注意:
這個時候測試類中是多了測試函數(shù)的,多了多少個,要取決于 ddt 提供的測試數(shù)據(jù)的組數(shù),有幾組就生成幾個測試用例,并且都注冊到原測試類中去;
unpack 其實就是為了把一個測試用例的多個測試數(shù)據(jù)全部傳入新生成的測試函數(shù)中去,這些測試數(shù)據(jù)和測試函數(shù)的參數(shù)一一對應(yīng)。
最后,ddt 會把最初的那個原始測試類方法給刪除(因為原測試函數(shù)已經(jīng)根據(jù)各組數(shù)據(jù)變成了新的測試函數(shù))。
# wrapper源碼45行
delattr(cls, name)
通過這樣的方式,ddt 根據(jù)測試數(shù)據(jù)的組數(shù),通過函數(shù) mk_test_name 生成多組測試用例,并通過 add_test 函數(shù)注冊到 unittest的TestSuite 里去。
2.被 @file_data 裝飾,即數(shù)據(jù)由外部文件提供
如果測試函數(shù)被 @file_data 裝飾,ddt 則會先獲取 file_data 里的數(shù)據(jù)文件名稱,然后通過函數(shù) process_file_data 里進行下一步處理。
# wrapper源碼的第49到52行
file_attr = getattr(func, FILE_ATTR)
process_file_data(cls, name, func, file_attr)
看起來只有短短的兩行,其實 ddt 在函數(shù) process_file_data 內(nèi)部做了很多操作。
首先 ddt 會先拿到我們提供的數(shù)據(jù)文件的絕對地址,并通過后綴名判斷它是 yaml 文件還是 json 文件,然后分別調(diào)用 yaml 或者 json 的 load 方法拿到文件里提供的數(shù)據(jù)。
拿到數(shù)據(jù)后,最終也是通過 mk_test_name 函數(shù)和 add_test 函數(shù),生成多條測試用例,并且注冊到 unittest 的 TestSuite 里去。
最后一樣是刪除原來的測試函數(shù):
# wrapper源碼54行
delattr(cls, name)
這就是 ddt 的整個實現(xiàn)邏輯了。
總結(jié)
????今天我們了解了 unittest 里數(shù)據(jù)驅(qū)動 DDT 的安裝、使用,以及實現(xiàn)原理。通過對其源代碼的解析,我們掌握DDT 是如何實現(xiàn)按照數(shù)據(jù)組數(shù)生成測試用例、更新測試方法名,以及根據(jù)數(shù)據(jù)類型 unpack 測試數(shù)據(jù)的。
????DDT 的源代碼非常經(jīng)典,代碼行數(shù)又不多,值得我們深讀。仔細(xì)琢磨并研究透 DDT 的源碼,有助于你的測試開發(fā)技術(shù)突飛猛進。
????我希望你能用單步調(diào)試的方式,結(jié)合本節(jié)課所講,邊執(zhí)行測試代碼邊走讀 DDT 代碼,這樣有助于你加深理解。
在此留一個課后作業(yè):
在本課時“ddt 使用文件提供數(shù)據(jù)——其他格式數(shù)據(jù)文件”這一小節(jié)中,我提及了使用其他數(shù)據(jù)格式進行數(shù)據(jù)驅(qū)動的方法,但是沒有給出代碼示例。
希望你結(jié)合本節(jié)所講內(nèi)容,以 Excel 格式的數(shù)據(jù)為例, 將 Excel 中的數(shù)據(jù)作為數(shù)據(jù)源提供給 DDT 使用
Tips:讀寫 Excel 可以使用相關(guān)的 Library,例如“讀”可以選擇 xlrd、“寫”可以選擇 xlwt。