pytest之fixture

背景

本文总结pytest中极具特色和功能强大的fixture。

说明

本文将从以下几点进行总结:

  1. fixture的概念和作用
  2. fixture的参数
  3. 如何使用fixture
  4. fixture的优先级
  5. fixture的作用范围
  6. fixture的autouse参数说明
  7. fixture的重命名
  8. fixture在测试用例结束后清除、恢复等行为
  9. fixture调用fixture
  10. 利用fixture传递参数

fixture的概念和作用

参考unittest框架中的setup\teardown的概念来理解fixture。

setup\teardown的作用范围是全局的。比如其概念是在测试用例的开始和结束被执行,那么该测试类中的所有测试用例都在开始前都会执行setup,在结束前都会执行setdown。

如果有这样一种场景,某个前置条件,只需要在某个用例开始前完成,在另一个用例开始前不需要甚至不允许被完成,setup的局限性就出来了。

fixture的作用范围可以指定到具体的某一个测试用例,而不是测试文件中全部测试用例。
其作用范围的具体内容将在下面总结。

以上,是pytest中fixture概念比unittest优秀的地方,相信这是大多数人在二者中选择pytest的原因。

fixture的概念是:
在测试函数的运行前后,由pytest运行的外壳函数。fixture是为替代测试用例完成导入、设置、清理工作而设计,使得测试用例可以专注测试步骤本身。fixture的代码可以定制,可以模块化。每个内置fixture有明确的名称,自定义fixture可以自定义fixture名称,pytest通过fixture名称来使用fixture。

fixture的作用是:
配置测试前系统的初始状态、完成测试用例的前置条件,传递测试数据,测试用例完成后对测试环境的请理、复原。

fixture的参数

fixture有以下参数:

fixture(scope="function", params=None, autouse=False, ids=None, name=None):

scope:
控制fixture生效的频率。准确设置scope有助于节省多次创建fixture产生的时间。
可取四个值:
function(函数级别):scope的默认值,每个测试函数可以且只需要运行一次该fixture。
class(测试类级别):每个测试类只需要运行一次该fixture,测试类中的方法都可以共享这个fixture。意味着fixture只需要在测试类级别创建一次,那么就可以供给多个测试方法复用。
module(测试模块级别):同理,每个测试模块只需要运行一次该fixture,该模块中的测试类、测试类中方法、测试函数都可以复用该fixture,而不需要反复创建。
session(会话级别):同理,会话级别的fixture每个测试会话只需要运行一次,那么该次pytest测试会话中的所有方法、函数都已复用该fixture。

params:
fixture函数的参数列表,fixture的参数化会用到这个参数。如果该参数列表中有N个参数,那么意味着调用该fixture时,会使得该fixture执行N次(以传入每个参数执行一次),那么同理可得,调用该fixture的测试用例也会执行N次。

autouse:
如果该参数设置为True,则scope指定的作用范围内,该fixture将默认被执行,而不需要通过名称来使用。

ids:
一个id列表,列表长度与params列表对应。其作用是为传入不同的参数运行的fixture指定相应的id。如果没有该id列表,那么当fixture参数化运行时,pytest使用fixture名加一串数字作为fixture标识。

name:
用来指定fixture的名字,当该参数没有被指定时,fixture的函数名就是fixture的名字。

如何使用fixture

使用fixture的两种方式:

  1. 在测试函数或测试方法的参数列表中指定fixture名字,即把fixture名字作为测试用例的传入参数。适用于测试函数和测试类中的测试方法。该方式可以使得测试函数或测试方法使用fixture的返回值。
  2. 使用usefixtures指定fixture。如@pytest.mark.usefixtures('fixture1','fixture2')。使用该标记来标记测试函数(方法)或测试类。该方法与上个方法相比,区别在于该方法无法使用fixture的返回值。但该方法非常适合对测试类使用。

示例1:
在测试函数或测试方法的参数列表中指定fixture名字

# ./conftest.py

import pytest
@pytest.fixture()
def fixture_1():
	print("我是fixture_1")
	return("我是fixture_1的返回值")

# ./test_case/test_func.py
import pytest
from func import *

class TestFunc:

	# 正常测试用例
	def test_add_by_class(self,fixture_1):
		assert add(2,3) == 5

def test_add_by_func_aaa(fixture_1):
	assert 'aaa' == 'aaa'
	relust = fixture_1
	print(relust)
	
# ./run_test.py
import pytest

if __name__ == '__main__':
	pytest.main(['-v','-s'])
		
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 2 items

test_case/test_func.py::TestFunc::test_add_by_class 我是fixture_1
PASSED
test_case/test_func.py::test_add_by_func_aaa 我是fixture_1
我是fixture_1的返回值
PASSED

