pytest学习纪要123 Marathon

pytest123

本文主要参考:https://www.cnblogs.com/yoyoketang/tag/pytest

如有侵权,请站内联系我

目录 pytest1231. setup和teardown2.fixture3.conftest.py4.生成HTML报告5. assert 断言6.skip7.函数传参/fixture传参8.失败标记xfail9.fixture避免每次传参10.fixture进阶11.fixture作用范围12.conftest.py作用范围13.--lf --ff运行上次失败的用例14.重复执行用例15.pytest-assume断言失败继续执行16.pytest-ordering 自定义用例的执行顺序17.fixture参数化params

pytest和unittest的对比

https://www.cnblogs.com/ella-li/p/11731784.html

一、用例编写规则

1.使用unittest编写测试用例必须遵循以下规则:

  1.必须首先 导入 import unittest

  2.测试类必须要继承 unittest.TestCase

  3.测试方法必须以test_kai开头

2.pyest是Python的第三方测试框架,是基于unittest的扩展框架,比unittest更简洁高效,使用pytest编写测试用例必须遵循以下规则:

  1.测试文件必须以test开头或者_test结尾

  2.测试方法需以test开头

  3.测试类必须以Test开头

二、前置跟后置

  1.unittest提供了setUp/tearDown,每个用例运行前、结束后运行一次。setUpClass和tearDownClass,用例执行前、结束后,只运行一次。

  2.pyets 可以在函数前使用@pytest.fixture()装饰器,fixture使用范围可以是:function(函数级别)、class、module(模块级别)、package(包级别)、session(多个测试类可以共用一个session)

  优势:

  1.fixure命名更加灵活,局限性比较小

  2.conftest.py 配置里可以实现数据共享,不需要import就能自动找到一些配置,可供多个py文件调用。

  3.scope="session" 以实现多个.py跨文件使用一个session来完成多个用例

三、断言

  1.unittest提供了assertEqual、assertIn、assertTrue、assertFalse等

  2.pytest直接在assert 后面接表达式

四、测试报告

  1.unittest使用HTMLTestRunnerNew库

  2.pytest有pytest-HTML、allure插件。

五、失败重跑

  1.unittest无此功能

  2.pytest支持

六、参数化

  1.unittest需要依赖于ddt库

  2.pytest直接使用@pytest.mark.parametrize装饰器

pytest使用须知

https://www.cnblogs.com/ella-li/p/11731775.html

一、pytest简洁和好处

  • 自动发现测试用例 testloader
  • 断言方便 ,自定义错误提示 assert 正则匹配
  • 灵活运行指定的测试用例,指定模块,制定测试类,测试用例 -k 标签化,回归 正向 冒烟 登陆
  • 环境管理灵活。
  • 丰富的插件,测试报告,插件介绍 allure(比较成熟的测试报告体系,unittest不支持这个插件)
  • 和unittest / nose兼容,pytest中标签、生成allure、分析最慢的测试用例和unittest兼容

unittest的好处:

1.标准库,兼容性好

2.更加容易理解

pytest和unittest兼容是有条件的,pytest使用以下功能就不能和unittes兼容:

  • parametrize
  • fixture(autouse=True的可以) 不能和setUp同时使用
  • Custom hooks

二、自动发现测试用例原理,命名方式

  • 文件名必须以test*.py 和 *test.py开头或结尾,
  • 如果有类名,必须以Test开头,没有init函数,测试方法名以test_开头的函数
  • 没有类,那么以函数为单位的函数名必须以test_开头

三、断言: 自定义提示文案

assert 1==1, "提示文案"

更多点击参考源...

  1. setup和teardown

基于模块的:

setup_module,teardown_module

模块测试前运行setup_module,结束前运行teardown_module

基于函数用例

setup_function,teardown_function

每个用例开始和结束调用一次

基于类和方法

setup_class,teardown_class

所有用例执行之前,所有用例执行之后

setup_method,teardown_method

每个方法用例开始前执行,结束后执行

运行的优先级:setup_class》setup_method》setup 》用例》teardown》teardown_method》teardown_class

备注:这里setup_method和teardown_method的功能和setup/teardown功能是一样的,一般二者用其中一个即可

setup_module/teardown_module的优先级是最大的,
然后函数里面用到的setup_function/teardown_function与类里面的setup_class/teardown_class互不干涉

2.fixture

实现以下场景:

用例1需要先登录,用例2不需要登录,用例3需要先登录。很显然这就无法用setup和teardown来实现了。这就是**本篇学习的目的,自定义测试用例的预置条件

1.firture相对于setup和teardown来说应该有以下几点优势

  • 命名方式灵活,不局限于setup和teardown这几个命名
  • conftest.py 配置里可以实现数据共享,不需要import就能自动找到一些配置
  • scope="module" 可以实现多个.py跨文件共享前置, 每一个.py文件调用一次
  • scope="session" 以实现多个.py跨文件使用一个session来完成多个用例

Fixtures可以选择使用yield语句为测试函数提供它们的值,而不是return。 在这种情况下,yield语句之后的代码块作为拆卸代码执行,而不管测试结果如何。fixture功能必须只产生一次

实现场景:用例1需要先登录,用例2不需要登录,用例3需要先登录

import pytest

# 不带参数时默认scope="function"
@pytest.fixture()
def login():
    print("输入账号,密码先登录")

def test_s1(login):
    print("用例1:登录之后其它动作111")

def test_s2():  # 不传login
    print("用例2:不需要登录,操作222")

def test_s3(login):
    print("用例3:登录之后其它动作333")

if __name__ == "__main__":
    pytest.main(["-s", "test_fix.py"])

2.如果@pytest.fixture()里面没有参数,那么默认scope="function",也就是此时的级别的function,针对函数有效

3.conftest.py

1.上面一个案例是在同一个.py文件中,多个用例调用一个登陆功能,如果有多个.py的文件都需要调用这个登陆功能的话,那就不能把登陆写到用例里面去了。

此时应该要有一个配置文件,单独管理一些预置的操作场景,pytest里面默认读取conftest.py里面的配置

conftest.py配置需要注意以下点:

  • conftest.py配置脚本名称是固定的,不能改名称
  • conftest.py与运行的用例要在同一个pakage下,并且有init.py文件
  • 不需要import导入 conftest.py,pytest用例会自动查找

2.参考脚本代码设计如下

pkg代码结构:
__init__.py

conftest.py
    # coding:utf-8
    import pytest

    @pytest.fixture()
    def login():
        print("输入账号,密码先登录")

test_fix1.py
    # coding:utf-8
    import pytest
    
    def test_s1(login):
        print("用例1:登录之后其它动作111")
    
    def test_s2():  # 不传login
        print("用例2:不需要登录,操作222")
    
    def test_s3(login):
        print("用例3:登录之后其它动作333")
    
    if __name__ == "__main__":
        pytest.main(["-s", "test_fix1.py"])

test_fix2.py
    # coding:utf-8
    import pytest
    
    def test_s4(login):
        print("用例4:登录之后其它动作111")
    
    def test_s5():  # 不传login
        print("用例5:不需要登录,操作222")
    
    if __name__ == "__main__":
        pytest.main(["-s", "test_fix2.py"])

3.单独运行test_fix1.py和test_fix2.py都能调用到login()方法,这样就能实现一些公共的操作可以单独拿出来了

fixture之yield实现teardown

4.生成HTML报告

安装pytest-html

支持python2.7 3.6

pip install pytest-html

测试并生成报告

cd到需要执行pytest用例的目录,
执行指令:
pytest --html=report.html
# 不指定文件下,默认测试目录下的所有的用例,
# 如果指定某用例文件:
pytest  test_fix.py [other test_*.py ...]--html=report.html
可以指定生成报告存储路径
pytest --html=./xxx/report.html
# 默认同测试目录下
  1. assert 断言

断言是写自动化测试基本最重要的一步,简单来讲就是实际结果和期望结果去对比,符合预期那就测试pass,不符合预期那就测试 failed。

pytest允许使用标准Python断言来验证Python测试中的期望和值。例如,可以写:

# content of test_assert1.py
def f():
    return 3
def test_function():
    assert f() == 4

断言f()函数的返回值,接下来会看到断言失败,因为返回的值是3,判断等于4,所以失败了

$ pytest test_assert1.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile:
collected 1 item
test_assert1.py F [100%]
================================= FAILURES =================================
______________________________ test_function _______________________________
def test_function():
> assert f() == 4
E assert 3 == 4
E + where 3 = f()
test_assert1.py:5: AssertionError
========================= 1 failed in 0.12 seconds =========================

