pytest学习总结

 官方pytest文档:Full pytest documentation — pytest documentation


 

 

一、pytest以及辅助插件的安装

1、pytest安装

pip install pytest

2、辅助插件及介绍

pytest-html(生成htm报告的插件)
pytest-xdist (多线程运行的插件)
pytest-ordering (改变用例的执行顺序的插件)
pytest-rerunfailres (失败用例重跑的插件)
allure-pytest (生成美观自定义的allure报告)pytest test_*.py --alluredir=result + allure serve result
        allure generate --alluredir=result -o report/ --clean
pytest-rerunfailures (失败重跑插件)
pytest-repeat   (重复执行用例的插件)
pytest-assume  (assume多重断言,断言失败后边的代码会继续执行)

//插件太多,可通过pip install -r pludin.txt一次性下载完成
例如:plugin.txt中内容如下
pytest-html
pytest-xdist
终端运行:pip install -r pugin.txt即可下载这两个插件

 3、安装插件报错

1)下面是没有安装相应插件报错,例:没有安装allure-pytest插件,运行pytest --alluredir=./temp 会报此错

ERROR: usage: run.py [options] [file_or_dir] [file_or_dir] [...]
run.py: error: unrecognized arguments: --alluredir=./reports/temp/ --clean-alluredir
  inifile: C:\Users\EDY\PycharmProjects\ProductAuto\pytest.ini
  rootdir: C:\Users\EDY\PycharmProjects\ProductAuto

 

 

二、pytest运行

 测试套件默认规则:测试文件必须以test_*开头,测试类名字必须是Test*开头,测试方法名字必须是test_*开头,否则运行时会找不到测试套件

pytest命令的参数:

-vs        输出详细信息。输出调试信息。如: pytest -vs
-n      多线程运行。( 前提安装插件: pytest-xdist)如: pytest -VS -n=2 --reruns 失败重跑(前提安装插件: pytest-rerunfailres )如: pytest --reruns=2    raise Exception()抛出异常,try except解决异常。 -x     出现一个用例失败则停止测试。如: pytest -x --maxfail 出现几个失败才终止,如: pytest --maxfail=2 --html   生成html的测试报告(前提安装插件: pytest-html) , 如: pytest --html=report/report.html -k     运行测试用例名称中包含某个字符串的测试用例。 -m     只测试被标记的用例
--strict-markers 未在pytest.ini配置文件注册的标记mark都会引发报错
--reruns n  (安装pytest-rerunfailures插件),失败重跑n次,最大运行次数n pytest --reruns 5
--reruns-delay n )(pytest-rerunfailures插件),pytest --rerun 2 --reruns-delay 5 失败重跑之间间隔5s
--count    重复执行测试,需安装pytest-repeat,使用:pytest --count=5或者pytest --count 5
        重复执行所有测试用例5次,主要结合-x使用,测试偶发bug,运行直到执行失败
        还有@pytest.mark.repeat(n)可对测试类、方法使用

有两种执行方式:

1)命令行运行:pytest -vs -x

2)主函数运行:

if __name__ == "__main__":
    pytest.main(["-vs”,“-x"])

指定运行:

pytest  test_*.py    //运行指定的test_*.py ,多个py文件分号隔开
pytest  testcase/    //运行指定的testcase目录下所有测试文件    
pytest -k "cs”    //测试包含有“cs”的测试类和方法
pytest  testcase/test_*.py::Test_lei::test_* //可具体指定某个方法

测试执行结果:

  • 退出code 0: 收集并成功通过所有测试用例
  • 退出code 1: 收集并运行了测试,部分测试用例执行失败
  • 退出code 2: 测试执行被用户中断
  • 退出code 3: 执行测试中发生内部错误
  • 退出code 4: pytest命令行使用错误
  • 退出code 5: 没有收集到测试用例

 

全局配置文件pytest.ini,一般建在项目根目录下

注意:

1)这个配置文件的名称不能改

2)编码格式是ANSI,写入中文后自动变为GB2312 中文简体

3)pytest.main()会带上其中配置的参数

pytest.ini配置文件内容解释,实际中根据需求可选择性添加配置。

[pytest]
//pytest参数配置 addopts = -VS
//需要执行用例模块目录 testpaths =./testcases
//可更改测试文件、类、方法的默认规则,一般不做更改 python_files = test_*.py python_classes = Test* python_functions = test_*

//标记用例,一般用于冒烟测试,以下的smoke是标签名,”冒烟测试“是描述,使用
markers=
  smoke:"冒烟测试"
  flow:"流水"

//与@pytest.mark.xfail()的strict一样,为False,意外pass的结果显示xpass,为True,则显示Failed
xfail_strict = False