============================== 2 passed in 0.04s ==============================
[Finished in 1.4s]
'''

示例2:
使用usefixtures指定fixture。

# ./conftest.py

import pytest

@pytest.fixture(scope='class')
def fixture_1():
	print("我是fixture_1")
	return("我是fixture_1的返回值")

# ./test_case/test_func.py
import pytest
from func import *

@pytest.mark.usefixtures('fixture_1')
class TestFunc:

	# 正常测试用例
	def test_add_by_class(self,fixture_1):
		assert add(2,3) == 5
		print(fixture_1)

	def test_add_by_class_11(self,fixture_1):
		assert add(2,3) == 5
		print(fixture_1)


def test_add_by_func_aaa(fixture_1):
	assert 'aaa' == 'aaa'
	print(fixture_1)
	
# ./run_test.py
import pytest

if __name__ == '__main__':
	pytest.main(['-v','-s'])
		
'''
stdout:
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 3 items

test_case/test_func.py::TestFunc::test_add_by_class 我是fixture_1
我是fixture_1的返回值
PASSED
test_case/test_func.py::TestFunc::test_add_by_class_11 我是fixture_1的返回值
PASSED
test_case/test_func.py::test_add_by_func_aaa 我是fixture_1
我是fixture_1的返回值
PASSED

============================== 3 passed in 0.04s ==============================
[Finished in 1.4s]
'''

Tips:
本示例2中fixture_1是测试类级别的,虽然我们在测试类和测试类中的方法都使用的fixture_1,但从stdout中可以看出,测试类中仅一个fixture_1被创建了,测试方法test_add_by_class_11中的fixture_1没有再次创建,但是可以复用fixture_1,因为可以获取其返回值。该点涉及到fixture的作用范围。后面会进一步总结。

fixture的优先级

如果一个测试用例同时使用了多个fixture,那么这些fixture谁先执行,谁后执行呢。
大概依据如下原则:

  1. 不同scope级别的fixture,优先执行高级别的fixture(scope级别由高到低:session-module-class-functin)
  2. 相同scope级别的fixture,按照使用的顺序(先使用先执行)和依赖关系(被依赖的先执行)来决定执行顺序。

参考如下示例 :

# ./conftest.py

import pytest

@pytest.fixture(scope='session')
def f_1():
	print("我是fixture_session")

@pytest.fixture(scope='module')
def f_2():
	print("我是fixture_module")

@pytest.fixture(scope='class')
def f_3():
	print("我是fixture_class")

@pytest.fixture(scope='function')
def f_temp():
	print("我是fixture_function_temp")

@pytest.fixture(scope='function')
def f_4(f_temp):
	print("我是fixture_function_4")

@pytest.fixture(scope='function')
def f_5():
	print("我是fixture_function_5")

@pytest.fixture(scope='function')
def f_6():
	print("我是fixture_function_6")

# ./test_case/test_func.py
import pytest

def test_add_by_func_aaa(f_6, f_5, f_4, f_3, f_2, f_1):
	assert 'aaa' == 'aaa'
	

# ./run_test.py
import pytest

if __name__ == '__main__':
	pytest.main(['-v','-s'])
		
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 1 item

test_case/test_func.py::test_add_by_func_aaa 
我是fixture_session
我是fixture_module
我是fixture_class
我是fixture_function_6
我是fixture_function_5
我是fixture_function_temp
我是fixture_function_4
PASSED

============================== 1 passed in 0.04s ==============================
[Finished in 1.4s]
'''	

fixture的作用范围

参考如下原则:

  1. session级别的fixture在整个测试会话中只需要创建一次,会话中的所有测试用例都可以共享它。
  2. module级别的fixture在整个测试模块中只需要创建一次,模块中的所有测试用例都可以共享它。
  3. class级别的fixture在整个测试类中只需要创建一次,测试类中的所有测试用例都可以共享它。
  4. function级别的fixture在整个测试用例中只需要创建一次,用例中的所有代码都可以共享它。

示例:

# ./conftest.py

import pytest

@pytest.fixture(scope='session')
def f_session():
	print("我是fixture_session")
	return "我是f_session的返回值"

@pytest.fixture(scope='module')
def f_module():
	print("我是fixture_module")
	return "我是f_module的返回值"

@pytest.fixture(scope='class')
def f_class():
	print("我是fixture_class")
	return "我是f_class的返回值"

@pytest.fixture(scope='function')
def f_function():
	print("我是fixture_function")
	return "我是f_function的返回值"


# ./test_case/test_func.py
import pytest
from func import *

@pytest.mark.usefixtures('f_session')
@pytest.mark.usefixtures('f_module')
@pytest.mark.usefixtures('f_class')
class TestFunc:

	# 正常测试用例
	def test_add_by_class(self,f_session, f_module, f_class, f_function):
		assert add(2,3) == 5


	def test_add_by_class_11(self,f_session, f_module, f_class, f_function):
		assert add(2,3) == 5
		print(f_session)
		print(f_module)
		print(f_class)
		print(f_function)


