python pytest 测试实战(二)

Menu: python pytest测试实战 3
参考链接
hook函数:https://docs.pytest.org/en/latest/_modules/_pytest/hookspec.html

conftest.py
	完成数据共享
	不同目录可以创建自己的conftest.py文件
	作用域:
		对当前同级目录下所有的文件及包下的所有测试文件,测试方法生效,
        如果同级目录下没有conftest.py,会找上级目录或者上上级目录的
        conftest.py里的fixture方法。不会找兄弟目录。
#conftest.py文件内容
import pytest

@pytest.fixture()
def login(request):          #在方法参数写request
    print("登录方法")
    print(request.param)     #方法体里面使用request.param接收参数
    print("teardown")

#test_demo1文件内容
import pytest

@pytest.mark.parametrize('login',[      #如果conftest里面的方法使用了request参数,则必须进行传参
    ('username','password')
],indirect=True)

def test_demo(login):      #方法接收方法名字
    print("test demo1")

#执行结果为
登录方法
('username', 'password')
teardown
test demo1
pytest 常用插件
pip install pytest-ordering 控制用例的执行顺序
pip install pytest-dependency 控制用例的依赖关系
pip install pytest-xdist 分布式并发执行测试用例
pip install pytest-rerunfailures 失败重跑
pip install pytest-assume 多重较验
pip intall pytest-html 测试报告
pip install pytest-rerunfailures 失败重跑
场景:
	测试失败后要重新运行n次,要在重新运行之间添加延迟时间,间隔n秒再运行
安装:
pip install pytest-rerunfailures
执行:
	 pytest -vs --reruns 5 test_class.py			#指定文件里面所有运行的测试用例
	 pytest -vs --reruns 5 --reruns-delay 1
	 @pytest.mark.flaky(reruns=5, reruns_delay=2)	#指定到单个用例
失败的fixture与setup_class也会被执行
不要与setup_class,setup_module方法使用,一个测试方法上面不要有多个装饰器

pip install pytest-assume 多重校验
#区别于assert,assert失败一条,下面的用例不会接着走,而第三方的pytest-assume就算执行失败也会往下走下去
import pytest
    
@pytest.mark.parametrize(('x', 'y'), [(1, 1), (1, 0), (0, 1)])
def test_simple_assume(x, y):
    pytest.assume(x == y)
    pytest.assume(True)  	#如果使用的是assert,
    						#上面的用例'pytest.assume(x == y)'未执行通过不会执行该条用例
    						#如果使用的是三方插件assume,就算上面用例未执行通过,也会执行该条用例
   
pip install pytest-dependency 控制用例的依赖关系
import pytest

@pytest.mark.dependency()
@pytest.mark.xfail(reason="deliberate fail")
def test_a():
    assert False

@pytest.mark.dependency()
def test_b():
    pass

#如果test_a 用例成功,test_c会执行
#如果test_a 用例失败,test_c会跳过
#depends=[] 列表里面加入依赖的测试用例名称
@pytest.mark.dependency(depends=["test_a"])
def test_c():
    pass

@pytest.mark.dependency(depends=["test_b"])
def test_d():
    pass

@pytest.mark.dependency(depends=["test_b", "test_c"])
def test_e():
    pass
#执行结果为:
test_dependency.py::test_a XFAIL (deliberate fail)                       [ 20%]
test_dependency.py::test_b PASSED                                        [ 40%]
test_dependency.py::test_c SKIPPED (test_c depends on test_a)            [ 60%]
Skipped: test_c depends on test_a

test_dependency.py::test_d PASSED                                        [ 80%]
test_dependency.py::test_e SKIPPED (test_e depends on test_c)            [100%]
Skipped: test_e depends on test_c

=================== 2 passed, 2 skipped, 1 xfailed in 0.07s ===================
pip install pytest-xdist 分布式并发执行测试用例

分布式并发执行测试用例原则:
	用例之间是独立的,用例之间没有依赖关系,用例可以完全独立运行
	用例执行没有顺序,随机顺序都能正常执行
	每个用例都能重复运行,运行结果不影响其他用例
安装及运行:
	pip install pytest-xdist 
	pytest -n 3			#表示三个并发,运行所有用例的时间会降低
	#用例多时效果明显,多进程并发执行,同时支持allure
	

pytest.ini
	自定义测试用例的编写规则,
	要放在执行命令的同级目录,通常建议放在项目的根目录,执行的时候也要在项目的根目录下执行。
	
[pytest]
markers = add
          div