从报错信息可以看到断言失败原因:E assert 3 == 4

异常信息

接下来再看一个案例,如果想在异常的时候,输出一些提示信息,这样报错后,就方便查看是什么原因了

def f():
    return 3

def test_function():

    a = f()
    assert a % 2 == 0, "判断a为偶数,当前a的值为:%s"%a

运行结果

================================== FAILURES ===================================
________________________________ test_function ________________________________

    def test_function():
    
        a = f()
>       assert a % 2 == 0, "判断a为偶数,当前a的值为:%s"%a
E       AssertionError: 判断a为偶数,当前a的值为:3
E       assert (3 % 2) == 0

test_03.py:9: AssertionError
========================== 1 failed in 0.18 seconds ===========================

这样当断言失败的时候,会给出自己写的失败原因了E AssertionError: 判断a为偶数,当前a的值为:3

异常断言

为了写关于引发异常的断言,可以使用pytest.raises作为上下文管理器,如下

# content of test_assert1.py

import pytest
def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

运行结果

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\YOYO\canshuhua, inifile:
plugins: metadata-1.7.0, html-1.19.0
collected 1 item

test_assert1.py.

========================== 1 passed in 0.31 seconds ===========================

如果我们要断言它抛的异常是不是预期的,比如执行:1/0,预期结果是抛异常:ZeroDivisionError: division by zero,那我们要断言这个异常,通常是断言异常的type和value值了。

这里1/0的异常类型是ZeroDivisionError,异常的value值是division by zero,于是用例可以这样设计

# content of test_assert1.py

# ** 作者:上海-悠悠 QQ交流群:588402570**

import pytest
def test_zero_division():
    '''断言异常'''
    with pytest.raises(ZeroDivisionError) as excinfo:
        1 / 0

    # 断言异常类型type
    assert excinfo.type == ZeroDivisionError
    # 断言异常value值
    assert "division by zero" in str(excinfo.value)

excinfo 是一个异常信息实例,它是围绕实际引发的异常的包装器。主要属性是.type、 .value 和 .traceback

注意:断言type的时候,异常类型是不需要加引号的,断言value值的时候需转str

在上下文管理器窗体中,可以使用关键字参数消息指定自定义失败消息:

with pytest.raises(ZeroDivisionError, message="Expecting ZeroDivisionError"):
    pass

结果:Failed: Expecting ZeroDivisionError

常用断言

pytest里面断言实际上就是python里面的assert断言方法,常用的有以下几种

  • assert xx 判断xx为真

  • assert not xx 判断xx不为真

  • assert a in b 判断b包含a

  • assert a == b 判断a等于b

  • assert a != b 判断a不等于b

    import pytest

    ** 作者:上海-悠悠 QQ交流群:588402570**

    def is_true(a):
    if a > 0:
    return True
    else:
    return False

    def test_01():
    '''断言xx为真'''
    a = 5
    b = -1
    assert is_true(a)
    assert not is_true(b)

    def test_02():
    '''断言b 包含 a'''
    a = "hello"
    b = "hello world"
    assert a in b

    def test_03():
    '''断言相等'''
    a = "yoyo"
    b = "yoyo"
    assert a == b

    def test_04():
    '''断言不等于'''
    a = 5
    b = 6
    assert a != b

    if name == "main":
    pytest.main(["-s", "test_01.py"])

6.skip

pytest.mark.skip可以标记无法在某些平台上运行的测试功能,或者您希望失败的测试功能

skip意味着只有在满足某些条件时才希望测试通过,否则pytest应该跳过运行测试。 常见示例是在非Windows平台上跳过仅限Windows的测试,或跳过测试依赖于当前不可用的外部资源(例如数据库)。

xfail意味着您希望测试由于某种原因而失败。 一个常见的例子是对功能的测试尚未实施,或尚未修复的错误。 当测试通过时尽管预计会失败(标有pytest.mark.xfail),它是一个xpass,将在测试摘要中报告。

pytest计数并分别列出skip和xfail测试。 未显示有关跳过/ xfailed测试的详细信息默认情况下,以避免混乱输出。 您可以使用-r选项查看与“short”字母对应的详细信息显示在测试进度中

pytest -rxXs # show extra info on xfailed, xpassed, and skipped tests

有关-r选项的更多详细信息,请运行pytest -h

skip

跳过测试函数的最简单方法是使用跳过装饰器标记它,可以传递一个可选的原因

@pytest.mark.skip(reason="no way of currently testing this")
def test_the_unknown():
    ...

或者,也可以通过调用来在测试执行或设置期间强制跳过pytest.skip(reason)功能:

def test_function():
    if not valid_config():
        pytest.skip("unsupported configuration")

也可以使用pytest.skip(reason,allow_module_level = True)跳过整个模块级别:

import pytest
if not pytest.config.getoption("--custom-flag"):
    pytest.skip("--custom-flag is missing, skipping tests", allow_module_level=True)

当在导入时间内无法评估跳过条件时,命令性方法很有用。

skipif

如果您希望有条件地跳过某些内容,则可以使用skipif代替。 这是标记测试的示例在Python3.6之前的解释器上运行时要跳过的函数

import sys
@pytest.mark.skipif(sys.version_info < (3,6),
reason="requires python3.6 or higher")
def test_function():
    ...

如果条件在收集期间评估为True,则将跳过测试函数,具有指定的原因使用-rs时出现在摘要中。

您可以在模块之间共享skipif标记。参考以下案例

# content of test_mymodule.py
import mymodule
minversion = pytest.mark.skipif(mymodule.__versioninfo__ < (1,1),
reason="at least mymodule-1.1 required")
@minversion
def test_function():
    ...

您可以导入标记并在另一个测试模块中重复使用它:

# test_myothermodule.py
from test_mymodule import minversion
@minversion
def test_anotherfunction():
    ...

对于较大的测试套件,通常最好有一个文件来定义标记,然后一致适用于整个测试套件。

或者,您可以使用条件字符串而不是布尔值,但它们之间不能轻易共享它们支持它们主要是出于向后兼容的原因

skip类或模块

您可以在类上使用skipif标记(与任何其他标记一样):

@pytest.mark.skipif(sys.platform == 'win32',
reason="does not run on windows")
class TestPosixCalls(object):
    def test_function(self):
        "will not be setup or run under 'win32' platform"

如果条件为True,则此标记将为该类的每个测试方法生成跳过结果

警告:强烈建议不要在使用继承的类上使用skipif。 pytest中的一个已知错误标记可能会导致超类中的意外行为。

如果要跳过模块的所有测试功能,可以在全局级别使用pytestmark名称

# test_module.py
pytestmark = pytest.mark.skipif(...)

如果将多个skipif装饰器应用于测试函数,则如果任何跳过条件为真,则将跳过它

skip文件或目录

有时您可能需要跳过整个文件或目录,例如,如果测试依赖于特定于Python的版本功能或包含您不希望pytest运行的代码。 在这种情况下,您必须排除文件和目录来自收藏。 有关更多信息,请参阅自定义测试集合。

skip缺少导入依赖项

您可以在模块级别或测试或测试设置功能中使用以下帮助程序

docutils = pytest.importorskip("docutils")

如果无法在此处导入docutils,则会导致测试跳过结果。 你也可以跳过库的版本号

docutils = pytest.importorskip("docutils", minversion="0.3")

将从指定模块的version属性中读取版本。

概要

这是一个快速指南,介绍如何在不同情况下跳过模块中的测试

1.无条件地跳过模块中的所有测试:

pytestmark = pytest.mark.skip("all tests still WIP")

2.根据某些条件跳过模块中的所有测试