//控制台日志输出控制器,为True输出,为False关闭
log_cli = True

//用来加速收集用例,去掉不要谝历的目录
norecursedirs = * (目录)

 

断言类型

1)assert正常断言

def test_001(self):  
  
assert var==3  //判断var等于3
   assert var in ‘ab’  //判断字符串‘ab’包含var
   assert var is False  //判断var 为假
   assert var not True  //判断var不为真
   assert var  //判断var为真

2)pytest.raise()异常断言

def test_002(self):
    with pytest.raises(ZeroDivisionError):
        1/0    

3)pytest.warns()警示断言

暂没有合适例子

 

临时目录

1)项目运行过程中,总会产生一些临时文件,pytest提供了tmp_path来创建临时目录。

tmp_path是一个pathlib/pathlib2.Path对象。以下是测试使用方法的示例如:

# test_tmp_path.py文件内容
import os

CONTENT = u"content"

def test_create_file(tmp_path):
    d = tmp_path / "sub"
    d.mkdir()
    p = d / "hello.txt"
    p.write_text(CONTENT)
    assert p.read_text() == CONTENT
    assert len(list(tmp_path.iterdir())) == 1
    assert 0

测试结果如下:除了assert0以外,其他都断言成功
_____________________________ test_create_file _____________________________ tmpdir = local('PYTEST_TMPDIR/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") p.write("content") assert p.read() == "content" assert len(tmpdir.listdir()) == 1 > assert 0 E assert 0

2)tmpdir_factory

 

四、前置和后置

前置和后置方法如下:

  • 模块级别:setup_module、teardown_module
  • 函数级别:setup_function、teardown_function,不在类中的方法
  • 类级别:setup_class、teardown_class
  • 方法级别:setup_method、teardown_method
  • 方法细化级别:setup、teardown
def setup_module():
    print("=====整个.py模块开始前只执行一次:打开浏览器=====")

def teardown_module():
    print("=====整个.py模块结束后只执行一次:关闭浏览器=====")

def setup_function():
    print("===每个函数级别用例开始前都执行setup_function===")

def teardown_function():
    print("===每个函数级别用例结束后都执行teardown_function====")

def test_one():
    print("one")

class TestCase():
    def setup_class(self):
        print("====整个测试类开始前只执行一次setup_class====")

    def teardown_class(self):
        print("====整个测试类结束后只执行一次teardown_class====")

    def setup_method(self):
        print("==类里面每个用例执行前都会执行setup_method==")

    def teardown_method(self):
        print("==类里面每个用例结束后都会执行teardown_method==")

    def setup(self):
        print("=类里面每个用例执行前都会执行setup=")

    def teardown(self):
        print("=类里面每个用例结束后都会执行teardown=")

    def test_three(self):
        print("two")

 

五、fixtrue实现前后置

@pytest.fixtrue(scope="xxx")和yeild实现前置和后置

1)参数scope的五个范围级别:fuction(默认)、class、module、package、session,实例化顺序级别从右至左

2)类调用fixtrue固件只能通过@pytest.mark.usefixtrues("aaa","bbb"),aaa和bbb均为函数名

3)方法能通过参数调用,如def aaa(bbb),aaa为调用函数,bbb为固件函数名,也能使用@pytest.mark.usefixtrues("aaa","bbb")调用

具体效果如下:

//不调用则不显示
@pytest.fixture()
def ttt():
    print("fuction-setup4")

@pytest.fixture()
def aaa():
    print("fuction-setup1")

@pytest.fixture()
def bbb():
    print("fuction-setup2")

@pytest.fixture(scope="class")
def ccc():
    print("setup")     //调用程序前执行
    yield  //后面的的方法体相当于后置
    print("teardown")    //调用程序后执行

//类调用fuction范围级别固件bbb,相当于类里面的每个方法都调用bbb
@pytest.mark.usefixtures("bbb")
//类调用class级别固件ccc,只在调用的类前执行一次
@pytest.mark.usefixtures("ccc")
class Test_study:

    def test_ddd(self,aaa):    //先bbb输出,再aaa输出
        print("AAAAAA")

    @pytest.mark.smoke
    def test_eee(self):
        print("BBBBBB")


-------------------结果如下--------------------------------

kuadi/test_kuadi_login.py::Test_study::test_ddd setup
fuction-setup2
fuction-setup1
AAAAAA
PASSED
kuadi/test_kuadi_login.py::Test_study::test_eee fuction-setup2
BBBBBB
PASSEDteardown

4)其他调用情况

