Pytest高階進階之Fixture

weixin_34075551發表於2017-03-18

一. fixture介紹

fixture是pytest的一個閃光點,pytest要精通怎麼能不學習fixture呢?跟著我一起深入學習fixture吧。其實unittest和nose都支援fixture,但是pytest做得更炫。
fixture是pytest特有的功能,它用pytest.fixture標識,定義在函式前面。在你編寫測試函式的時候,你可以將此函式名稱做為傳入引數,pytest將會以依賴注入方式,將該函式的返回值作為測試函式的傳入引數。
fixture有明確的名字,在其他函式,模組,類或整個工程呼叫它時會被啟用。
fixture是基於模組來執行的,每個fixture的名字就可以觸發一個fixture的函式,它自身也可以呼叫其他的fixture。
我們可以把fixture看做是資源,在你的測試用例執行之前需要去配置這些資源,執行完後需要去釋放資源。比如module型別的fixture,適合於那些許多測試用例都只需要執行一次的操作。
fixture還提供了引數化功能,根據配置和不同元件來選擇不同的引數。
fixture主要的目的是為了提供一種可靠和可重複性的手段去執行那些最基本的測試內容。比如在測試網站的功能時,每個測試用例都要登入和退出,利用fixture就可以只做一次,否則每個測試用例都要做這兩步也是冗餘。

下面會反覆提高Python的Module概念,Python中的一個Module對應的就是一個.py檔案。其中定義的所有函式或者是變數都屬於這個Module。這個Module 對於所有函式而言就相當於一個全域性的名稱空間,而每個函式又都有自己區域性的名稱空間。

二. Fixture基礎例項入門

把一個函式定義為Fixture很簡單,只能在函式宣告之前加上“@pytest.fixture”。其他函式要來呼叫這個Fixture,只用把它當做一個輸入的引數即可。
test_fixture_basic.py

import pytest

@pytest.fixture()
def before():
    print '\nbefore each test'

def test_1(before):
    print 'test_1()'

def test_2(before):
    print 'test_2()'
    assert 0

下面是執行結果,test_1和test_2執行之前都呼叫了before,也就是before執行了兩次。預設情況下,fixture是每個測試用例如果呼叫了該fixture就會執行一次的。