pytestmark = pytest.mark.skipif(sys.platform == "win32", "tests for linux

˓→ only"

3.如果缺少某些导入,则跳过模块中的所有测试

pexpect = pytest.importorskip("pexpect")

7.函数传参/fixture传参

前言

为了提高代码的复用性,我们在写用例的时候,会用到函数,然后不同的用例去调用这个函数。

比如登录操作,大部分的用例都会先登录,那就需要把登录单独抽出来写个函数,其它用例全部的调用这个登陆函数就行。

但是登录的账号不能写死,有时候我想用账号1去登录,执行用例1,用账号2去登录执行用例2,所以需要对函数传参。

登录函数传参

把登录单独出来,写一个函数,传2个参数user和psw,写用例的时候调用登录函数,输入几组user,psw参数化登录用例

测试用例传参需要用装饰器@pytest.mark.parametrize,里面写两个参数

  • 第一个参数是字符串,多个参数中间用逗号隔开

  • 第二个参数是list,多组数据用元祖类型

    test_01.py

    coding:utf-8

    import pytest

    ** 作者:上海-悠悠 QQ交流群:588402570**

    测试登录数据

    test_login_data = [("admin", "111111"), ("admin", "")]

    def login(user, psw):
    '''普通登录函数'''
    print("登录账户:%s"%user)
    print("登录密码:%s"%psw)
    if psw:
    return True
    else:
    return False

    @pytest.mark.parametrize("user, psw", test_login_data)
    def test_login(user, psw):
    '''登录用例'''
    result = login(user, psw)
    assert result == True, "失败原因:密码为空"

    if name == "main":
    pytest.main(["-s", "test_01.py"])

运行结果

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\, inifile:
plugins: metadata-1.7.0, html-1.19.0, allure-adaptor-1.7.10
collected 2 items

..\..\..\..\..\..\YOYO\marktest\test_01.py 登录账户:admin
登录密码:111111
.登录账户:admin
登录密码:
F
user = 'admin', psw = ''

    @pytest.mark.parametrize("user, psw", [("admin", "111111"), ("admin", "")])
    def test_01(user, psw):
        result = login(user, psw)
>       assert result == True
E       assert False == True

D:\YOYO\marktest\test_01.py:18: AssertionError


================================== FAILURES ===================================
_______________________________ test_01[admin-] _______________________________

user = 'admin', psw = ''

    @pytest.mark.parametrize("user, psw", [("admin", "111111"), ("admin", "")])
    def test_01(user, psw):
        result = login(user, psw)
>       assert result == True
E       assert False == True

D:\YOYO\marktest\test_01.py:18: AssertionError
===================== 1 failed, 1 passed in 0.05 seconds ======================

从结果可以看出,有2个用例,一个测试通过,一个测试失败了,互不影响

request参数

如果想把登录操作放到前置操作里,也就是用到@pytest.fixture装饰器,传参就用默认的request参数

user = request.param 这一步是接收传入的参数,本案例是传一个参数情况

# test_02.py
# coding:utf-8
import pytest

#** 作者:上海-悠悠 QQ交流群:588402570**

# 测试账号数据
test_user_data = ["admin1", "admin2"]

@pytest.fixture(scope="module")
def login(request):
    user = request.param
    print("登录账户:%s"%user)
    return user

@pytest.mark.parametrize("login", test_user_data, indirect=True)
def test_login(login):
    '''登录用例'''
    a = login
    print("测试用例中login的返回值:%s" % a)
    assert a != ""


if __name__ == "__main__":
    pytest.main(["-s", "test_02.py"])

运行结果:

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\, inifile:
plugins: metadata-1.7.0, html-1.19.0, allure-adaptor-1.7.10
collected 2 items

..\..\..\..\..\..\YOYO\marktest\test_02.py 登录账户:admin1
测试用例中login的返回值:admin1
.登录账户:admin2
测试用例中login的返回值:admin2
.

========================== 2 passed in 0.01 seconds ===========================

添加indirect=True参数是为了把login当成一个函数去执行,而不是一个参数

request传2个参数

如果用到@pytest.fixture,里面用2个参数情况,可以把多个参数用一个字典去存储,这样最终还是只传一个参数

不同的参数再从字典里面取对应key值就行,如: user = request.param["user"]

# test_03.py
# coding:utf-8
import pytest

# ** 作者:上海-悠悠 QQ交流群:588402570**

# 测试账号数据
test_user_data = [{"user": "admin1", "psw": "111111"},
                  {"user": "admin1", "psw": ""}]

@pytest.fixture(scope="module")
def login(request):
    user = request.param["user"]
    psw = request.param["psw"]
    print("登录账户:%s" % user)
    print("登录密码:%s" % psw)
    if psw:
        return True
    else:
        return False

# indirect=True 声明login是个函数
@pytest.mark.parametrize("login", test_user_data, indirect=True)
def test_login(login):
    '''登录用例'''
    a = login
    print("测试用例中login的返回值:%s" % a)
    assert a, "失败原因:密码为空"


if __name__ == "__main__":
    pytest.main(["-s", "test_03.py"])

运行结果

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\, inifile:
plugins: metadata-1.7.0, html-1.19.0, allure-adaptor-1.7.10
collected 2 items

..\..\..\..\..\..\YOYO\marktest\test_03.py 登录账户:admin1
登录密码:111111
测试用例中login的返回值:True
.登录账户:admin1
登录密码:
测试用例中login的返回值:False
F
login = False

    @pytest.mark.parametrize("login", test_user_data, indirect=True)
    def test_login(login):
        '''登录用例'''
        a = login
        print("测试用例中login的返回值:%s" % a)
>       assert a, "失败原因:密码为空"
E       AssertionError: 失败原因:密码为空
E       assert False

D:\YOYO\marktest\test_03.py:25: AssertionError


================================== FAILURES ===================================
_____________________________ test_login[login1] ______________________________

login = False

    @pytest.mark.parametrize("login", test_user_data, indirect=True)
    def test_login(login):
        '''登录用例'''
        a = login
        print("测试用例中login的返回值:%s" % a)
>       assert a, "失败原因:密码为空"
E       AssertionError: 失败原因:密码为空
E       assert False

D:\YOYO\marktest\test_03.py:25: AssertionError
===================== 1 failed, 1 passed in 0.05 seconds ======================

如果要用到login里面的返回值,def test_login(login)时,传入login参数,函数返回值就是login了

多个fixtrue

用例上面是可以同时放多个fixture的,也就是多个前置操作,可以支持装饰器叠加,使用parametrize装饰器叠加时,用例组合是2个参数个数相乘

# test_04.py
# ** 作者:上海-悠悠 QQ交流群:588402570**
# coding:utf-8
import pytest

# 测试账号数据
test_user = ["admin1", "admin2"]
test_psw = ["11111", "22222"]


@pytest.fixture(scope="module")
def input_user(request):
    user = request.param
    print("登录账户:%s" % user)
    return user


@pytest.fixture(scope="module")
def input_psw(request):
    psw = request.param
    print("登录密码:%s" % psw)
    return psw



@pytest.mark.parametrize("input_user", test_user, indirect=True)
@pytest.mark.parametrize("input_psw", test_psw, indirect=True)
def test_login(input_user, input_psw):
    '''登录用例'''
    a = input_user
    b = input_psw
    print("测试数据a-> %s, b-> %s" % (a,b))
    assert b

if __name__ == "__main__":
    pytest.main(["-s", "test_04.py"])

运行结果

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: E:\YOYO\par, inifile:
plugins: metadata-1.7.0, html-1.19.0
collected 4 items

test_04.py 登录账户:admin1
登录密码:11111
测试数据a-> admin1, b-> 11111
.登录账户:admin2
测试数据a-> admin2, b-> 11111
.登录密码:22222
测试数据a-> admin2, b-> 22222
.登录账户:admin1
测试数据a-> admin1, b-> 22222
.

========================== 4 passed in 0.05 seconds ===========================

如果参数user有2个数据,参数psw有2个数据,那么组合起来的案例是两个相乘,也就是组合2*2 = 4个用例


pytest可以支持自定义标记,自定义标记可以把一个web项目划分多个模块,然后指定模块名称执行。一个大项目自动化用例时,可以划分多个模块,

也可以使用标记功能,标明哪些是模块1用例,哪些是模块2的,运行代码时候指定mark名称运行就可以

mark标记

1.以下用例,标记test_send_http()为webtest

# content of test_server.py

import pytest

@pytest.mark.webtest
def test_send_http():
    pass # perform some webtest test for your app

def test_something_quick():
    pass

def test_another():
    pass

class TestClass:
    def test_method(self):
        pass

if __name__ == "__main__":
    pytest.main(["-s", "test_server.py", "-m=[not]webtest"])

只运行用webtest标记的测试,cmd运行的时候,加个-m 参数,指定参数值webtest


8.失败标记xfail

前言

当用例a失败的时候,如果用例b和用例c都是依赖于第一个用例的结果,那可以直接跳过用例b和c的测试,直接给他标记失败xfail

用到的场景,登录是第一个用例,登录之后的操作b是第二个用例,登录之后操作c是第三个用例,很明显三个用例都会走到登录。

如果登录都失败了,那后面2个用例就没测试必要了,直接跳过,并且标记为失败用例,这样可以节省用例时间。

用例设计

1.pytest里面用xfail标记用例为失败的用例,可以直接跳过。实现基本思路

  • 把登录写为前置操作

  • 对登录的账户和密码参数化,参数用canshu = [{"user":"amdin", "psw":"111"}]表示

  • 多个用例放到一个Test_xx的class里

  • test_01,test_02, test_03全部调用fixture里面的login功能

  • test_01测试登录用例

  • test_02和test_03执行前用if判断登录的结果,登录失败就执行,pytest.xfail("登录不成功, 标记为xfail")

    content of test_05.py

    coding:utf-8

    import pytest

    ** 作者:上海-悠悠 QQ交流群:588402570**

    canshu = [{"user":"amdin", "psw":"111"}]

    @pytest.fixture(scope="module")
    def login(request):
    user = request.param["user"]
    psw = request.param["psw"]
    print("正在操作登录,账号:%s, 密码:%s" % (user, psw))
    if psw:
    return True
    else:
    return False

    @pytest.mark.parametrize("login", canshu, indirect=True)
    class Test_xx():

      def test_01(self, login):
          '''用例1登录'''
          result = login
          print("用例1:%s" % result)
          assert result == True
    
    
      def test_02(self, login):
          result = login
          print("用例3,登录结果:%s" % result)
          if not result:
              pytest.xfail("登录不成功, 标记为xfail")
    
          assert 1 == 1
    
      def test_03(self, login):
          result = login
          print("用例3,登录结果:%s" %result)
          if not result:
              pytest.xfail("登录不成功, 标记为xfail")
    
          assert 1 == 1
    

    if name == "main":
    pytest.main(["-s", "test_05.py"])

上面传的登录参数是登录成功的案例,三个用例全部通过

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\, inifile:
plugins: metadata-1.7.0, html-1.19.0, allure-adaptor-1.7.10
collected 3 items

..\..\..\..\..\..\YOYO\peizhi\test_05.py 正在操作登录,账号:amdin, 密码:111
用例1:True
.用例3,登录结果:True
.用例3,登录结果:True
.

========================== 3 passed in 0.02 seconds ===========================

标记为xfail

1.再看看登录失败情况的用例,修改登录的参数

# content of test_05.py
# coding:utf-8
import pytest

# ** 作者:上海-悠悠 QQ交流群:588402570**

canshu = [{"user":"amdin", "psw":""}]

@pytest.fixture(scope="module")
def login(request):
    user = request.param["user"]
    psw = request.param["psw"]
    print("正在操作登录,账号:%s, 密码:%s" % (user, psw))
    if psw:
        return True
    else:
        return False


@pytest.mark.parametrize("login", canshu, indirect=True)
class Test_xx():

    def test_01(self, login):
        '''用例1登录'''
        result = login
        print("用例1:%s" % result)
        assert result == True


    def test_02(self, login):
        result = login
        print("用例3,登录结果:%s" % result)
        if not result:
            pytest.xfail("登录不成功, 标记为xfail")

        assert 1 == 1

    def test_03(self, login):
        result = login
        print("用例3,登录结果:%s" %result)
        if not result:
            pytest.xfail("登录不成功, 标记为xfail")

        assert 1 == 1


if __name__ == "__main__":
    pytest.main(["-s", "test_05.py"])

运行结果

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\, inifile:
plugins: metadata-1.7.0, html-1.19.0, allure-adaptor-1.7.10
collected 3 items

..\..\..\..\..\..\YOYO\peizhi\test_05.py 正在操作登录,账号:amdin, 密码:
用例1:False
F
self = <YOYO.peizhi.test_05.Test_xx object at 0x00000000045ACF98>, login = False

    def test_01(self, login):
        '''用例1登录'''
        result = login
        print("用例1:%s" % result)
>       assert result == True
E       assert False == True

D:\YOYO\peizhi\test_05.py:24: AssertionError
用例3,登录结果:False
x
Test ignored.用例3,登录结果:False
x
Test ignored.

================================== FAILURES ===================================
___________________________ Test_xx.test_01[login0] ___________________________

self = <YOYO.peizhi.test_05.Test_xx object at 0x00000000045ACF98>, login = False

    def test_01(self, login):
        '''用例1登录'''
        result = login
        print("用例1:%s" % result)
>       assert result == True
E       assert False == True

D:\YOYO\peizhi\test_05.py:24: AssertionError
===================== 1 failed, 2 xfailed in 0.06 seconds =====================

从结果可以看出用例1失败了,用例2和3没执行,直接标记为xfail了


9.fixture避免每次传参

前言

平常写自动化用例会写一些前置的fixture操作,用例需要用到就直接传该函数的参数名称就行了。当用例很多的时候,每次都传这个参数,会比较麻烦。

fixture里面有个参数autouse,默认是Fasle没开启的,可以设置为True开启自动使用fixture功能,这样用例就不用每次都去传参了

调用fixture三种方法

  • 1.函数或类里面方法直接传fixture的函数参数名称
  • 2.使用装饰器@pytest.mark.usefixtures()修饰
  • 3.autouse=True自动使用

用例传fixture参数

方法一:先定义start功能,用例全部传start参数,调用该功能

# content of test_06.py
import time
import pytest

# ** 作者:上海-悠悠 QQ交流群:588402570**

@pytest.fixture(scope="function")
def start(request):
    print('\n-----开始执行function----')


def test_a(start):
    print("-------用例a执行-------")




class Test_aaa():

    def test_01(self, start):
        print('-----------用例01--------------')

    def test_02(self, start):
        print('-----------用例02------------')

if __name__ == "__main__":
    pytest.main(["-s", "test_06.py"])

装饰器usefixtures

方法二:使用装饰器@pytest.mark.usefixtures()修饰需要运行的用例

# content of test_07.py
import time
import pytest

# ** 作者:上海-悠悠 QQ交流群:588402570**

@pytest.fixture(scope="function")
def start(request):
    print('\n-----开始执行function----')


@pytest.mark.usefixtures("start")
def test_a():
    print("-------用例a执行-------")

@pytest.mark.usefixtures("start")
class Test_aaa():

    def test_01(self):
        print('-----------用例01--------------')

    def test_02(self):
        print('-----------用例02------------')

if __name__ == "__main__":
    pytest.main(["-s", "test_07.py"])

设置autouse=True

方法三、autouse设置为True,自动调用fixture功能

  • start设置scope为module级别,在当前.py用例模块只执行一次,autouse=True自动使用

  • open_home设置scope为function级别,每个用例前都调用一次,自动使用

    content of test_08.py

    import time
    import pytest

    ** 作者:上海-悠悠 QQ交流群:588402570**

    @pytest.fixture(scope="module", autouse=True)
    def start(request):
    print('\n-----开始执行moule----')
    print('module : %s' % request.module.name)
    print('----------启动浏览器---------')
    yield
    print("------------结束测试 end!-----------")

    @pytest.fixture(scope="function", autouse=True)
    def open_home(request):
    print("function:%s \n--------回到首页--------" % request.function.name)

    def test_01():
    print('-----------用例01--------------')

    def test_02():
    print('-----------用例02------------')

    if name == "main":
    pytest.main(["-s", "test_08.py"])

运行结果:

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\, inifile:
plugins: metadata-1.7.0, html-1.19.0, allure-adaptor-1.7.10
collected 2 items

..\..\..\..\..\..\YOYO\peizhi\test_08.py 
-----开始执行moule----
module      : YOYO.peizhi.test_08
----------启动浏览器---------
function:test_01 
--------回到首页--------
-----------用例01--------------
.function:test_02 
--------回到首页--------
-----------用例02------------
.------------结束测试-----------


========================== 2 passed in 0.01 seconds ===========================

上面是函数去实现用例,写的class里也是一样可以的

# content of test_09.py
import time
import pytest

# ** 作者:上海-悠悠 QQ交流群:588402570**

@pytest.fixture(scope="module", autouse=True)
def start(request):
    print('\n-----开始执行moule----')
    print('module      : %s' % request.module.__name__)
    print('----------启动浏览器---------')
    yield
    print("------------结束测试 end!-----------")



class Test_aaa():
    @pytest.fixture(scope="function", autouse=True)
    def open_home(self, request):
        print("function:%s \n--------回到首页--------" % request.function.__name__)


    def test_01(self):
        print('-----------用例01--------------')

    def test_02(self):
        print('-----------用例02------------')

if __name__ == "__main__":
    pytest.main(["-s", "test_09.py"])

10.fixture进阶

前言

fixture是pytest的核心功能,也是亮点功能,熟练掌握fixture的使用方法,pytest用起来才会得心应手!

fixture简介

fixture的目的是提供一个固定基线,在该基线上测试可以可靠地和重复地执行。fixture提供了区别于传统单元测试(setup/teardown)有显著改进:

  • 有独立的命名,并通过声明它们从测试函数、模块、类或整个项目中的使用来激活。
  • 按模块化的方式实现,每个fixture都可以互相调用。
  • fixture的范围从简单的单元扩展到复杂的功能测试,允许根据配置和组件选项对fixture和测试用例进行参数化,或者跨函数 function、类class、模块module或整个测试会话sessio范围。

fixture作为参数传入

定义fixture跟定义普通函数差不多,唯一区别就是在函数上加个装饰器@pytest.fixture(),fixture命名不要用test开头,跟用例区分开。用例才是test开头的命名。

fixture是可以有返回值的,如果没return默认返回None。用例调用fixture的返回值,直接就是把fixture的函数名称当成变量名称,如下案例

# test_fixture1.py
import pytest

@pytest.fixture()
def user():
    print("获取用户名")
    a = "yoyo"
    return a

def test_1(user):
    assert user == "yoyo"

if __name__ == "__main__":
    pytest.main(["-s", "test_fixture1.py"])

运行结果

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\YOYO\fixt, inifile:
plugins: rerunfailures-4.1, metadata-1.7.0, html-1.19.0, allure-adaptor-1.7.10
collected 1 item

test_fixture1.py 获取用户名
.

========================== 1 passed in 0.20 seconds ===========================

error和failed区别

测试结果一般有三种:passed、failed、error。(skip的用例除外)


如果在test_用例里面断言失败,那就是failed

# test_fixture2.py
import pytest

@pytest.fixture()
def user():
    print("获取用户名")
    a = "yoyo"
    return a

def test_1(user):
    assert user == "yoyo111"  # 用例失败就是failed

if __name__ == "__main__":
    pytest.main(["-s", "test_fixture2.py"])

如果在fixture里面断言失败了,那就是error

test_fixture3.py
import pytest

@pytest.fixture()
def user():
    print("获取用户名")
    a = "yoyo"
    assert a == "yoyo123"  # fixture失败就是error
    return a

def test_1(user):
    assert user == "yoyo"

if __name__ == "__main__":
    pytest.main(["-s", "test_fixture3.py"])

还有一种情况也会出现error,那就是自己代码写的有问题,自己本身代码报错,那就是error了。

使用多个fixture

如果用例需要用到多个fixture的返回数据,fixture也可以return一个元组、list或字典,然后从里面取出对应数据。

# test_fixture4.py
import pytest

@pytest.fixture()
def user():
    print("获取用户名")
    a = "yoyo"
    b = "123456"
    return (a, b)


def test_1(user):
    u = user[0]
    p = user[1]
    print("测试账号:%s, 密码:%s" % (u, p))
    assert u == "yoyo"

if __name__ == "__main__":
    pytest.main(["-s", "test_fixture4.py"])

当然也可以分开定义成多个fixture,然后test_用例传多个fixture参数

# test_fixture5.py
import pytest

@pytest.fixture()
def user():
    print("获取用户名")
    a = "yoyo"
    return a

@pytest.fixture()
def psw():
    print("获取密码")
    b = "123456"
    return b

def test_1(user, psw):
    '''传多个fixture'''
    print("测试账号:%s, 密码:%s" % (user, psw))
    assert user == "yoyo"

if __name__ == "__main__":
    pytest.main(["-s", "test_fixture5.py"])

fixture与fixture互相调用

fixture与fixture直接也能互相调用的

import pytest

@pytest.fixture()
def first():
    print("获取用户名")
    a = "yoyo"
    return a

@pytest.fixture()
def sencond(first):
    '''psw调用user fixture'''
    a = first
    b = "123456"
    return (a, b)

def test_1(sencond):
    '''用例传fixture'''
    print("测试账号:%s, 密码:%s" % (sencond[0], sencond[1]))

    assert sencond[0] == "yoyo"

if __name__ == "__main__":
    pytest.main(["-s", "test_fixture6.py"])

11.fixture作用范围

fixture里面有个scope参数可以控制fixture的作用范围: session > module > class > function

fixture(scope="function", params=None, autouse=False, ids=None, name=None):
    """使用装饰器标记fixture的功能
     ** 作者:上海-悠悠 QQ交流群:588402570**
     可以使用此装饰器(带或不带参数)来定义fixture功能。 fixture功能的名称可以在以后使用
     引用它会在运行测试之前调用它:test模块或类可以使用pytest.mark.usefixtures(fixturename标记。 
     测试功能可以直接使用fixture名称作为输入参数,在这种情况下,夹具实例从fixture返回功能将被注入。

    :arg scope: scope 有四个级别参数 "function" (默认), "class", "module" or "session".

    :arg params: 一个可选的参数列表,它将导致多个参数调用fixture功能和所有测试使用它

    :arg autouse:  如果为True,则为所有测试激活fixture func 可以看到它。 如果为False(默认值)则显式需要参考来激活fixture

    :arg ids: 每个字符串id的列表,每个字符串对应于params 这样他们就是测试ID的一部分。 如果没有提供ID它们将从params自动生成

    :arg name:   fixture的名称。 这默认为装饰函数的名称。 如果fixture在定义它的同一模块中使用,夹具的功能名称将被请求夹具的功能arg遮蔽; 解决这个问题的一种方法是将装饰函数命名
                       “fixture_ <fixturename>”然后使用”@ pytest.fixture(name ='<fixturename>')“”。
  • function 每一个函数或方法都会调用
  • class 每一个类调用一次,一个类可以有多个方法
  • module,每一个.py文件调用一次,该文件内又有多个function和class
  • session 是多个文件调用一次,可以跨.py文件调用,每个.py文件就是module

scope="function"

@pytest.fixture()如果不写参数,默认就是scope="function",它的作用范围是每个测试用例来之前运行一次,销毁代码在测试用例运行之后运行。

import pytest

@pytest.fixture()
def first():
    print("\n获取用户名")
    a = "yoyo"
    return a

@pytest.fixture(scope="function")
def sencond():
    print("\n获取密码")
    b = "123456"
    return b

def test_1(first):
    '''用例传fixture'''
    print("测试账号:%s" %first)
    assert first == "yoyo"

def test_2(sencond):
    '''用例传fixture'''
    print("测试密码:%s" %sencond)
    assert sencond == "123456"

if __name__ == "__main__":
    pytest.main(["-s", "test_fixture7.py"])

运行结果:

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\YOYO\fixt, inifile:
plugins: rerunfailures-4.1, metadata-1.7.0, html-1.19.0, allure-adaptor-1.7.10
collected 2 items

test_fixture7.py 
获取用户名
测试账号:yoyo
.
获取密码
测试密码:123456
.

========================== 2 passed in 0.01 seconds ===========================

用例放到类里面也一样

import pytest

@pytest.fixture()
def first():
    print("\n获取用户名")
    a = "yoyo"
    return a

@pytest.fixture(scope="function")
def sencond():
    print("\n获取密码")
    b = "123456"
    return b

class TestCase():
    def test_1(self, first):
        '''用例传fixture'''
        print("测试账号:%s" % first)
        assert first == "yoyo"

    def test_2(self, sencond):
        '''用例传fixture'''
        print("测试密码:%s" % sencond)
        assert sencond == "123456"

if __name__ == "__main__":
    pytest.main(["-s", "test_fixture7.py"])

scope="class"

fixture为class级别的时候,如果一个class里面有多个用例,都调用了此fixture,那么此fixture只在该class里所有用例开始前执行一次

import pytest

@pytest.fixture(scope="class")
def first():
    print("\n获取用户名,scope为class级别只运行一次")
    a = "yoyo"
    return a

class TestCase():
    def test_1(self, first):
        '''用例传fixture'''
        print("测试账号:%s" % first)
        assert first == "yoyo"

    def test_2(self, first):
        '''用例传fixture'''
        print("测试账号:%s" % first)
        assert first == "yoyo"

if __name__ == "__main__":
    pytest.main(["-s", "test_fixture9.py"])

运行结果:

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\YOYO\fixt, inifile:
plugins: rerunfailures-4.1, metadata-1.7.0, html-1.19.0, allure-adaptor-1.7.10
collected 2 items

test_fixture9.py 
获取用户名,scope为class级别只运行一次
测试账号:yoyo
.测试账号:yoyo
.

========================== 2 passed in 0.13 seconds ===========================

scope="module"

fixture为module级别时,在当前.py脚本里面所有用例开始前只执行一次

import pytest

@pytest.fixture(scope="module")
def first():
    print("\n获取用户名,scope为module级别当前.py模块只运行一次")
    a = "yoyo"
    return a


def test_1(first):
    '''用例传fixture'''
    print("测试账号:%s" % first)
    assert first == "yoyo"

class TestCase():
    def test_2(self, first):
        '''用例传fixture'''
        print("测试账号:%s" % first)
        assert first == "yoyo"

if __name__ == "__main__":
    pytest.main(["-s", "test_fixture10.py"])

运行结果

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\YOYO\fixt, inifile:
plugins: rerunfailures-4.1, metadata-1.7.0, html-1.19.0, allure-adaptor-1.7.10
collected 2 items

test_fixture10.py 
获取用户名,scope为module级别当前.py模块只运行一次
测试账号:yoyo
.测试账号:yoyo
.

========================== 2 passed in 0.14 seconds ===========================

scope="session"

fixture为session级别是可以跨.py模块调用的,也就是当我们有多个.py文件的用例时候,如果多个用例只需调用一次fixture,那就可以设置为scope="session",并且写到conftest.py文件里

conftest.py文件名称是固定的,pytest会自动识别该文件。放到工程的根目录下,就可以全局调用了,如果放到某个package包下,那就只在该package内有效

conftest.py

import pytest

@pytest.fixture(scope="session")
def first():
    print("\n获取用户名,scope为session级别多个.py模块只运行一次")
    a = "yoyo"
    return a

test_fixture11.py和test_fixture12.py用例脚本

# test_fixture11.py

import pytest
def test_1(first):
    '''用例传fixture'''
    print("测试账号:%s" % first)
    assert first == "yoyo"

if __name__ == "__main__":
    pytest.main(["-s", "test_fixture11.py"])


# test_fixture12.py
import pytest

def test_2(first):
    '''用例传fixture'''
    print("测试账号:%s" % first)
    assert first == "yoyo"

if __name__ == "__main__":
    pytest.main(["-s", "test_fixture12.py"])

如果想同时运行test_fixture11.py和test_fixture12.py,在cmd执行

pytest -s test_fixture11.py test_fixture12.py

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\YOYO\fixt, inifile:
plugins: rerunfailures-4.1, metadata-1.7.0, html-1.19.0, allure-adaptor-1.7.10
collected 2 items

test_fixture11.py .                                                      [ 50%]
test_fixture12.py .                                                      [100%]

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

D:\YOYO\fixt>pytest -s test_fixture11.py test_fixture12.py
============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: D:\YOYO\fixt, inifile:
plugins: rerunfailures-4.1, metadata-1.7.0, html-1.19.0, allure-adaptor-1.7.10
collected 2 items

test_fixture11.py
获取用户名,scope为session级别多个.py模块只运行一次
测试账号:yoyo
.
test_fixture12.py 测试账号:yoyo
.

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

12.conftest.py作用范围

前言

一个测试工程下是可以有多个conftest.py的文件,一般在工程根目录放一个conftest.py起到全局作用。

在不同的测试子目录也可以放conftest.py,作用范围只在该层级以及以下目录生效。

conftest层级关系

在web_conf_py项目工程下建两个子项目baidu、blog,并且每个目录下都放一个conftest.py和init.py(python的每个package必须要有init.py)

web_conf_py是工程名称

├─baidu
│  │  conftest.py
│  │  test_1_baidu.py
│  │  __init__.py
│  
│          
├─blog
│  │  conftest.py
│  │  test_2_blog.py
│  │  __init__.py
│   
│  conftest.py
│  __init__.py

案例分析

web_conf_py工程下conftest.py文件代码案例

# web_conf_py/conftest.py
import pytest

@pytest.fixture(scope="session")
def start():
    print("\n打开首页")

baidu目录下conftest.py和test_1_baidu.py

# web_conf_py/baidu/conftest.py
import pytest

@pytest.fixture(scope="session")
def open_baidu():
    print("打开百度页面_session")


# web_conf_py/baidu/test_1_baidu.py

import pytest

def test_01(start, open_baidu):
    print("测试用例test_01")
    assert 1

def test_02(start, open_baidu):
    print("测试用例test_02")
    assert 1

if __name__ == "__main__":
    pytest.main(["-s", "test_1_baidu.py"])

运行test_1_baidu.py结果可以看出,start和open_baidu是session级别的,只运行一次

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: E:\YOYO\web_conf_py\baidu, inifile:
plugins: metadata-1.7.0, html-1.19.0
collected 2 items

test_1_baidu.py 
打开首页
打开百度页面_session
测试用例test_01
.测试用例test_02
.

========================== 2 passed in 0.01 seconds ===========================

blog目录下conftest.py和test_2_blog.py代码

# web_conf_py/blog/conftest.py
import pytest

@pytest.fixture(scope="function")
def open_blog():
    print("打开blog页面_function")


# web_conf_py/blog/test_2_blog.py

import pytest

def test_03(start, open_blog):
    print("测试用例test_03")
    assert 1

def test_04(start, open_blog):
    print("测试用例test_04")
    assert 1

def test_05(start, open_baidu):
    '''跨模块调用baidu模块下的conftest'''
    print("测试用例test_05,跨模块调用baidu")
    assert 1

if __name__ == "__main__":
    pytest.main(["-s", "test_2_blog.py"])

运行结果可以看出,start起到全局作用,blog目录下的open_blog是function级别,每个用例调用一次。

test_05(start, open_baidu)用例不能跨模块调用baidu模块下的open_baidu,所以test_05用例会运行失败

============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: E:\YOYO\web_conf_py\blog, inifile:
plugins: metadata-1.7.0, html-1.19.0
collected 3 items

test_2_blog.py 
打开首页
打开blog页面_function
测试用例test_03
.打开blog页面_function
测试用例test_04
.E

=================================== ERRORS ====================================
__________________________ ERROR at setup of test_05 __________________________
file E:\YOYO\web_conf_py\blog\test_2_blog.py, line 11
  def test_05(start, open_baidu):
E       fixture 'open_baidu' not found
>       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, metadata, monkeypatch, open_blog, pytestconfig, record_property, record_xml_attribute, record_xml_property, recwarn, start, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

E:\YOYO\web_conf_py\blog\test_2_blog.py:11
====================== 2 passed, 1 error in 0.02 seconds ======================

13.--lf --ff运行上次失败的用例

前言

“80%的bug集中在20%的模块,越是容易出现bug的模块,bug是越改越多“平常我们做手工测试的时候,比如用100个用例需要执行,其中10个用例失败了,

当开发修复完bug后,我们一般是重点测上次失败的用例。

那么自动化测试也一样,当用例特别多时,为了节省时间,第一次部分用例失败了,修复完之后,可以只测上次失败的用例。

pytest -h

命令行输入pytest -h,找到里面两个命令行参数: --lf 和 --ff

  • --lf, --last-failed 只重新运行上次运行失败的用例(或如果没有失败的话会全部跑)
  • --ff, --failed-first 运行所有测试,但首先运行上次运行失败的测试(这可能会重新测试,从而导致重复的fixture setup/teardown)

--lf 和 --ff

lf是last-failed的缩写,我第一次运行全部测试用例有4个通过passed, 2个失败failed,1个error

E:\YOYO\web_conf_py>pytest
============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: E:\YOYO\web_conf_py, inifile:
plugins: metadata-1.7.0, html-1.19.0
collected 7 items

baidu\test_1_baidu.py ..                                                 [ 28%]
baidu\test_2.py FF                                                       [ 57%]
blog\test_2_blog.py ..E                                                  [100%]

=================================== ERRORS ====================================
__________________________ ERROR at setup of test_05 __________________________
file E:\YOYO\web_conf_py\blog\test_2_blog.py, line 11
  def test_05(start, open_baidu):
E       fixture 'open_baidu' not found
>       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, metadata, monkeypatch, open_blog, pytestconfig, record_property, record_xml_attribute, record_xml_property, recwarn, start, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

E:\YOYO\web_conf_py\blog\test_2_blog.py:11
================================== FAILURES ===================================
___________________________________ test_06 ___________________________________

start = None, open_baidu = None

    def test_06(start, open_baidu):
        print("测试用例test_01")
>       assert 1==2
E       assert 1 == 2

baidu\test_2.py:5: AssertionError
---------------------------- Captured stdout call -----------------------------
测试用例test_01
___________________________________ test_07 ___________________________________

start = None, open_baidu = None

    def test_07(start, open_baidu):
        print("测试用例test_02")
>       assert 1==2
E       assert 1 == 2

baidu\test_2.py:9: AssertionError
---------------------------- Captured stdout call -----------------------------
测试用例test_02
================= 2 failed, 4 passed, 1 error in 0.21 seconds =================

如果只想运行其中2个failed的和1error用例,那么可以直接在cmd输入指令

pytest --lf

E:\YOYO\web_conf_py>pytest --lf
============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: E:\YOYO\web_conf_py, inifile:
plugins: metadata-1.7.0, html-1.19.0
collected 7 items / 4 deselected
run-last-failure: rerun previous 3 failures

baidu\test_2.py FF                                                       [ 66%]
blog\test_2_blog.py E                                                    [100%]

========================== 2 failed, 4 deselected, 1 error in 0.16 seconds===========================

如果想先运行上次失败的,后运行其它通过的用例

pytest --ff

E:\YOYO\web_conf_py>pytest --ff
============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: E:\YOYO\web_conf_py, inifile:
plugins: metadata-1.7.0, html-1.19.0
collected 7 items
run-last-failure: rerun previous 3 failures first

baidu\test_2.py FF                                                       [ 28%]
blog\test_2_blog.py E                                                    [ 42%]
baidu\test_1_baidu.py ..                                                 [ 71%]
blog\test_2_blog.py ..                                                   [100%]

================= 2 failed, 4 passed, 1 error in 0.14 seconds =================

14.重复执行用例

前言

平常在做功能测试的时候,经常会遇到某个模块不稳定,偶然会出现一些bug,对于这种问题我们会针对此用例反复执行多次,最终复现出问题来。

自动化运行用例时候,也会出现偶然的bug,可以针对单个用例,或者针对某个模块的用例重复执行多次。

pytest-repeat

pytest-repeat是pytest的一个插件,用于重复执行单个用例,或多个测试用例,并指定重复次数,pytest-repeat支持的版本:

  • Python 2.7, 3.4+ 或 PyPy
  • py.test 2.8或更高

使用pip安装pytest-repeat

pip install pytest-repeat

使用--count命令行选项指定要运行测试用例和测试次数

py.test --count=10 test_file.py

重复执行--count

运行以下代码,项目结构如下

web_conf_py是项目工程名称
│  conftest.py  
│  __init__.py
│              
├─baidu
│  │  conftest.py
│  │  test_1_baidu.py
│  │  test_2.py
│  │  __init__.py 
│          
├─blog
│  │  conftest.py
│  │  test_2_blog.py
│  │  __init__.py      

代码参考:

# web_conf_py/conftest.py
import pytest

@pytest.fixture(scope="session")
def start():
    print("\n打开首页")
    return "yoyo"

# web_conf_py/baidu/conftest.py
import pytest

@pytest.fixture(scope="session")
def open_baidu():
    print("打开百度页面_session")

# web_conf_py/baidu/test_1_baidu.py
import pytest
import time

def test_01(start, open_baidu):
    print("测试用例test_01")
    time.sleep(1)
    assert start == "yoyo"

def test_02(start, open_baidu):
    print("测试用例test_02")
    time.sleep(1)
    assert start == "yoyo"

if __name__ == "__main__":
    pytest.main(["-s", "test_1_baidu.py"])


# web_conf_py/baidu/test_2.py
import pytest
import time

def test_06(start, open_baidu):
    print("测试用例test_01")
    time.sleep(1)
    assert start == "yoyo"
def test_07(start, open_baidu):
    print("测试用例test_02")
    time.sleep(1)
    assert start == "yoyo"

if __name__ == "__main__":
    pytest.main(["-s", "test_2.py"])

cmd进入到工程目录后,不带--count参数只会执行一次

pytest baidu/test_1_baidu.py -s

E:\YOYO\web_conf_py>pytest baidu/test_1_baidu.py -s
============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: E:\YOYO\web_conf_py, inifile:
plugins: xdist-1.23.2, repeat-0.7.0, metadata-1.7.0, html-1.19.0, forked-0.2
collected 2 items

baidu\test_1_baidu.py
打开首页
打开百度页面_session
测试用例test_01
.测试用例test_02
.

========================== 2 passed in 1.03 seconds ===========================

加上参数--count=5,用例会重复执行5次

pytest baidu/test_1_baidu.py -s --count=5

E:\YOYO\web_conf_py>pytest baidu/test_1_baidu.py -s --count=5
============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: E:\YOYO\web_conf_py, inifile:
plugins: xdist-1.23.2, repeat-0.7.0, metadata-1.7.0, html-1.19.0, forked-0.2
collected 10 items

baidu\test_1_baidu.py
打开首页
打开百度页面_session
测试用例test_01
.测试用例test_01
.测试用例test_01
.测试用例test_01
.测试用例test_01
.测试用例test_02
.测试用例test_02
.测试用例test_02
.测试用例test_02
.测试用例test_02
.

========================== 10 passed in 5.06 seconds ==========================

从运行的用例结果看,是先重复5次test_01,再重复5次test_02,有时候我们希望执行的顺序是test_01,test_02按这样顺序重复五次,接下来就用到一个参数--repeat-scope

--repeat-scope

--repeat-scope类似于pytest fixture的scope参数,--repeat-scope也可以设置参数: session , module,class或者function(默认值)

  • function(默认)范围针对每个用例重复执行,再执行下一个用例
  • class 以class为用例集合单位,重复执行class里面的用例,再执行下一个
  • module 以模块为单位,重复执行模块里面的用例,再执行下一个
  • session 重复整个测试会话,即所有收集的测试执行一次,然后所有这些测试再次执行等等

使用--repeat-scope=session重复执行整个会话用例

pytest baidu/test_1_baidu.py -s --count=5 --repeat-scope=session

E:\YOYO\web_conf_py>pytest baidu/test_1_baidu.py -s --count=5 --repeat-scope=session
============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: E:\YOYO\web_conf_py, inifile:
plugins: xdist-1.23.2, repeat-0.7.0, metadata-1.7.0, html-1.19.0, forked-0.2
collected 10 items

baidu\test_1_baidu.py
打开首页
打开百度页面_session
测试用例test_01
.测试用例test_02
.测试用例test_01
.测试用例test_02
.测试用例test_01
.测试用例test_02
.测试用例test_01
.测试用例test_02
.测试用例test_01
.测试用例test_02
.

========================== 10 passed in 5.07 seconds ==========================

@pytest.mark.repeat(count)

如果要在代码中标记要重复多次的测试,可以使用@pytest.mark.repeat(count)装饰器

# test_1_baidu.py
import pytest
import time

def test_01(start, open_baidu):
    print("测试用例test_01")
    time.sleep(0.5)
    assert start == "yoyo"

@pytest.mark.repeat(5)
def test_02(start, open_baidu):
    print("测试用例test_02")
    time.sleep(0.5)
    assert start == "yoyo"

if __name__ == "__main__":
    pytest.main(["-s", "test_1_baidu.py"])

这样执行用例时候,就不用带上--count参数,只针对test_02重复执行5次

E:\YOYO\web_conf_py>pytest baidu/test_1_baidu.py -s
============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-3.6.3, py-1.5.4, pluggy-0.6.0
rootdir: E:\YOYO\web_conf_py, inifile:
plugins: xdist-1.23.2, repeat-0.7.0, metadata-1.7.0, html-1.19.0, forked-0.2
collected 6 items

baidu\test_1_baidu.py
打开首页
打开百度页面_session
测试用例test_01
.测试用例test_02
.测试用例test_02
.测试用例test_02
.测试用例test_02
.测试用例test_02
.

========================== 6 passed in 3.05 seconds ===========================

重复测试直到失败

如果您正在尝试诊断间歇性故障,那么一遍又一遍地运行相同的测试直到失败是有用的。您可以将pytest的-x选项与pytest-repeat结合使用,以强制测试运行器在第一次失败时停止。例如:

py.test --count=1000 -x test_file.py

这将尝试运行test_file.py 1000次,但一旦发生故障就会停止

UnitTest样式测试

不幸的是,此插件不支持unittest框架的用例,pytest-repeat无法使用unittest.TestCase测试类。无论如何,这些测试将始终运行一次--count,并显示警告


15.pytest-assume断言失败继续执行

前言

pytest的断言失败后,后面的代码就不会执行了,通常一个用例我们会写多个断言,有时候我们希望第一个断言失败后,后面能继续断言。

pytest-assume插件可以解决断言失败后继续断言的问题。github地址https://github.com/astraw38/pytest-assume

环境准备

先安装pytest-assume依赖包

pip install pytest-assume

遇到问题

以下是一个简单案例,输入的测试数据有3种,我们需要断言同时满足三种情况

  • x == y

  • x+y > 1

  • x > 1

    import pytest

    上海-悠悠

    @pytest.mark.parametrize(('x', 'y'),
    [(1, 1), (1, 0), (0, 1)])
    def test_simple_assume(x, y):
    print("测试数据x=%s, y=%s" % (x, y))
    assert x == y
    assert x+y > 1
    assert x > 1

运行结果

如果第一个断言就失败了,后面的2个断言都不会执行了

pytest-assume使用案例

使用pytest.assume断言

import pytest
# 上海-悠悠

@pytest.mark.parametrize(('x', 'y'),
                         [(1, 1), (1, 0), (0, 1)])
def test_simple_assume(x, y):
    print("测试数据x=%s, y=%s" % (x, y))
    pytest.assume(x == y)
    pytest.assume(x+y > 1)
    pytest.assume(x > 1)
    print("测试完成!")

运行结果

上下文管理器

pytest.assume 也可以使用上下文管理器去断言

import pytest
from pytest import assume
# 上海-悠悠

@pytest.mark.parametrize(('x', 'y'),
                         [(1, 1), (1, 0), (0, 1)])
def test_simple_assume(x, y):
    print("测试数据x=%s, y=%s" % (x, y))
    with assume: assert x == y
    with assume: assert x+y > 1
    with assume: assert x > 1
    print("测试完成!")

这样看起来会更优雅一点,对之前写的代码改起来也方便一些

需要注意的是每个with块只能有一个断言,如果一个with下有多个断言,当第一个断言失败的时候,后面的断言就不会起作用的.

import pytest
from pytest import assume
# 以下这种是错误的示例,不要一个with下写多个断言
# 上海-悠悠


@pytest.mark.parametrize(('x', 'y'),
                         [(1, 1), (1, 0), (0, 1)])
def test_simple_assume(x, y):
    print("测试数据x=%s, y=%s" % (x, y))
    with assume:
        assert x == y
        assert x+y > 1
        assert x > 1
    print("测试完成!")

16.pytest-ordering 自定义用例的执行顺序

前言

测试用例在设计的时候,我们一般要求不要有先后顺序,用例是可以打乱了执行的,这样才能达到测试的效果.

有些同学在写用例的时候,用例写了先后顺序, 有先后顺序后,后面还会有新的问题(如:上个用例返回数据作为下个用例传参,等等一系列的问题。。。)

github 上有个 pytest-ordering 插件可以控制用例的执行顺序,github插件地址https://github.com/ftobia/pytest-ordering

环境准备

先安装依赖包

pip install pytest-ordering

使用案例

先看pytest默认的执行顺序,是按 test_ording.py 文件写的用例先后顺序执行的

# test_ording.py
import pytest
# 上海-悠悠


def test_foo():
    print("用例11111111111")
    assert True


def test_bar():
    print("用例22222222222")
    assert True


def test_g():
    print("用例333333333333333")
    assert True

运行结果

D:\demo>pytest test_ording.py -vs
============================= test session starts =============================
platform win32 -- Python 3.6.0
cachedir: .pytest_cache
metadata: 
plugins: ordering-0.6,
collected 3 items

test_ording.py::test_foo 用例11111111111
PASSED
test_ording.py::test_bar 用例22222222222
PASSED
test_ording.py::test_g 用例333333333333333
PASSED

========================== 3 passed in 0.07 seconds ===========================

使用 pytest-ordering 插件后改变测试用例顺序

# test_ording.py
import pytest
# 上海-悠悠


@pytest.mark.run(order=2)
def test_foo():
    print("用例11111111111")
    assert True


@pytest.mark.run(order=1)
def test_bar():
    print("用例22222222222")
    assert True


@pytest.mark.run(order=3)
def test_g():
    print("用例333333333333333")
    assert True

运行结果

D:\demo>pytest test_ording.py -vs
============================= test session starts =============================
platform win32 -- Python 3.6.0
cachedir: .pytest_cache
metadata: 
plugins: ordering-0.6,
collected 3 items

test_ording.py::test_bar 用例22222222222
PASSED
test_ording.py::test_foo 用例11111111111
PASSED
test_ording.py::test_g 用例333333333333333
PASSED

========================== 3 passed in 0.04 seconds ===========================

这样就是按指定的顺序执行的用例


17.fixture参数化params

前言

参数化是自动化测试里面必须掌握的一个知识点,用过 unittest 框架的小伙伴都知道使用 ddt 来实现测试用例的参数化。

pytest 测试用例里面对应的参数可以用 parametrize 实现,随着用例的增多,我们的需求也会越来越多,那么如何在 fixture 中使用参数呢?

fixture 源码

先看下 fixture 源码,有这几个参数:scope,params,autouse,ids,name。

def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
    """Decorator to mark a fixture factory function.

    This decorator can be used, with or without parameters, to define a
    fixture function.

    The name of the fixture function can later be referenced to cause its
    invocation ahead of running tests: test
    modules or classes can use the ``pytest.mark.usefixtures(fixturename)``
    marker.

    Test functions can directly use fixture names as input
    arguments in which case the fixture instance returned from the fixture
    function will be injected.

    Fixtures can provide their values to test functions using ``return`` or ``yield``
    statements. When using ``yield`` the code block after the ``yield`` statement is executed
    as teardown code regardless of the test outcome, and must yield exactly once.

    :arg scope: the scope for which this fixture is shared, one of
                ``"function"`` (default), ``"class"``, ``"module"``,
                ``"package"`` or ``"session"``.

                ``"package"`` is considered **experimental** at this time.

    :arg params: an optional list of parameters which will cause multiple
                invocations of the fixture function and all of the tests
                using it.
                The current parameter is available in ``request.param``.

    :arg autouse: if True, the fixture func is activated for all tests that
                can see it.  If False (the default) then an explicit
                reference is needed to activate the fixture.

    :arg ids: list of string ids each corresponding to the params
                so that they are part of the test id. If no ids are provided
                they will be generated automatically from the params.

    :arg name: the name of the fixture. This defaults to the name of the
                decorated function. If a fixture is used in the same module in
                which it is defined, the function name of the fixture will be
                shadowed by the function arg that requests the fixture; one way
                to resolve this is to name the decorated function
                ``fixture_<fixturename>`` and then use
                ``@pytest.fixture(name='<fixturename>')``.
    """
    if callable(scope) and params is None and autouse is False:
        # direct decoration
        return FixtureFunctionMarker("function", params, autouse, name=name)(scope)
    if params is not None and not isinstance(params, (list, tuple)):
        params = list(params)
    return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)

重点看 params 参数:一个可选的参数列表,它将导致多次调用fixture函数和使用它的所有测试

获取当前参数可以使用 request.param

    :arg params: an optional list of parameters which will cause multiple
                invocations of the fixture function and all of the tests
                using it.
                The current parameter is available in ``request.param``.

fixture 之 params 使用示例

request 是pytest的内置 fixture ,主要用于传递参数

# test_fixture_params.py
import pytest
# 作者-上海悠悠 QQ交流群:717225969
# blog地址 https://www.cnblogs.com/yoyoketang/


# 测试数据,存放在list
user_data = ["user1", "user2"]

@pytest.fixture(scope="function", params=user_data)
def users(request):
    '''注册用户参数化'''
    return request.param


def test_register(users):
    print("注册用户:%s"%users)

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

运行结果

>pytest test_fixture_params.py -s
============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-4.5.0, py-1.5.4, pluggy-0.13.1
rootdir: D:\soft\demo
plugins: allure-pytest-2.8.6
collected 2 items

test_fixture_params.py 注册用户:user1
.注册用户:user2
.

========================== 2 passed in 0.02 seconds ===========================

前置与后置

如果每次注册用户之前,需先在前置操作里面清理用户注册表的数据,可以执行SQL,传不同用户的参数

# test_fixture_params.py
import pytest
# 作者-上海悠悠 QQ交流群:717225969
# blog地址 https://www.cnblogs.com/yoyoketang/

def delete_sql(user):
    '''这里执行SQL'''
    sql = "delete from auth_user WHERE username = '%s';"%user
    print("执行的sql:%s"%sql)
    # 调用执行SQL的封装函数


# 测试数据,存放在list
user_data = ["user1", "user2"]

@pytest.fixture(scope="function", params=user_data)
def users(request):
    '''注册用户参数化'''

    # 前置操作
    delete_sql(request.param)

    yield request.param

    # # 后置操作
    # delete_sql(request.param)


def test_register(users):
    print("注册用户:%s"%users)

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

运行结果

collected 2 items

test_fixture_params.py 执行的sql:delete from auth_user WHERE username = 'user1';
注册用户:user1
.执行的sql:delete from auth_user WHERE username = 'user2';
注册用户:user2
.

========================== 2 passed in 0.06 seconds ===========================

后置操作可以写到 yield 后面,参考上面的格式。

原文地址:https://www.cnblogs.com/davis12/p/13830070.html