@pytest.fixture(scope="class")
def aaa():
    print("aaa_start")
    yield
    print("aaa_end")

@pytest.fixture()
def bbb():
    print("bbb_start")
    yield
    print("bbb_end")

@pytest.mark.usefixtures("bbb")
def test_fff():
    print("fff")

class Test_QQQ:
    @pytest.mark.usefixtures("bbb")
    def test_ccc(self):
        print("ccc")

    def test_ddd(self, aaa):
        print("ddd")

    def test_eee(self):
        print("eee")

结果------------
fds.py::test_fff bbb_start
fff
PASSEDbbb_end

fds.py::Test_QQQ::test_ccc bbb_start
ccc
PASSEDbbb_end

fds.py::Test_QQQ::test_ddd aaa_start
ddd
PASSED
fds.py::Test_QQQ::test_eee eee
PASSEDaaa_end

 

5)yield和addfinalizer函数的使用

//yield配合fixtrue实现前后置方法
@pytest.fixture(scope="class")
def ccc():
    print("setup")
    yield
    print("teardown")

//with配合yield,等待测试完成后,自动关闭连接
@pytest.fixture(scope="module")
def smtp_connection():
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
        yield smtp_connection

//通过后置操作在测试完成后关闭浏览器连接
@pytest.fixture(scope="module")
def test_addfinalizer(request):
    # 前置操作setup
    print("==再次打开浏览器==")
    test = "test_addfinalizer"

    def fin():
        # 后置操作teardown
        print("==再次关闭浏览器==")

    request.addfinalizer(fin)
    # 返回前置操作的变量
    return test

 

六、conftest文件

conftest文件注意事项:

  • pytest会默认读取conftest.py里面的所有fixture,所以不用导入fixtrue
  • conftest.py 文件名称是固定的,不能改动
  • conftest.py中fixtrue只对同一个package下的所有测试用例生效,其他目录中引入会报错not found
  • 不同目录可以有自己的conftest.py,一个项目中可以有多个conftest.py
  • 下级目录的conftest中fixtrue可以覆盖重写上级目录中同名的fixtrue,且就近生效
  • 测试用例文件中不需要手动import conftest.py,pytest会自动查找

 

七、参数化

@pytest.mark.parametrize()

官方文档:How to parametrize fixtures and test functions — pytest documentation

用法如下:

//测试方法和函数使用
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) def test_eval(test_input, expected): assert eval(test_input) == expected
//测试类使用
@pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])
class TestClass:
    def test_simple_case(self, n, expected):
        assert n + 1 == expected

    def test_weird_simple_case(self, n, expected):
        assert (n * 1) + 1 == expected
//模块全局使用
pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])

class TestClass:
    def test_simple_case(self, n, expected):
        assert n + 1 == expected
//mark标记使用
@pytest.mark.parametrize(
    "test_input,expected",
    [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)],
)
def test_eval(test_input, expected):
    assert eval(test_input) == expected
//叠加使用
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
    print({x},{y})

结果按顺序:0,2/1,2/0.3/1,3

 

@pytest.fixtrue()

第一个例子
#
content of conftest.py import pytest import smtplib @pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"]) def smtp_connection(request): smtp_connection = smtplib.SMTP(request.param, 587, timeout=5) yield smtp_connection print("finalizing {}".format(smtp_connection)) smtp_connection.close()
//第二个例子
@pytest.fixture(params=[0, 1], ids=["spam", "ham"]) def a(request): return request.param def test_a(a): pass def idfn(fixture_value): if fixture_value == 0: return "eggs" else: return None @pytest.fixture(params=[0, 1], ids=idfn) def b(request): return request.param def test_b(b): pass 结果------------------------------------------------- <Function test_a[spam]> <Function test_a[ham]> <Function test_b[eggs]> <Function test_b[1]>

@pytest.fixtrue带标记

@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)])
def data_set(request):
    return request.param

def test_data(data_set):
    pass

fixtrue中使用fixtrue:smtp_connection()是第一个用例中fixtrue

class App:
    def __init__(self, smtp_connection):
        self.smtp_connection = smtp_connection

@pytest.fixture(scope="module")
def app(smtp_connection):
    return App(smtp_connection)

def test_smtp_connection_exists(app):
    assert app.smtp_connection

按fix true实例自动对测试进行分组

@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):
    param = request.param
    print("  SETUP modarg", param)
    yield param
    print("  TEARDOWN modarg", param)


@pytest.fixture(scope="function", params=[1, 2])
def otherarg(request):
    param = request.param
    print("  SETUP otherarg", param)
    yield param
    print("  TEARDOWN otherarg", param)


def test_0(otherarg):
    print("  RUN test0 with otherarg", otherarg)