python_files= check_*  test_*				#自定义文件修改后可识别 check_*  test_* 开头的文件
python_classes = Check_* Test_*
python_functions = aaa_* test_*
addopts=-vs --alluredir=./result
norecursedirs = result logs datas test_demo*   运行时忽略某些文件夹
设计测试用例的原则
不建议测试用例设计顺序
让测试用例尽量的简单,

Menu: python pytest测试实战 4

数据驱动
测试数据的数据驱动,测试步骤不变,测试数据提出来 yaml
测试步骤的数据驱动
	这些数据可以从excel,csv,yaml,json远程数据库读取来的数据,yaml文件可以进行注释
	
测试步骤的数据驱动
	如果多条用例,执行用例时取yaml数据里面不同的数据进行执行
#calc.yml文件内容   路径为:datas/calc.yml
add:

  int1:
    - 1
    - 2
    - 5
  int2:
    - 2
    - 4
    - 6
  bigint: [100,200,300]
  float: [0.2,0.3,0.5]
  minus:
    - -1
    - -2
    - -3

div:

  int1:
    - 2
    - 2
    - 1
  int2:
    - 4
    - 4
    - 1
  bigint: [200,200,1]
  float: [1,2,0.5]
  minus:
    - -1
    - -2
    - -3
#add.yml文件内容
- add
- div
#测试步骤的数据驱动
# 测试文件
import pytest
import yaml

from pythoncode.calc import Calculator


with open('datas/calc.yml',encoding='utf-8') as f:		#运行编码展示有问题,读取文件也需要修改编码格式

    datas = yaml.safe_load(f)               #如果多条用例,执行用例时取yaml数据里面不同的数据进行执行
    addkeys = datas['add'].keys()           #使用add 数据  datas['add'].keys()多维数组
    addvalues = datas['add'].values()

    divkeys = datas['div'].keys()           #如果多条用例,执行用例时取yaml数据里面不同的数据进行执行
    divvalues = datas['div'].values()       #使用div 数据

def get_steps():
    with open('steps/add.yml') as f:
        steps = yaml.safe_load(f)
        print(steps)
        return steps

cal = Calculator()                          #实例化对象

#不同的用例使用不同的方法
def steps(a,b,result):
    steps1 = get_steps()
    for step in steps1:
        if step == 'add':
            assert result == cal.add(a,b)   #使用add方法
        elif step == 'div':
            assert result == cal.div(a,b)   #使用div方法

@pytest.mark.parametrize('a,b,result',addvalues,ids=addkeys)
def test_add(a,b,result):
    steps(a,b,result)

class TestCalc:

    # setup_class, teardown_class每个类里面 执行前后分别 执行
    def setup_class(self):
        self.cal = Calculator()                     #在类里面实例化,类里面需要使用对象时一定要在类里面实例化
        print("类级别 setup")

    def teardown_class(self):
        print("类级别 teardown")

    # @pytest.mark.parametrize('a,b,result',
    #                          [[1, 2, 5],
    #                           [2, 4, 6],
    #                           [100, 200, 300],
    #                           [0.2, 0.3, 0.5],
    #                           [-1, -2, -3]],ids=['int1','int2','bigint','float','负数'])      #ids参数增加可读性,给测试的数据取别名,别名也可以在allure中进行展示

    @pytest.mark.parametrize('a,b,result',addvalues,ids=addkeys)   #通过yaml文件读取上面注释内容,使用yaml文件add数据
    def test_add2(self,a,b,result):                                #类里面方法要使用self
        steps(a,b,result)


    @pytest.mark.parametrize('a,b,result', divvalues, ids=divkeys) #通过yaml文件读取上面注释内容,使用yaml文件div数据
    def test_div(self, a, b, result):
        assert result == self.cal.div(a, b)

#注意事项:yml文件编写
以下为示例

#@pytest.mark.parametrize('a,b,result',[
#         [1,2,3],
#         [100,200,300],
#         [0.01,0.03,0.04],
#         [-1,-2,-3]],ids = ['int1','bigint','float','minus'])
			
			
add:
  -
    - 1
    - 2
    - 3
  -
    - 100
    - 200
    - 300
  - [0.01,0.02,0.03]
  - [-1,-2,-3]
  
div:
  -
    - 1
    - 2
    - 3
  -
    - 100
    - 200
    - 300
  - [0.01,0.02,0.03]
  - [-1,-2,-3]
  
#结果为:            #没有key  'list' object has no attribute 'keys'
{
      add: 
   [ [ 1, 2, 3 ],
     [ 100, 200, 300 ],
     [ 0.01, 0.02, 0.03 ],
     [ -1, -2, -3 ] ],
  div: 
   [ [ 1, 2, 3 ],
     [ 100, 200, 300 ],
     [ 0.01, 0.02, 0.03 ],
     [ -1, -2, -3 ] ] }
	 
