天天看點

python實作自動化測試架構如何進行資料參數化?這個包可以了解下

作者:雨滴測試

1.資料參數化介紹

隻要你是負責編寫自動化測試腳本的,資料參數化這個思想你就肯定會用 ,資料參數化的工具你肯定的懂一些 ,因為它能大大的提高我們自動化腳本編寫效率 。

1.1什麼是資料參數化

所謂的資料參數化 ,是指所執行的測試用例步驟相同、而資料不同 ,每次運作用例隻變化的是資料 ,于是将這些資料專門放在一起進行批量循環運作 ,進而完成測試用例執行的目的 。

以登入功能為例 ,若一個登入功能每次操作的步驟是 :

  1. 輸入使用者名
  2. 輸入密碼
  3. 點選登入按鈕 。

但是,因為每次輸入的資料不同,導緻生成的測試用例就不同了 ,同樣還是這個登入功能,加上資料就變為以下的用例了 。

  • 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/

第八,如何進行資料參數化 ,也就是本篇博文了 。