使用DDT實現(xiàn)自動化測試數(shù)據(jù)驅(qū)動

什么是數(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。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容