def test_1(modarg):
    print("  RUN test1 with modarg", modarg)


def test_2(otherarg, modarg):
    print("  RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg))

--------------------------------结果-------------------------------
collecting ... collected 8 items

test_module.py::test_0[1]   SETUP otherarg 1
  RUN test0 with otherarg 1
PASSED  TEARDOWN otherarg 1

test_module.py::test_0[2]   SETUP otherarg 2
  RUN test0 with otherarg 2
PASSED  TEARDOWN otherarg 2

test_module.py::test_1[mod1]   SETUP modarg mod1
  RUN test1 with modarg mod1
PASSED
test_module.py::test_2[mod1-1]   SETUP otherarg 1
  RUN test2 with otherarg 1 and modarg mod1
PASSED  TEARDOWN otherarg 1

test_module.py::test_2[mod1-2]   SETUP otherarg 2
  RUN test2 with otherarg 2 and modarg mod1
PASSED  TEARDOWN otherarg 2

test_module.py::test_1[mod2]   TEARDOWN modarg mod1
  SETUP modarg mod2
  RUN test1 with modarg mod2
PASSED
test_module.py::test_2[mod2-1]   SETUP otherarg 1
  RUN test2 with otherarg 1 and modarg mod2
PASSED  TEARDOWN otherarg 1

test_module.py::test_2[mod2-2]   SETUP otherarg 2
  RUN test2 with otherarg 2 and modarg mod2
PASSED  TEARDOWN otherarg 2
  TEARDOWN modarg mod2

测试功能不需要直接访问fixtrue时,如建一个临时目录或者文件供测试使用,结合@pytest.markusefixtrues()

# content of conftest.py 

import os
import tempfile

import pytest


@pytest.fixture
def cleandir():
    with tempfile.TemporaryDirectory() as newpath:
        old_cwd = os.getcwd()
        os.chdir(newpath)
        yield
        os.chdir(old_cwd)

------------------------------------
# content of test_setenv.py
import os
import pytest


@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit:
    def test_cwd_starts_empty(self):
        assert os.listdir(os.getcwd()) == []
        with open("myfile", "w") as f:
            f.write("hello")

    def test_cwd_again_starts_empty(self):
        assert os.listdir(os.getcwd()) == []
//其他用法
@pytest.mark.usefixtures("cleandir", "anotherfixture")  指定多个fix true
def test():
    pass

//使用pytestmark在测试模块级别指定夹具的用途
pytestmark = pytest.mark.usefixtures("cleandir")

//将项目中所有测试所需的夹具放入ini文件中
[pytest]
usefixtures = cleandir

结合@pytest.mark.parametrize()使用,fixtrue作测试数据处理功能,如:factory

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

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

name = ["name1", "name2"]
pwd = ["pwd1", "pwd2"]

@pytest.mark.parametrize("input_user", name, indirect=True)
@pytest.mark.parametrize("input_psw", pwd, indirect=True)
def test_more_fixture(input_user, input_psw):
    print("fixture返回的内容:", input_user, input_psw)

fixtrue会覆盖同名的fixtrue

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture(params=['one', 'two', 'three']) //最初的参数化fixtrue
        def parametrized_username(request):
            return request.param

        @pytest.fixture  //最初的非参数化fixtrue
        def non_parametrized_username(request):  
            return 'username'

    test_something.py
        # content of tests/test_something.py
        import pytest

        @pytest.fixture  //非参数化fixtrue覆盖参数化fixtrue
        def parametrized_username():
            return 'overridden-username'

     //参数化fixtrue覆盖非参数化fixtrue @pytest.fixture(params
=['one', 'two', 'three']) def non_parametrized_username(request): return request.param def test_username(parametrized_username): assert parametrized_username == 'overridden-username' def test_parametrized_username(non_parametrized_username): assert non_parametrized_username in ['one', 'two', 'three'] test_something_else.py # content of tests/test_something_else.py def test_username(parametrized_username): assert parametrized_username in ['one', 'two', 'three'] def test_username(non_parametrized_username): assert non_parametrized_username == 'username'

直接参数化的fixtrue覆盖非参数化fixtrue

//直接参数化也可以覆盖fixtrue
tests/ __init__.py conftest.py # content of tests/conftest.py import pytest @pytest.fixture def username(): return 'username' test_something.py # content of tests/test_something.py import pytest @pytest.mark.parametrize('username', ['directly-overridden-username']) def test_username(username): assert username == 'directly-overridden-username'

 

 

八、其他装饰器

官方API Reference — pytest documentation

1、@pytest.mark.xfail()、pytest.xfail

