Pytest Fixture

關於pytest fixtures,根據官方文檔介紹: fixture 用於提供一個固定的基線,使 Cases 可以在此基礎上可靠地、重複地執行。對比 PyUnit 經典的setup/teardown形式,它在以下方面有了明顯的改進:

  1. fixture擁有一個明確的名稱,通過聲明使其能夠在函數、類、模塊,甚至整個測試會話中被激活使用;
  2. fixture以一種模塊化的方式實現,原因在於每一個fixture的名字都能觸發一個fixture函數,而這個函數本身又能調用其它的fixture;
  3. fixture的管理從簡單的單元測試擴展到複雜的功能測試,允許通過配置和組件選項參數化fixture和測試用例,或者跨功能、類、模塊,甚至整個測試會話複用fixture;

一句話概括:在整個測試執行的上下文中,fixture扮演注入者(injector)的角色,而測試用例扮演消費者(client)的角色,測試用例可以輕鬆的接收和處理需要預初始化操作的應用對象,而不用過分關心其實現的具體細節。

fixture的實例化順序

fixture支持的作用域(Scope):function(default)、class、module、package、session。
其中,package作用域是在 pytest 3.7 的版本中,正式引入的,目前仍處於實驗性階段。
多個fixture的實例化順序,遵循以下原則:

  1. 高級別作用域的(例如:session)優先於 低級別的作用域的(例如:class或者function)實例化;
  2. 相同級別作用域的,其實例化順序遵循它們在測試用例中被聲明的順序(也就是形參的順序),或者fixture之間的相互調用關係;
  3. 指明autouse=True的fixture,先於其同級別的其它fixture實例化。

fixture 實現 teardown 功能

有以下幾種方法:

注意:在yield之前或者addfinalizer註冊之前代碼發生錯誤退出的,都不會再執行後續的清理操作。

  1. 將fixture變爲生成器方法(推薦)
    即將fixture函數中的return關鍵字替換成yield,則yield之後的代碼,就是我們要的清理操作。
@pytest.fixture(scope='session', autouse=True)
def clear_token():
    yield
    from libs.redis_m import RedisManager
    rdm = RedisManager()
    rdm.expire_token(seconds=60)
  1. 使用addfinalizer方法
    fixture函數能夠接收一個request的參數,表示測試請求的上下文(下面會詳細介紹),我們可以使用request.addfinalizer方法爲fixture添加清理函數。
@pytest.fixture()
def smtp_connection_fin(request):
    smtp_connection = smtplib.SMTP("smtp.163.com", 25, timeout=5)

    def fin():
        smtp_connection.close()

    request.addfinalizer(fin)
    return smtp_connection
  1. 使用with寫法(不推薦)
    對於支持with寫法的對象,我們也可以隱式的執行它的清理操作:
@pytest.fixture()
def smtp_connection_yield():
    with smtplib.SMTP("smtp.163.com", 25, timeout=5) as smtp_connection:
        yield smtp_connection

fixture可以訪問測試請求的上下文

fixture函數可以接收一個request的參數,表示測試用例、類、模塊,甚至測試會話的上下文環境;
例如可以擴展下上面的smtp_connection_yield,讓其根據不同的測試模塊使用不同的服務器:

@pytest.fixture(scope='module')
def smtp_connection_request(request):
    server, port = getattr(request.module, 'smtp_server', ("smtp.163.com", 25))
    with smtplib.SMTP(server, port, timeout=5) as smtp_connection:
        yield smtp_connection
        print("斷開 %s:%d" % (server, port))

在測試模塊中指定smtp_server

smtp_server = ("mail.python.org", 587)
def test_163(smtp_connection_request):
    response, _ = smtp_connection_request.ehlo()
    assert response == 250

fixture返回工廠函數

如果需要在一個測試用例(function)中,多次使用同一個fixture實例,相對於直接返回數據,更好的方法是返回一個產生數據的工廠函數。並且,對於工廠函數產生的數據,也可以在fixture中對其管理:

@pytest.fixture
def make_customer_record():

    # 記錄生產的數據
    created_records = []

    # 工廠
    def _make_customer_record(name):
        record = models.Customer(name=name, orders=[])
        created_records.append(record)
        return record

    yield _make_customer_record

    # 銷燬數據
    for record in created_records:
        record.destroy()


def test_customer_records(make_customer_record):
    customer_1 = make_customer_record("Lisa")
    customer_2 = make_customer_record("Mike")
    customer_3 = make_customer_record("Meredith")

fixture的參數化

如果你需要在一系列的測試用例的執行中,每輪執行都使用同一個fixture,但是有不同的依賴場景,那麼可以考慮對fixture進行參數化;這種方式適用於對多場景的功能模塊進行詳盡的測試。

@pytest.fixture(scope='module', params=['smtp.163.com', "mail.python.org"])
def smtp_connection_params(request):
    server = request.param
    with smtplib.SMTP(server, 587, timeout=5) as smtp_connection:
        yield smtp_connection

def test_parames(smtp_connection_params):
    response, _ = smtp_connection_params.ehlo()
    assert response == 250

在不同的層級上覆寫fixture

注意:低級別的作用域可以調用高級別的作用域,但是高級別的作用域調用低級別的作用域會返回一個ScopeMismatch的異常。

在大型的測試中,可能需要在本地覆蓋項目級別的fixture,以增加可讀性和便於維護:

@pytest.fixture(scope="module", autouse=True)
def init(frag_login):
    pass

@pytest.fixture(scope='session')
def active_user_account(cmd_line_args, conf):
    tail_num = cmd_line_args.get("tailnum", None)
    if tail_num is None:
        tail_num = "1"
    for user in conf['unified']:
        if str(user['uid'])[-1] == tail_num:
            return user
    msg = f"尾號[{tail_num}], 在配置文件中未找到"
    logger.error(msg)
    raise ValueError(msg)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章