def test_add_by_func_aaa(f_session, f_module, f_function):
	assert 'aaa' == 'aaa'
	print(f_session)
	print(f_module)
	print(f_function)
	
	
	
# ./test_case/test_func_0001.py
def test_fixture(f_session, f_module, f_function):
	print(f_session)
	print(f_module)
	print(f_function)

# ./run_test.py
import pytest

if __name__ == '__main__':
	pytest.main(['-v','-s'])
	

'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 4 items

test_case/test_func.py::TestFunc::test_add_by_class 我是fixture_session
我是fixture_module
我是fixture_class
我是fixture_function
PASSED
test_case/test_func.py::TestFunc::test_add_by_class_11 我是fixture_function
我是f_session的返回值
我是f_module的返回值
我是f_class的返回值
我是f_function的返回值
PASSED
test_case/test_func.py::test_add_by_func_aaa 我是fixture_function
我是f_session的返回值
我是f_module的返回值
我是f_function的返回值
PASSED
test_case/test_func_0001.py::test_fixture 我是fixture_module
我是fixture_function
我是f_session的返回值
我是f_module的返回值
我是f_function的返回值
PASSED

============================== 4 passed in 0.05s ==============================
[Finished in 1.4s]
'''

从以上示例看出,
在test_func.py测试模块中,虽然测试类、测试类中测试方法、测试函数都使用了f_session, f_module, f_class, f_function这几个fixture。但f_session、f_module、f_class只创建了一次,测试模块中的测试用例就可以共享该fixture。而f_function这个fixture在每个测试用例都创建了。
在test_func_0001.py测试模块中,虽然测试用例使用了f_session, f_module, f_function三个fixture,但是只有 f_module, f_function被创建了,但可以共享f_session。

fixture的autouse参数说明

当autouse=True时,在scope指定的范围内,fixture将被强制执行,而无需通过名字使用。
不建议大面积使用该参数,仅在某种需要强制执行fixture的场景中使用。比如web自动化中每个测试用例执行完成后都需要销毁浏览器,就可以使用autouse=True的fixture。

示例:

# ./conftest.py

import pytest


@pytest.fixture(scope='function', autouse=True)
def f_function():
	print("我是fixture_function")
	return "我是f_function的返回值"

# ./test_case/test_func.py
import pytest
from func import *


class TestFunc:

	# 正常测试用例
	def test_add_by_class(self):
		assert add(2,3) == 5


	def test_add_by_class_11(self):
		assert add(2,3) == 5
		


def test_add_by_func_aaa():
	assert 'aaa' == 'aaa'
	
	
# ./run_test.py
import pytest

if __name__ == '__main__':
	pytest.main(['-v','-s'])
		
	
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 3 items

test_case/test_func.py::TestFunc::test_add_by_class 我是fixture_function
PASSED
test_case/test_func.py::TestFunc::test_add_by_class_11 我是fixture_function
PASSED
test_case/test_func.py::test_add_by_func_aaa 我是fixture_function
PASSED

============================== 3 passed in 0.05s ==============================
[Finished in 1.4s]

fixture的重命名

测试用例通过fixture的名字使用fixture。fixture默认名字就是fixture函数名。可以通过name参数指定fixture名字。
示例:

# ./conftest.py

import pytest


@pytest.fixture(scope='function', name="a_renamed_fixture")
def f_function():
	print("我是fixture_function")
	return "我是f_function的返回值"


# ./test_case/test_func.py
import pytest
from func import *


class TestFunc:

	# 正常测试用例
	def test_add_by_class(self):
		assert add(2,3) == 5


	def test_add_by_class_11(self):
		assert add(2,3) == 5
		


def test_add_by_func_aaa(a_renamed_fixture):
	assert 'aaa' == 'aaa'
	print(a_renamed_fixture)
	
	
	
# ./run_test.py
import pytest

if __name__ == '__main__':
	pytest.main(['-v','-s'])
		
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 3 items

test_case/test_func.py::TestFunc::test_add_by_class PASSED
test_case/test_func.py::TestFunc::test_add_by_class_11 PASSED
test_case/test_func.py::test_add_by_func_aaa 我是fixture_function
我是f_function的返回值
PASSED

============================== 3 passed in 0.05s ==============================
[Finished in 1.4s]

'''

fixture在测试用例结束后清除、恢复等行为

前面的示例,都是在说测试用例的前置操作,即测试用例执行前,fixture做了什么。那么怎样实现测试用例执行完毕后,使用fiixture做点什么呢。
很简单,只需要一个yield关键字。
示例:

# ./conftest.py

import pytest


@pytest.fixture(scope='function', name="a_renamed_fixture")
def f_function():
	yield
	print("我是fixture_function,来执行请理恢复任务")
	
