1.資料參數化介紹
隻要你是負責編寫自動化測試腳本的,資料參數化這個思想你就肯定會用 ,資料參數化的工具你肯定的懂一些 ,因為它能大大的提高我們自動化腳本編寫效率 。
1.1什麼是資料參數化
所謂的資料參數化 ,是指所執行的測試用例步驟相同、而資料不同 ,每次運作用例隻變化的是資料 ,于是将這些資料專門放在一起進行批量循環運作 ,進而完成測試用例執行的目的 。
以登入功能為例 ,若一個登入功能每次操作的步驟是 :
- 輸入使用者名
- 輸入密碼
- 點選登入按鈕 。
但是,因為每次輸入的資料不同,導緻生成的測試用例就不同了 ,同樣還是這個登入功能,加上資料就變為以下的用例了 。
- case1 : 輸入正确的使用者名 ,輸入正确的密碼 ,點選登入
- case2 : 輸入正确的使用者,輸入錯誤的密碼,點選登入
- case3 :輸入正确的使用者名,輸入空的密碼,點選登入
- casen : ...
可以看到 ,在這些用例中,每條用例最大的不同是什麼呢 ?其實就是資料不同 。但是由于資料不同,進而生成了多條測試用例 ,在功能測試中,這些用例是需要分别寫、分别執行 。
1.2.為什麼要進行資料參數化 ?
在功能測試中,即使是相同的步驟 ,隻是資料不同 ,我們亦然也要盡量分開編寫每一條用例 ,比如像上面的編寫方式 ,因為這些編寫它的易讀性更好 ,功能測試設計測試用例和執行用例往往不是一個人 ,是以用例編寫的易讀性是就是一個很重要的因素 。
但是如果将上面的用例進行自動化實作 ,雖然按照一條用例對應一個方法是一種很清晰的思路 ,但是它的最大問題就是代碼備援 ,當一個功能中步驟相同,隻是資料不同時,你的資料越多,代碼備援度就越高 。你會發現每個測試方法中的代碼就會是相同的 。
像代碼備援這種問題,在編寫自動化時是必須要考慮的一個問題,因為随着代碼量越多 ,備援度越高、越難維護 。
以下就是是通過正常方式實作登入的自動化腳本 :
import unittest
from package_unittest.login import login
class TestLogin(unittest.TestCase):
# case1 : 輸入正确的使用者名和正确的密碼進行登入
def test_login_success(self):
expect_reslut = 0
actual_result = login('admin','123456').get('code')
self.assertEqual(expect_reslut,actual_result)
# case2 : 輸入正确的使用者名和錯誤的密碼進行登入
def test_password_is_wrong(self):
expect_reslut = 3
actual_result = login('admin', '1234567').get('code')
self.assertEqual(expect_reslut, actual_result)
# case3 : 輸入正确的使用者名和空的密碼進行登入
def test_password_is_null(self):
expect_reslut = 2
actual_result = login('admin', '').get('code')
self.assertEqual(expect_reslut, actual_result)
可以看到,三條用例對應三個測試方法,雖然清晰 ,代碼每個方法中的代碼幾乎是相同的。
那如果用參數化實作的代碼是什麼呢 ? 可以看下面的這段代碼 :
class TestLogin(unittest.TestCase):
@parameterized.expand(cases)
def test_login(self,expect_result,username,password):
actual_result = login(username,password).get('code')
self.assertEqual(expect_result,actual_result)
以上代碼隻有一條用例 ,不管這個功能有幾條都能執行 。
通過上面兩種形式的比較可以看出 :為什麼要進行資料參數化呢 ?其實就是降低代碼備援、提高代碼複用度 ,将主要編寫測試用例的時間轉化為編寫測試資料上來 。
1.3.如何進行資料參數化
在代碼中實作資料參數化都需要借助于外部工具 ,比如專門用于unittest的ddt , 既支援unittest、也支援pytest的parameterized ,專門在pytest中使用的fixture.params .
參數化工具 | 支援測試架構 | 備注 |
ddt | unittest | 第三方包,需要下載下傳安裝 |
parameterized | nose,unittest,pytest | 第三方包,需要下載下傳安裝 |
@pytest.mark.parametrize | pytest | 本身屬于pytest中的功能 |
@pytest.fixture(params=[]) | pytest | 本身屬于pytest中的功能 |
以上實作資料參數化的工具有兩個共同點:
- 都能實作資料參數化
- 都時裝飾器來作用于測試用例腳本 。
2.子產品介紹
1.下載下傳安裝 :
# 下載下傳
pip install parameterized
# 驗證 :
pip show parameterized
2.導包
# 直接導入parameterized類
from parameterized import parameterized
3.官網示例
@parameterized 和 @parameterized.expand 裝飾器接受清單 或元組或參數(...)的可疊代對象,或傳回清單或 可疊代:
from parameterized import parameterized, param
# A list of tuples
@parameterized([
(2, 3, 5),
(3, 5, 8),
])
def test_add(a, b, expected):
assert_equal(a + b, expected)
# A list of params
@parameterized([
param("10", 10),
param("10", 16, base=16),
])
def test_int(str_val, expected, base=10):
assert_equal(int(str_val, base=base), expected)
# An iterable of params
@parameterized(
param.explicit(*json.loads(line))
for line in open("testcases.jsons")
)
def test_from_json_file(...):
...
# A callable which returns a list of tuples
def load_test_cases():
return [
("test1", ),
("test2", ),
]
@parameterized(load_test_cases)
def test_from_function(name):
...
請注意,使用疊代器或生成器時,将加載所有項 在測試運作開始之前放入記憶體(我們顯式執行此操作以確定 生成器在多程序或多線程中隻耗盡一次 測試環境)。
@parameterized裝飾器可以使用測試類方法,并且可以獨立使用 功能:
from parameterized import parameterized
class AddTest(object):
@parameterized([
(2, 3, 5),
])
def test_add(self, a, b, expected):
assert_equal(a + b, expected)
@parameterized([
(2, 3, 5),
])
def test_add(a, b, expected):
assert_equal(a + b, expected)
@parameterized.expand可用于生成測試方法 無法使用測試生成器的情況(例如,當測試 類是單元測試的一個子類。測試用例):
import unittest
from parameterized import parameterized
class AddTestCase(unittest.TestCase):
@parameterized.expand([
("2 and 3", 2, 3, 5),
("3 and 5", 3, 5, 8),
])
def test_add(self, _, a, b, expected):
assert_equal(a + b, expected)
将建立測試用例:
$ nosetests example.py
test_add_0_2_and_3 (example.AddTestCase) ... ok
test_add_1_3_and_5 (example.AddTestCase) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
請注意,@parameterized.expand 的工作原理是在測試上建立新方法 .class。如果第一個參數是字元串,則該字元串将添加到末尾 的方法名稱。例如,上面的測試用例将生成方法test_add_0_2_and_3和test_add_1_3_and_5。
@parameterized.expand 生成的測試用例的名稱可以是 使用 name_func 關鍵字參數進行自定義。該值應 是一個接受三個參數的函數:testcase_func、param_num、 和參數,它應該傳回測試用例的名稱。testcase_func是要測試的功能,param_num将是 參數清單中測試用例參數的索引,參數(參數的執行個體)将是将使用的參數。
import unittest
from parameterized import parameterized
def custom_name_func(testcase_func, param_num, param):
return "%s_%s" %(
testcase_func.__name__,
parameterized.to_safe_name("_".join(str(x) for x in param.args)),
)
class AddTestCase(unittest.TestCase):
@parameterized.expand([
(2, 3, 5),
(2, 3, 5),
], name_func=custom_name_func)
def test_add(self, a, b, expected):
assert_equal(a + b, expected)
将建立測試用例:
$ nosetests example.py
test_add_1_2_3 (example.AddTestCase) ... ok
test_add_2_3_5 (example.AddTestCase) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
param(...) 幫助程式類存儲一個特定測試的參數 箱。它可用于将關鍵字參數傳遞給測試用例:
from parameterized import parameterized, param
@parameterized([
param("10", 10),
param("10", 16, base=16),
])
def test_int(str_val, expected, base=10):
assert_equal(int(str_val, base=base), expected)
如果測試用例具有文檔字元串,則該測試用例的參數将為 附加到文檔字元串的第一行。可以控制此行為 doc_func參數:
from parameterized import parameterized
@parameterized([
(1, 2, 3),
(4, 5, 9),
])
def test_add(a, b, expected):
""" Test addition. """
assert_equal(a + b, expected)
def my_doc_func(func, num, param):
return "%s: %s with %s" %(num, func.__name__, param)
@parameterized([
(5, 4, 1),
(9, 6, 3),
], doc_func=my_doc_func)
def test_subtraction(a, b, expected):
assert_equal(a - b, expected)
$ nosetests example.py
Test addition. [with a=1, b=2, expected=3] ... ok
Test addition. [with a=4, b=5, expected=9] ... ok
0: test_subtraction with param(*(5, 4, 1)) ... ok
1: test_subtraction with param(*(9, 6, 3)) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
最後@parameterized_class參數化整個類,使用 屬性清單或将應用于 .class:
from yourapp.models import User
from parameterized import parameterized_class
@parameterized_class([
{ "username": "user_1", "access_level": 1 },
{ "username": "user_2", "access_level": 2, "expected_status_code": 404 },
])
class TestUserAccessLevel(TestCase):
expected_status_code = 200
def setUp(self):
self.client.force_login(User.objects.get(username=self.username)[0])
def test_url_a(self):
response = self.client.get('/url')
self.assertEqual(response.status_code, self.expected_status_code)
def tearDown(self):
self.client.logout()
@parameterized_class(("username", "access_level", "expected_status_code"), [
("user_1", 1, 200),
("user_2", 2, 404)
])
class TestUserAccessLevel(TestCase):
def setUp(self):
self.client.force_login(User.objects.get(username=self.username)[0])
def test_url_a(self):
response = self.client.get("/url")
self.assertEqual(response.status_code, self.expected_status_code)
def tearDown(self):
self.client.logout()
@parameterized_class裝飾器接受class_name_func論點, 它控制由 @parameterized_class 生成的參數化類的名稱:
from parameterized import parameterized, parameterized_class
def get_class_name(cls, num, params_dict):
# By default the generated class named includes either the "name"
# parameter (if present), or the first string value. This example shows
# multiple parameters being included in the generated class name:
return "%s_%s_%s%s" %(
cls.__name__,
num,
parameterized.to_safe_name(params_dict['a']),
parameterized.to_safe_name(params_dict['b']),
)
@parameterized_class([
{ "a": "hello", "b": " world!", "expected": "hello world!" },
{ "a": "say ", "b": " cheese :)", "expected": "say cheese :)" },
], class_name_func=get_class_name)
class TestConcatenation(TestCase):
def test_concat(self):
self.assertEqual(self.a + self.b, self.expected)
$ nosetests -v test_math.py
test_concat (test_concat.TestConcatenation_0_hello_world_) ... ok
test_concat (test_concat.TestConcatenation_0_say_cheese__) ... ok
使用單個參數
如果測試函數隻接受一個參數并且該值不可疊代, 然後可以提供值清單,而無需将每個值包裝在 元:
@parameterized([1, 2, 3])
def test_greater_than_zero(value):
assert value > 0
但請注意,如果單個參數是可疊代的(例如清單或 元組),那麼它必須包裝在元組、清單或 param(...) 裝飾器中:
@parameterized([
([1, 2, 3], ),
([3, 3], ),
([6], ),
])
def test_sums_to_6(numbers):
assert sum(numbers) == 6
雖然看似以上功能支援的挺多 ,但其實真正用的不多 ,因為它跟架構有很大關系的 。具體說明下 :
總結:
- 它支援nose是最好的 . 如果你的自動化中使用nose,那麼以上功能基本都能用到 。
- 如果你用的測試架構是unittest ,你隻能用到它的expand()這個函數 ,不過有這個函數也就夠了 。
- 如果你用的測試架構是pytest , 它支援了Pytest3的版本,再高版本的就不支援了,同時pytest也有自己的參數化工具,一般也不用它了。
3.項目實踐
通過資料參數胡重新編寫登入測試用例 ,将以前yaml中的登入用例資料轉化為paramterized的資料格式 ,它的資料格式要求為:[(),(),()] . 是以,編寫測試用例的資料就變為了以下的代碼 。
# 将登入資料轉化為paramterize所識别的格式。
def get_data():
yaml_path = get_file_path('login.yaml') # 擷取login.yaml的全路徑
result = read_yaml(yaml_path) # 轉化為python對象
login_data = result.get('login') # 擷取字典中login的值
logger.debug("登入結果:{}".format(login_data))
return (login_data) # 擷取字典中login的值
@allure.epic("vshop")
@allure.story("登入")
class TestLogin(unittest.TestCase):
# case1 : 測試登入功能
@parameterized.expand(get_data())
def test_login(self,case_name,username,password,code,message):
logger.info("從參數化擷取的資料:{}|{}|{}|{}|{}".format(case_name,username,password,code,message))
with allure.step("執行用例:{},輸入使用者名:{},輸入密碼:{}".format(case_name,username,password)):
login_result = login(username,password)
self.assertEqual(code, login_result.get('errno'))
self.assertEqual(message, login_result.get('errmsg'))
這樣的話,我們隻編寫了一條測試用例 ,但是在測試資料中有幾條資料 ,都可以正常運作 。
4.項目總結
至此,我們已經實作了五步了 ,分别是 :
第一 、如何編寫一個接口自動化架構 ,在第一篇博文中介紹了 。https://www.toutiao.com/item/7223778665283404323/
第二、如何使用unittest編寫測試用例 ,已經在第二篇博文中介紹了 。https://www.toutiao.com/item/7225986414469825024/
第三、如何使用requests實作接口請求 ,并和測試用例如何對接 ,已經在第三篇博文中介紹了。https://www.toutiao.com/item/7231485629643997748/
第四、如何使用yaml編寫測試資料 ,已經在第四篇博文中介紹了 。https://www.toutiao.com/item/7236369710286733861/
第五,如何使用allure生成測試報告,已經在第五篇博文中介紹了 。https://www.toutiao.com/item/7243783682144944697/
第六 ,如何使用loguru記錄日志 ,已經在第六篇博文中介紹了 。https://www.toutiao.com/item/7253833815246815796/
第七,如何使用pymysql連接配接資料庫,已經在第七篇博文中介紹了 。https://www.toutiao.com/item/7256573953278214668/
第八,如何進行資料參數化 ,也就是本篇博文了 。