=================================================
add:
  int:
    - 1
    - 2
    - 3
  bigint:
    - 100
    - 200
    - 300
  float: [0.01,0.02,0.03]
  minus: [-1,-2,-3]
  
div:
  int:
    - 1
    - 2
    - 3
  bigint:
    - 100
    - 200
    - 300
  float: [0.01,0.02,0.03]
  minus: [-1,-2,-3]

#结果为:
{
      add: 
   {
      int: [ 1, 2, 3 ],
     bigint: [ 100, 200, 300 ],
     float: [ 0.01, 0.02, 0.03 ],
     minus: [ -1, -2, -3 ] },
  div: 
   {
      int: [ 1, 2, 3 ],
     bigint: [ 100, 200, 300 ],
     float: [ 0.01, 0.02, 0.03 ],
     minus: [ -1, -2, -3 ] } }
	 
	 
===============================================
int:
  - 1
  - 2
  - 3
bigint:
  - 100
  - 200
  - 300
float: [0.01,0.02,0.03]
minus: [-1,-2,-3]

#结果为:
{
      int: [ 1, 2, 3 ],
  bigint: [ 100, 200, 300 ],
  float: [ 0.01, 0.02, 0.03 ],
  minus: [ -1, -2, -3 ] }
Pytest高级用法
Pytest插件:
	插件可以改变pytest行为,可用的hook函数有很多
	https://docs.pytest.org/en/latest/_modules/_pytest/hookspec.html
Pytest插件加载方式:
内置plugin:
	从代码内部的_pytest目录加载				        #内部目录加载
外部插件(第三方插件):
	通过setuptools entry points机制发现的第三方插件   #前面介绍过第三方插件
	
conftest.py存放的本地插件:(重点)				#通过conftest.py文件自定义
	自动模块发现机制
pytest --trace-config 查看当前pytest中所有的plugin(带有hook方法的文件)

hook(钩子)函数定制和扩展插件                  #重点
conftest.py:本地的插件库,存放fixture函数或者hook函数作用于该文件所在的目录及其所有的子目录
#hook(钩子)函数定制和扩展插件  
Pytest编写自己的插件(1)
pytest_collection_modifyitems 收集上来的测试用例实现定制化功能
解决问题:
	自定义用例的执行顺序
	解决编码问题(中文的测试用例名称)
	自动添加标签
#hook钩子函数官方文档链接:https://docs.pytest.org/en/latest/_modules/_pytest/hookspec.html
#conftest.py文件 使用钩子函数定制和扩展插件
#conftest.py文件内容:
def pytest_collection_modifyitems(
    session: "Session", config: "Config", items: List["Item"]
) -> None:
    print(items)        #查看items测试用例数据
    print(len(items))
    #倒序执行 items里面的测试用例
    items.reverse()		#自定义用例的执行顺序

    #含有中文的测试用例名称,改写编码格式  #conftest.py文件通过hook函数修改编码后仍然展示错误,需要修改读取文件编码格式
    for item in items:          #循环遍历,针对items下面的每一条用例
        item.name = item.name.encode('utf-8').decode('unicode-escape')
        item._nodeid = item.nodeid.encode('utf-8').decode('unicode-escape')
        #给用例添加mark标签
        if 'add' in item.nodeid:             #nodeid是整个用例名称
            item.add_marker(pytest.mark.add)    #如果用例名称包含add,则给该条用例加上add标签
        elif 'div' in item.nodeid:
            item.add_marker(pytest.mark.div)    #如果用例名称包含div,则给该条用例加上div标签

用例失败重跑

#用例失败重跑
用例失败重跑,在命令行执行  #pytest帮助文档命令参数
--lf, --last-failed 只重新运行上次运行失败的用例(或如果没有失败的话会全部跑)
--ff, --failed-first 运行所有测试,但首先运行上次运行失败的测试(这可能会重新测试,从而导致重复的fixture setup/teardown)
Pytest编写自己的插件(2)
pytest_addoption为pytest命令行增加自定义参数,每个pytest  到这个hook方法
解决问题:
	命令行添加一个参数
	
#不同环境获取不同的测试数据	
parser: 用户命令行参数与ini文件值的解析器
#命令行添加一个参数

###处理命令后传来的参数,设置成fixture,将test环境、dev环境或者其他环境分别处理,获取想要的不同环境下的测试数据###