# ./test_case/test_func.py
import pytest

def test_add_by_func_aaa(a_renamed_fixture):
	assert 'aaa' == 'aaa'
	print("测试用例开始执行")
	
# ./run_test.py
import pytest

if __name__ == '__main__':
	pytest.main(['-v','-s'])
		
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 1 item

test_case/test_func.py::test_add_by_func_aaa 测试用例开始执行
PASSED我是fixture_function,来执行请理恢复任务


============================== 1 passed in 0.04s ==============================
[Finished in 1.4s]
'''	

为了更清晰些,我们再看看一个没有yileld的示例:

# ./conftest.py

import pytest


@pytest.fixture(scope='function', name="a_renamed_fixture")
def f_function():
	#yield
	print("我是fixture_function,来执行请理恢复任务")
	
# ./test_case/test_func.py
import pytest

def test_add_by_func_aaa(a_renamed_fixture):
	assert 'aaa' == 'aaa'
	print("测试用例开始执行")
# ./run_test.py
import pytest

if __name__ == '__main__':
	pytest.main(['-v','-s'])
	
	
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 1 item

test_case/test_func.py::test_add_by_func_aaa 我是fixture_function,来执行请理恢复任务
测试用例开始执行
PASSED

============================== 1 passed in 0.04s ==============================
[Finished in 1.4s]
'''	
	

fixture调用fixture

pytest中测试用例会请求fixture对象。
但由于fixture对象的模块化属性,使得fixture对象请求调用fixture对象成为可能。
调用原则参考fixture的优先级原则:
即 “ 不同scope级别的fixture,优先执行高级别的fixture(scope级别由高到低:session-module-class-functin)”
根据这以原则,可以明白,低优先级的fixture可以调用高优先级的fixture。但高优先级的fixture不可以调用低优先级的fixture。
因为根据“依赖关系(被依赖的先执行)”,被调用(即被依赖)的fixture(低级别)将先执行,这时就违背了高级别的fixture优先执行的原则。
正确示例:低优先级的fixture调用高优先级的fixture

# ./conftest.py

import pytest


@pytest.fixture(scope='function')
def f_function(f_session):
	print("我是fixture_function")
	print(f_session)
	return "FFFunc"



@pytest.fixture(scope='session')
def f_session():
	print("我是fixture_session")
	#print(f_function)
	return "SSSess"	


# ./test_case/test_func.py
import pytest



def test_add_by_func_aaa(f_function):
	assert 'aaa' == 'aaa'
	
# ./run_test.py
import pytest

if __name__ == '__main__':
	pytest.main(['-v','-s'])
		
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 1 item

test_case/test_func.py::test_add_by_func_aaa 我是fixture_session
我是fixture_function
SSSess
PASSED

============================== 1 passed in 0.03s ==============================
[Finished in 1.3s]
'''	

错误示例:高优先级的fixture调用低优先级的fixture。结果为直接报错。

# ./conftest.py

import pytest


@pytest.fixture(scope='function')
def f_function():
	print("我是fixture_function")
	#print(f_session)
	return "FFFunc"



@pytest.fixture(scope='session')
def f_session(f_function):
	print("我是fixture_session")
	print(f_function)
	return "SSSess"	


# ./test_case/test_func.py
import pytest

def test_add_by_func_aaa(f_session):
	assert 'aaa' == 'aaa'
	
# ./run_test.py
import pytest

if __name__ == '__main__':
	pytest.main(['-v','-s'])
		
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 1 item

test_case/test_func.py::test_add_by_func_aaa ERROR

=================================== ERRORS ====================================
___________________ ERROR at setup of test_add_by_func_aaa ____________________
ScopeMismatch: You tried to access the 'function' scoped fixture 'f_function' with a 'session' scoped request object, involved factories
conftest.py:14:  def f_session(f_function)
conftest.py:6:  def f_function()
============================== 1 error in 0.04s ===============================
[Finished in 1.3s]
'''	

利用fixture传递参数

由于fixture本身也是个函数对象,所以有返回值。
fixture传底参数其实就是依靠返回值传递参数。
比如,web自动化中,URL就可以通过fixture传递给每个测试用例。
示例:

# ./conftest.py

import pytest


url = r"https://www.baidu.com"

@pytest.fixture(scope='function')
def f_function():
	
	return url

# ./test_case/test_func.py
import pytest
def test_add_by_func_aaa(f_function):
	print(f_function)
	

# ./run_test.py
import pytest

if __name__ == '__main__':
	pytest.main(['-v','-s'])
		
	
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 1 item

test_case/test_func.py::test_add_by_func_aaa https://www.baidu.com
PASSED

============================== 1 passed in 0.04s ==============================
[Finished in 1.4s]
'''	
	



最后

以上,除了fixture参数化,就是常用的fixture特性。如果描述有误,请指出。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值