@pytest.mark.xfail(condition,*,reason=none,raises=none,run=True,strict=False)

1)raises:none表示默认匹配所有异常错误,值为type(Exception),只能是异常值

2)condition:boolea or str类型,就是有条件执行,只跳过部分特殊用例时使用

3)run:为true,该怎么样就怎么样,为False,则该测试套件不会运行,且始终标为xfail

4)reson:原因说明

5)strict:为False,用例通过显示xpass,用例失败显示xfail,为True,用例失败显示xfail,用例意外通过则显示fail

函数、方法、类执行前使用@pytest.mark.xfail(),预期失败

对方法使用:

//raises参数为空或者与所报的错误匹配时结果是xfail,此结果是预期失败,说明用例通过
   @pytest.mark.xfail()   
    def test_002(self):
        print("账户修改")
        raise Exception

//raises参数与所报的错误不匹配时结果是failed
    @pytest.mark.xfail(raises=ZeroDivisionError)
    def test_004(self):
        print("账户修改")
        raise Exception

--------------------结果------------------------

test_interface.py::Test_kuaidi::test_002 账户修改
XFAIL
test_interface.py::Test_kuaidi::test_004 账户修改
FAILED

对类使用:

@pytest.mark.xfail(raises=ZeroDivisionError)
class Test_kuaidi:

    def test_002(self):  //不匹配的用例显示failed
        print("账户修改")
        raise Exception

    def test_004(self):    //xpass,意外通过
        print("账户修改")
        # raise Exception

    def test_005(self):    //匹配的用例显示xfail
        1/0

------------------------结果展示---------------

test_interface.py::Test_kuaidi::test_002 账户修改
FAILED
test_interface.py::Test_kuaidi::test_004 账户修改
XPASS
test_interface.py::Test_kuaidi::test_005 XFAIL

pytest.xfail(reason=none),主要使用来对已知错误和功能缺失使用的,使用例子:

def test_aaa():
    pytest.xfail(reason="功能未做")

---------结果-------------------------

test_interface.py::test_aaa XFAIL (功能未做)

 

2、@pytest.mark.skip()、@pytest.mark.skipif()、pytest.skip、pytest.importorskip

都是跳过用例,区别:

@pytest.mark.skip(reason=None):reason跳过的原因,不影响程序运行

@pytest.mark.skipif(condition, reason=None):condition参数的值只能是Boolean或者str,有条件跳过

@pytest.mark.skip使用在测试类、测试方法、测试函数上,则测试类、测试方法、测试函数中所有用例都会跳过

@pytest.mark.skipif使用在测试类、测试方法、测试函数上,condition有值则根据条件跳过,没有值和@pytest.mark.skip一样

@pytest.mark.skipif(sys.platform == 'win32', reason="does not run on windows")
class TestSkipIf(object):
    def test_function(self):
        print("不能在window上运行")

 pytest.skip(reason=none,allow_module_level=False,msg)

allow_module_level:默认为False,为True则会允许模块调用,执行时会跳过模块中剩余的用例

def test_aaa():
    pytest.skip("功能未做") //注意,pytest.skip(reason="功能未做")会报错

结果:test_interface.py::test_aaa SKIPPED (功能未做)

pytest.skip(r"功能未做",allow_module_level=True)

结果:跳过所有剩余用例

pytest.importorskip(modname,minversion=none,reason=none)

作用:缺少导入包,跳过用例测试

参数:modname,导入的模块名;minversion,模块最小版本号

expy = pytest.importorskip("pytest", minversion="7.0",reason="pytest7.0版本没有出来")

@expy
def test_import():
    print("test")
结果:会跳过所有该模块的用例测试

 

3、@pytest.mark.flaky()

前提安装pytest-rerunfailures,reruns:重跑次数,reruns_delay:重跑间隔时间

禁止:1、不能和@pytest.fixtrue()一起使用 

   2、该插件与pytest-xdist的looponfail不兼容

   3、与核心--pdb标志不兼容

    @pytest.mark.flaky(reruns=2,reruns_delay=5)
    def test_ddd(self):
        assert False

---------------------执行结果-------------------------
kuadi/test_kuadi_login.py::Test_study::test_ddd RERUN
kuadi/test_kuadi_login.py::Test_study::test_ddd RERUN
kuadi/test_kuadi_login.py::Test_study::test_ddd FAILED

 

 

九、hooks函数

学习地址:Pytest权威教程21-API参考-04-钩子函数(Hooks) - 韩志超 - 博客园 (cnblogs.com)

 

posted on 2021-12-31 22:56  执剑之心  阅读(1136)  评论(0编辑  收藏  举报