def pytest_addoption(parser):
    mygroup = parser.getgroup("hogwarts")     #group 将下面所有的 option都展示在这个group下。
    mygroup.addoption("--env",    #注册一个命令行选项
                      default='test',			#默认参数是test环境
                      dest='env',
                      help='set your run env'	#展示说明
                      )
	#上面内容设定后在conftest同级目录命令行运行pytest --help可查看自定义的命令				  
					  
@pytest.fixture(scope='session')
def cmdoption(request):				#解析参数
    return request.config.getoption("--env", default='test')
#conftest文件内容:
#不同环境获取不同的测试数据
#命令行添加一个参数
#parser: 用户命令行参数与ini文件值的解析器
###处理命令后传来的参数,设置成fixture,将test环境、dev环境或者其他环境分别处理,获取想要的不同环境下的测试数据###
def pytest_addoption(parser):
   mygroup = parser.getgroup("hogwarts")  # group 将下面所有的 option都展示在这个group下。
   mygroup.addoption("--env",  # 注册一个命令行选项
                     default='test',  # 默认参数是test环境
                     dest='env',
                     help='set your run env'  # 展示说明
                     )
'''
上面内容设定后在conftest同级目录命令行运行pytest --help可查看自定义的命令
'''

@pytest.fixture(scope='session')
def cmdoption(request):  # 解析参数
   myenv = request.config.getoption("--env", default='test')
   if myenv == 'test':
       datapath = 'datas/test/data.yml'

   elif myenv == 'dev':
       datapath = 'datas/dev/data.yml'
   with open(datapath,encoding='utf-8') as f:
       datas = yaml.safe_load(f)

   port = datas['env']['port']     #yml数据为字典,通过字典的方式取值即可
   ip = datas['env']['ip']
   return port,ip

#测试文件test_env.py文件内容

# 测试用例通过传入 fixture方法,获取 测试数据/ 开发数据
def test_case(cmdoption):
   print("测试环境验证")
   port, ip = cmdoption
   print(f"环境ip : {ip} , port:{port}")
   url = 'http://' + str(ip) + ":" + str(port)     #int类型需要转换为字符串进行拼接
   # requests.get(url)
   print(url)

#执行结果为:
测试环境验证
环境ip : 10.10.2.1 , port:8888
http://10.10.2.1:8888
Pytest编写自己的插件(3)

pytest_generate_tests 可以实现自定义动态参数化方案或者扩展
解决问题:参数化简化
重写hook函数
	def pytest_generate_tests(metafunc:"Metafunc") -> None:
		if "param" in metafunc.fixturenames:				#测试数据需要用到该函数时,需要注意参数名称和conftest.py里面的参数param保持一致
			metafunc.parametrize("param",metafunc.module.par_to_test,	# 测试数据要与par_to_test名称保持一致
			ids=metafunc.module.case,scope='function')					# 测试数据要与case名称保持一致
#conftest文件内容:
#通过 方法动态的生成测试用例
def pytest_generate_tests(metafunc:"Metafunc") -> None:
    if "param" in metafunc.fixturenames:
        metafunc.parametrize("param",metafunc.module.mydatas, #metafunc.module.datas 测试数据要与mydatas名称保持一致
        ids=metafunc.module.myids,scope='function')         #metafunc.module.myids 测试数据要与myids名称保持一致

#test_param文件内容:
# mydatas = [[1,2,3],[0.1,0.2,0.3]]
# myids = ['整数','浮点']
import yaml

with open('datas/param.yml',encoding='utf-8') as f: #使用'utf-8'让编译器识别中文
    datas = yaml.safe_load(f)
    # myids 和mydatas 要与conftest.py 勾子函数里面的
    # metafunc.module.mydatas, ids=metafunc.module.myids 保持一致
    mydatas = datas.values()
    myids = datas.keys()

def test_params(param):             #测试数据需要用到该函数时,需要注意参数名称和conftest.py里面的参数param保持一致
    print("参数化简化,动态生成测试用例")
    print(param)

#datas/param.yml文件内容:
# mydatas = [[1,2,3],[0.1,0.2,0.3]]
# myids = ['整数','浮点']
整数:
  -
    - 1
    - 2
    - 3
浮点数:
  -
    - 0.1
    - 0.2
    - 0.3
#执行结果为:
参数化简化,动态生成测试用例
[[0.1, 0.2, 0.3]]
参数化简化,动态生成测试用例
[[1, 2, 3]]
#判断小数
#场景:
浮点型进行除、减的算法时,比如结果是3.5,断言结果是3.50000000004
处理规则:
	1.decimal 结果一定要和decimal进行比较

	2.hamcrest close_to

你可能感兴趣的:(Python编程语言与测试框架)