C:\Users\yatyang\PycharmProjects\pytest_example>pytest -v -s test_fixture_basic.py
============================= test session starts =============================
platform win32 -- Python 2.7.13, pytest-3.0.6, py-1.4.32, pluggy-0.4.0 -- C:\Python27\python.exe
cachedir: .cache
metadata: {'Python': '2.7.13', 'Platform': 'Windows-7-6.1.7601-SP1', 'Packages': {'py': '1.4.32', 'pytest': '3.0.6', 'pluggy': '0.4.0'}, 'JAVA_HOME': 'C:\\Program Files (x86)\\Java\\jd
k1.7.0_01', 'Plugins': {'html': '1.14.2', 'metadata': '1.3.0'}}
rootdir: C:\Users\yatyang\PycharmProjects\pytest_example, inifile:
plugins: metadata-1.3.0, html-1.14.2
collected 2 items 

test_fixture_basic.py::test_1
before each test
test_1()
PASSED
test_fixture_basic.py::test_2
before each test
test_2()
FAILED

================================== FAILURES ===================================
___________________________________ test_2 ____________________________________

before = None

    def test_2(before):
        print 'test_2()'
>       assert 0
E       assert 0

test_fixture_basic.py:12: AssertionError
===================== 1 failed, 1 passed in 0.23 seconds ======================

如果你的程式出現了下面的錯誤,就是開始忘記新增‘import pytest',所以不要忘記羅。

=================================== ERRORS ====================================
_________________ ERROR collecting test_fixture_decorator.py __________________
test_fixture_decorator.py:2: in <module>
    @pytest.fixture()
E   NameError: name 'pytest' is not defined
!!!!!!!!!!!!!!!!!!! Interrupted: 1 errors during collection !!!!!!!!!!!!!!!!!!!
=========================== 1 error in 0.36 seconds ===========================

三. 呼叫fixture的三種方式

1. 在測試用例中直接呼叫它,例如第二部分的基礎例項。

2. 用fixture decorator呼叫fixture

可以用以下三種不同的方式來寫,我只變化了函式名字和類名字,內容沒有變。第一種是每個函式前宣告,第二種是封裝在類裡,類裡的每個成員函式宣告,第三種是封裝在類裡在前宣告。在可以看到3中不同方式的執行結果都是一樣。
test_fixture_decorator.py

import pytest

@pytest.fixture()
def before():
    print('\nbefore each test')

@pytest.mark.usefixtures("before")
def test_1():
    print('test_1()')

@pytest.mark.usefixtures("before")
def test_2():
    print('test_2()')

class Test1:
    @pytest.mark.usefixtures("before")
    def test_3(self):
        print('test_1()')

    @pytest.mark.usefixtures("before")
    def test_4(self):
        print('test_2()')

@pytest.mark.usefixtures("before")
class Test2:
    def test_5(self):
        print('test_1()')

    def test_6(self):
        print('test_2()')

執行結果如下,和上面的基礎例項的執行效果一樣。

C:\Users\yatyang\PycharmProjects\pytest_example>pytest -v -s test_fixture_decorator.py
============================= test session starts =============================
platform win32 -- Python 2.7.13, pytest-3.0.6, py-1.4.32, pluggy-0.4.0 -- C:\Python27\python.exe
cachedir: .cache
metadata: {'Python': '2.7.13', 'Platform': 'Windows-7-6.1.7601-SP1', 'Packages': {'py': '1.4.32', 'pytest': '3.0.6', 'pluggy': '0.4.0'}, 'JAVA_HOME': 'C:\\Program Files (x86)\\Java\\jd
k1.7.0_01', 'Plugins': {'html': '1.14.2', 'metadata': '1.3.0'}}
rootdir: C:\Users\yatyang\PycharmProjects\pytest_example, inifile:
plugins: metadata-1.3.0, html-1.14.2
collected 6 items 

test_fixture_decorator.py::test_1
before each test
test_1()
PASSED
test_fixture_decorator.py::test_2
before each test
test_2()
PASSED
test_fixture_decorator.py::Test1::test_3
before each test
test_1()
PASSED
test_fixture_decorator.py::Test1::test_4
before each test
test_2()
PASSED
test_fixture_decorator.py::Test2::test_5
before each test
test_1()
PASSED
test_fixture_decorator.py::Test2::test_6
before each test
test_2()
PASSED

========================== 6 passed in 0.10 seconds ===========================

3. 用autos呼叫fixture

fixture decorator一個optional的引數是autouse, 預設設定為False。
當預設為False,就可以選擇用上面兩種方式來試用fixture。
當設定為True時,在一個session內的所有的test都會自動呼叫這個fixture。
許可權大,責任也大,所以用該功能時也要謹慎小心。

import time
import pytest

@pytest.fixture(scope="module", autouse=True)
def mod_header(request):
    print('\n-----------------')
    print('module      : %s' % request.module.__name__)
    print('-----------------')

@pytest.fixture(scope="function", autouse=True)
def func_header(request):
    print('\n-----------------')
    print('function    : %s' % request.function.__name__)
    print('time        : %s' % time.asctime())
    print('-----------------')

def test_one():
    print('in test_one()')

def test_two():
    print('in test_two()')

從下面的執行結果,可以看到mod_header在該module內執行了一次,而func_header對於每個test都執行了一次,總共兩次。該方式如果用得好,還是可以使程式碼更為簡潔。
但是對於不熟悉自己組的測試框架的人來說,在pytest裡面去新寫測試用例,需要去了解是否已有一些fixture是module或者class級別的需要注意。

C:\Users\yatyang\PycharmProjects\pytest_example>pytest -v -s test_fixture_auto.py
============================= test session starts =============================
platform win32 -- Python 2.7.13, pytest-3.0.6, py-1.4.32, pluggy-0.4.0 -- C:\Python27\python.exe
cachedir: .cache
metadata: {'Python': '2.7.13', 'Platform': 'Windows-7-6.1.7601-SP1', 'Packages': {'py': '1.4.32', 'pytest': '3.0.6', 'pluggy': '0.4.0'}, 'JAVA_HOME': 'C:\\Program Files (x86)\\Java\\jd
k1.7.0_01', 'Plugins': {'html': '1.14.2', 'metadata': '1.3.0'}}
rootdir: C:\Users\yatyang\PycharmProjects\pytest_example, inifile:
plugins: metadata-1.3.0, html-1.14.2
collected 2 items 

test_fixture_auto.py::test_one
-----------------
module      : test_fixture_auto
-----------------

-----------------
function    : test_one
time        : Sat Mar 18 06:56:54 2017
-----------------
in test_one()
PASSED
test_fixture_auto.py::test_two
-----------------
function    : test_two
time        : Sat Mar 18 06:56:54 2017
-----------------
in test_two()
PASSED

========================== 2 passed in 0.03 seconds ===========================

四. fixture scope

function:每個test都執行,預設是function的scope
class:每個class的所有test只執行一次
module:每個module的所有test只執行一次
session:每個session只執行一次

比如你的所有test都需要連線同一個資料庫,那可以設定為module,只需要連線一次資料庫,對於module內的所有test,這樣可以極大的提高執行效率。

五. fixture 返回值

在上面的例子中,fixture返回值都是預設None,我們可以選擇讓fixture返回我們需要的東西。如果你的fixture需要配置一些資料,讀個檔案,或者連線一個資料庫,那麼你可以讓fixture返回這些資料或資源。

如何帶引數
fixture還可以帶引數,可以把引數賦值給params,預設是None。對於param裡面的每個值,fixture都會去呼叫執行一次,就像執行for迴圈一樣把params裡的值遍歷一次。
test_fixture_param.py

import pytest

@pytest.fixture(params=[1, 2, 3])
def test_data(request):
    return request.param

def test_not_2(test_data):
    print('test_data: %s' % test_data)
    assert test_data != 2

可以看到test_not_2裡面把用test_data裡面定義的3個引數執行裡三次。

C:\Users\yatyang\PycharmProjects\pytest_example>pytest -v -s test_fixture_param.py
============================= test session starts =============================
platform win32 -- Python 2.7.13, pytest-3.0.6, py-1.4.32, pluggy-0.4.0 -- C:\Python27\python.exe
cachedir: .cache
metadata: {'Python': '2.7.13', 'Platform': 'Windows-7-6.1.7601-SP1', 'Packages': {'py': '1.4.32', 'pytest': '3.0.6', 'pluggy': '0.4.0'}, 'JAVA_HOME': 'C:\\Program Files (x86)\\Java\\jd
k1.7.0_01', 'Plugins': {'html': '1.14.2', 'metadata': '1.3.0'}}
rootdir: C:\Users\yatyang\PycharmProjects\pytest_example, inifile:
plugins: metadata-1.3.0, html-1.14.2
collected 3 items 

test_fixture_param.py::test_not_2[1] test_data: 1
PASSED
test_fixture_param.py::test_not_2[2] test_data: 2
FAILED
test_fixture_param.py::test_not_2[3] test_data: 3
PASSED

================================== FAILURES ===================================
________________________________ test_not_2[2] ________________________________

test_data = 2

    def test_not_2(test_data):
        print('test_data: %s' % test_data)
>       assert test_data != 2
E       assert 2 != 2

test_fixture_param.py:9: AssertionError
===================== 1 failed, 2 passed in 0.24 seconds ======================

本文對pytest的fixture進行了深入的講解和練習,希望讀者能夠把fixture這些高階功能都用到平時的pytest功能中,提高執行效率。

相關文章