今日之失,未必不为后日之得。大家好,刚才在翻看之前整理的一些关于自动化测试的文档,突然发现一个比较详细的关于pytest框架的介绍和使用文章,对于打算使用python进行自动化测试编写的小伙伴,还是很值得一看的,希望对大家有所帮助。

Pytest简介

一、前言

        pytest是python一个第三方单元测试框架,有非富的第三方插件可以扩展,例如:Allure,兼容unittest但比原生未做功能增强的unittest其更高效简洁。

二、pytest安装

1.pytest通过pip安装即可,安装命令:

pip install pytest

2.查看是否安装成功

pytest --version

三、Pytest的常用功能

1.用例运行规则

2.用例运行的级别

3.@pytest.fixture使用

4.@pytest.mark使用

5.插件之pytest-ordering(用例执行顺序)

6.插件之pytest-rerunfailures(失败重跑)

7.插件之pytest-assume(多断言)

8.插件之pytest-xdist(并发)

Pytest运行及控制台输出信息

运行一个简单的用例:

def test_passing():

    assert (1, 2, 3) == (1, 2, 3)

运行结果及说明:

测试运行可能出现的结果总结(上图6、7运行结果列举)

示例:

import pytest


# 测试通过

def test_passing():

    assert (1, 2, 3) == (1, 2, 3)

# 测试失败

def test_failing():

    assert (1, 2, 3) == (3, 2, 1)

# 跳过不执行

@pytest.mark.skip()

def test_skip():

    assert (1, 2, 3) == (3, 2, 1)

# 预期失败,确实失败

@pytest.mark.xfail()

def test_xfail():

    assert (1, 2, 3) == (3, 2, 1)

# 预期失败,但是结果pass

@pytest.mark.xfail()

def test_xpass():

    assert (1, 2, 3) == (1, 2, 3)

运行结果:

Pytest常用命令行选项--case相关

pytest 命令运行语法
pytest --help

常用case相关参数说明

详细举例说明

case文件为:

import  pytest


def test_a():

    assert (1, 2, 3) == (1, 2, 3)


def test_failing():

    assert (1, 2, 3) == (3, 2, 1)


def test_failing2():

    assert (1, 2, 3) == (3, 2, 1)


def test_b():

    assert (1, 2, 3) == (1, 2, 3)


@pytest.mark.smoke1

def test_c():

    assert (1, 2, 3) == (1, 2, 3)


@pytest.mark.smoke2

def test_d():

    assert (1, 2, 3) == (1, 2, 3)

1、–collect–only:收集目录下所有的用例

pytest -–collect-only

2、-k:模糊筛选指定的case,只要匹配到了就,就收集运行

pytest -k "a or b"

从运行结果可以看出,-k是会按条件模糊查找case名称中的信息,只要匹配到了就,就收集运行

3、-m:运行带用指定标记的用例,常于@pytest.mark结合使用

pytest -m “smoke1 or smork2” test_two.py

注:上图的mark警告信息是由于,mark标记 smoke1等不是fixture自带的标记,为自定义标记,所以报错,解决办法将在@pytest.mark内详细说明
4、-x:遇到失败停止运行脚本

上图可以看出,遇到停卡,其它几条用例就不再运行
5、–maxfail=num:达到num错误次数再停卡整个case,此参数与-x类似,都是遇错误终止

pytest --maxfail=2  test_two.py

6、--lf(--last-failed):运行最后一次失败用例,且只运行失败的case(前提是已经运行过的用例有失败的一次运行,-x我们已经有过一次失败了)

pytest --lf test_two.py

7、--ff(--failed-first):同lf相似,但–ff是所有的case都会运行,只是优先运行上次失败的case,再运行其它状态的case

pytest --lf test_two.py

失败重试:
• 测试失败后要重新运行n次,要在重新运行之间添加延迟时间,间隔n秒再运行。
• 安装:pip install pytest-rerunfailures
• pytest -v - -reruns 5 --reruns-delay 1 — 每次等1秒 重试5次

比如:10个用例执行到第4个失败然后重试会重试第四个,如果成功就继续往下执行。

Pytest前后置处理函数Fixture简介

        Fixture是pytest的核心功能,也是亮点功能.属装饰器函数(在不改变被装饰函数的前提下对函数进行功能增强),用于在测试用例运行之前进行前后置工作处理工作。与setup/teardown类似,但更强大灵活

Fixture简介

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

1.有独立的命名,并通过声明它们从测试函数、模块、类或整个项目中的使用来激活。

2.按模块化的方式实现,每个fixture都可以互相调用。

        Fixture的范围从简单的单元扩展到复杂的功能测试,允许根据配置和组件选项对fixture和测试用例进行参数化,或者跨函数function、类class、模块module或整个测试会话sessio范围。

Fixture工作原理

@Pytest.fixture()装饰器用于声明函数是一个fixture

如果测试函数的参数列表中包含fixture装饰的函数名

那么pytest就会检测到然后在测试函数运行之前执行该fixture

如果fixture装饰的函数有返回值,那么fixture在完成任务后,将数据再返回给测试函数,相当于传参。

Pytest搜索fixture的顺序

测试用例的参数列表中包含一个fixture名,pytest会用该名称搜索fixture

优先搜索测试所在的模块

然后再搜索模块同一文件路径下的conftest.py

找不到再搜索上一层的conftest

Pytest之Fixture参数详解及使用

Fixture的调用方式:

@pytest.fixture(scope = "function",params=None,autouse=False,ids=None,name=None)

参数详解:

1、SCOPE

用于控制Fixture的作用范围

作用类似于Pytest的setup/teardown

默认取值为function(函数级别),控制范围的排序为:session > module > class > function

作用范围举例:

scope = “function”

语法:

@pytest.fixture() # 或者

@pytest.fixture(scope='function')

场景一:做为参数传入

import pytest


# fixture函数(类中) 作为多个参数传入

@pytest.fixture()

def login():

    print("打开浏览器")

    a = "account"

return a


@pytest.fixture()

def logout():

    print("关闭浏览器")


class TestLogin:

    #传入lonin fixture

    def test_001(self, login):

        print("001传入了loging fixture")

        assert login == "account"


    #传入logout fixture

    def test_002(self, logout):

        print("002传入了logout fixture")


    def test_003(self, login, logout):

        print("003传入了两个fixture")


    def test_004(self):

        print("004未传入仍何fixture哦")


if __name__ == '__main__':

    pytest.main()

运行结果:

从运行结果可以看出,fixture做为参数传入时,会在执行函数之前执行该fixture函数。再将值传入测试函数做为参数使用,这个场景多用于登录

场景二、Fixture的相互调用
代码:

import pytest


# fixtrue作为参数,互相调用传入

@pytest.fixture()

def account():

    a = "account"

    print("第一层fixture")

    return a

    

# fixture的相互调用一定是要在测试类里调用这层fixture才会生次,普通函数单独调用是不生效的

@pytest.fixture()   

def login(account):

    print("第二层fixture")



class TestLogin:

    def test_1(self, login):

        print("直接使用第二层fixture,返回值为{}".format(login))



    def test_2(self, account):

        print("只调用account fixture,返回值为{}".format(account))





if __name__ == '__main__':

    pytest.main()

运行结果:

注:

1.即使fixture之间支持相互调用,但普通函数直接使用fixture是不支持的,一定是在测试函数内调用才会逐级调用生效

2.有多层fixture调用时,最先执行的是最后一层fixture,而不是先执行传入测试函数的fixture

3.上层fixture的值不会自动return,这里就类似函数相互调用一样的逻辑

scope = “class”:

当测试类内的每一个测试方法都调用了fixture,fixture只在该class下所有测试用例执行前执行一次

测试类下面只有一些测试方法使用了fixture函数名,这样的话,fixture只在该class下第一个使用fixture函数的测试用例位置开始算,后面所有的测试用例执行前只执行一次。而该位置之前的测试用例就不管。

语法:

@pytest.fixture(scope='class')

场景一、

import pytest


# fixture作用域 scope = 'class'

@pytest.fixture(scope='class')

def login():

    print("scope为class")





class TestLogin:

    def test_1(self, login):

        print("用例1")



    def test_2(self, login):

        print("用例2")



    def test_3(self, login):

        print("用例3")




if __name__ == '__main__':

    pytest.main()

运行结果:

场景二、

import pytest


@pytest.fixture(scope='class')

def login():

    a = '123'

    print("输入账号密码登陆")



class TestLogin:

    def test_1(self):

        print("用例1")



    def test_2(self, login):

        print("用例2")



    def test_3(self, login):

        print("用例3")



    def test_4(self):

        print("用例4")



if __name__ == '__main__':

    pytest.main()

运行结果:

scope = “module”:与class相同,只从.py文件开始引用fixture的位置生效

import pytest


# fixture scope = 'module'

@pytest.fixture(scope='module')

def login():

    print("fixture范围为module")



def test_01():

    print("用例01")



def test_02(login):

    print("用例02")



class TestLogin():

    def test_1(self):

        print("用例1")



    def test_2(self):

        print("用例2")



    def test_3(self):

        print("用例3")



if __name__ == '__main__':

    pytest.main()

运行结果:

scope = “session”:用法将在conftest.py文章内详细介绍

session的作用范围是针对.py级别的,module是对当前.py生效,seesion是对多个.py文件生效

session只作用于一个.py文件时,作用相当于module

所以session多数与contest.py文件一起使用,做为全局Fixture

2、params:

Fixture的可选形参列表,支持列表传入

默认None,每个param的值

fixture都会去调用执行一次,类似for循环

可与参数ids一起使用,作为每个参数的标识,详见ids

被Fixture装饰的函数要调用是采用:Request.param(固定写法,如下图)

举个栗子:

3、ids:
  • 用例标识ID
  • 与params配合使用,一对一关系
    举个栗子:
    未配置ids之前,用例:

配置了IDS后:

4、autouse:

默认False

若为True,刚每个测试函数都会自动调用该fixture,无需传入fixture函数名

由此我们可以总结出调用fixture的三种方式:

1.函数或类里面方法直接传fixture的函数参数名称

2.使用装饰器@pytest.mark.usefixtures()修饰

3.autouse=True自动调用,无需传仍何参数,作用范围跟着scope走(谨慎使用)

让我们来看一下,当autouse=ture的效果:

5、Name:

fixture的重命名

通常来说使用 fixture 的测试函数会将 fixture 的函数名作为参数传递,但是 pytest 也允许将fixture重命名

如果使用了name,那只能将name传如,函数名不再生效

调用方法:

@pytest.mark.usefixtures(‘fixture1’,‘fixture2’)

举栗:

import pytest


@pytest.fixture(name="new_fixture")

def test_name():

    pass

    

# 使用name参数后,传入重命名函数,执行成功

def test_1(new_fixture):

    print("使用name参数后,传入重命名函数,执行成功")



# 使用name参数后,仍传入函数名称,会失败

def test_2(test_name):

    print("使用name参数后,仍传入函数名称,会失败")

运行结果:

Pytest前后置处理函数Fixture之yiled

import pytest


@pytest.fixture()

def login():

    print("今天的笔记做完了吗?")

    yield

    print("今天的笔记做完啦!!!")



def test_01(login):

    print("我是用例一")

    

if __name__ == '__main__':

    pytest.main()

运行结果:

从以上运行结果我们可以看到,带有yiled的fixture函数,yiled函数语句分别在测试类前后被执行,我们来总结一下带yiled的fixture特性。

总结:

一般的fixture 函数会在测试函数之前运行,但是如果 fixture 函数包含 yiled,那么会在 yiled处停止并转而运行测试函数,等测试函数执行完毕后再回到该 fixture 继续执行 yiled 后面的代码。

可以将 yiled前面的代码看作是 setup,yiled 后面的部分看作是 teardown 的过程。

无论是测试函数中发生了什么是成功还是失败或者 error 等情况,yiled 后面的代码都会被执行,yiled 中的返回数据也可以在测试函数中使用

Pytest之conftest.py

conftest.py是什么?

conftest.py是fixture函数的一个集合,可以理解为公共的提取出来放在一个文件里,然后供其它模块调用。不同于普通被调用的模块,conftest.py使用时不需要导入,Pytest会自动查找

conftest.py使用场景

如果我们有很多个前置函数,写在各个py文件中是不很乱?再或者说,我们很多个py文件想要使用同一个前置函数该怎么办?这也就是conftest.py的作用

conftest.py使用原则

conftest.py这个文件名是固定的,不可以更改。

conftest.py与运行用例在同一个包下,并且该包中有__init__.py文件使用的时候

不需要导入conftest.py,会自动寻找。

conftest.py使用举例

conftest.py文件(scope=“session”)

import pytest


@pytest.fixture(scope="session")

def login():

    print("输入账号密码")

    yield

    print("清理数据完成")

case文件:

import pytest


class TestLogin1():

    def test_1(self, login):

        print("用例1")



    def test_2(self):

        print("用例2")



    def test_3(self, login):

        print("用例3")

if __name__ == '__main__':

    pytest.main()

运行结果

从上图可以看出,conftest.py内的fixture方法的作用范围是session,调用时,整个.py文件只会调用一次

conftest.py作用域

我们先来看下目录结构

内层case文件:b.py

import pytest


class TestLogin1():

    # 调用了内层fixture

    def test_1(self, login2):

        print("用例1")


    # 调用了外层fixture

    def test_3(self, login):

        print("用例3")

if __name__ == '__main__':

    pytest.main()

内层conftest.py

import pytest


@pytest.fixture(scope="session")

def login2():

    print("内层fixture前置")

外层case:a.py

import pytest


class TestLogin1():

    # 调用了内层fixture

    def test_1(self, login2):

        print("用例1")

    # 调用了外层fixture

    def test_3(self, login):

        print("用例3")

if __name__ == '__main__':

    pytest.main()

外层conftest.py

import pytest


@pytest.fixture(scope="session")

def login():

    print("外层fixture")

运行内层b.py

由上图过行结果可以看出,内外层的conftest.py都可以被找到并被调用执行
我们再来运行一下外层case,a.py

可以看到,外层调用内层的fixture是失败的

由以上运行结果可以总结出:

conftest.py的作用域与Python变量作用域相同

内层包内conftest.py不允许被其它包的测试类或方法使用,相当于本地变量

外层conftest.py可被内层测试类和方法使用,相当于全局变量

Pytest跳过用例之@pytest.mark.skip和@pytest.mark.skipif

@pytest.mark.skip:跳过该条测试用例

用法:

@pytest.mark.skip(self,reason=None)

def test_one():

    pass

参数说明:

@pytest.mark.skipif:跳过符合条件的测试用例

用法:

@pytest.mark.skipif(self,condition, reason=None)

def test_two():

    pass

参数说明:

两者的区别

@pytest.mark.skip:无差别跳过,只要用这个mark就跳过

@pytest.mark.skipif:符合条件的才跳过

举个栗子:

import pytest


# 设置跳过condition

con = "跳过条件"

@pytest.mark.skip("这条是skip,无差别跳过")

def test_mark_skip():

    assert 1==1

    

@pytest.mark.skipif(con == "跳过条件",reason='skipif有条件的跳过')

def test_mark_skipif():

    assert 1==1

    

@pytest.mark.skipif(con == "不符合",reason='skipif不符合条件的执行')

def test_mark_noskipif():

    assert  2==2

    

if __name__ == '__main__':

    pytest.main()

运行结果

Pytest之调用Fixture@pytest.mark.usefixtures

前言

@pytest.mark.usefixtures是Pytest调用fixture的方法之一,与直接传入fixture不同的是,它无法获取到被fixture装饰的函数的返回值。

@pytest.mark.usefixtures的使用场景是被测试函数需要多个fixture做前后置工作时使用

用法

@pytest.mark.usefixtures(self,*fixturenames)

def test_one():

    pass

参数说明:

举栗

场景一:传入单个Fixture

import pytest


@pytest.fixture()

def one():

    print("我是一个fixture函数")



@pytest.mark.usefixtures('one')

def test_one_fixture():

    print("测试用例一")

    assert 1==1

运行结果:

场景一:传入多个Fixture

import pytest


@pytest.fixture()

def one():

    print("我是第一个fixture函数")



@pytest.fixture()

def two():

    print("我是第二个fixture函数")

    

@pytest.mark.usefixtures('one','two')

def test_one_fixture():

    print("测试用例一")

    assert 1==1

运行结果

场景一:叠加多个Fixture

import pytest


@pytest.fixture()

def one():

    print("我是第一个fixture函数")



@pytest.fixture()

def two():

    print("我是第二个fixture函数")



@pytest.mark.usefixtures('one')

@pytest.mark.usefixtures('two')

def test_one_fixture():

    print("测试用例一")

    assert 1==1

运行结果:

总结:

  1. 单个fixture:在被测试函数之前执行
  2. 多个fixture: 按传值先后顺序执行
  3. 叠加fixture: 自下至上执行,离测试函数越近的先被执行

Pytest之@pytest.mark.自定义标签使用

前言:

        Pytest允许用户自定义自己的用例标签,用于将用例进行分组,以便在运行用例的时候筛选你想要运行的用例。

@pytest.mark.自定义标签的使用:
  1. 可以标记测试方法、测试类,标记名可以自定义,最好起有意义的名字;
  2. 同一测试类/方法可同时拥有多个标记;
import pytest

 

@pytest.mark.login

class TestLogin:

    """
    登陆功能测试类
    """


    @pytest.mark.smoke

    @pytest.mark.success

    def test_login_sucess(self):

        """
        登陆成功
        """

        # 实现登陆逻辑

        pass

 

    @pytest.mark.failed

    def test_login_failed(self):

        """
        登陆失败
        """

        # 实现登陆逻辑

        pass

 

 

@pytest.mark.logout

class TestLogout:

    """
    登出功能测试类
    """


    @pytest.mark.smoke

    @pytest.mark.success

    def test_logout_sucess(self):

        """
        登出成功
        """

        # 实现登出功能

        pass

 

    @pytest.mark.failed

    def test_logout_failed(self):

        """
        登出失败
        """

        # 实现登出功能

        pass

运行方法:

语法:

pytest -m "自定义标签名"

或:

pytest.main(['-m 自定义标签名'])

示例:

 pytest -m "smoke" testmark.py

运行结果:

从以上运行结果可以看出,只有被选择的标记用例被运行

组合运行用例
  1. 使用 -m 参数运行标记的测试用例;
  2. -m 参数支持 and、or 、not 等表达式;
# 运行登陆功能的用例

pytest.main(['-m login'])

# 运行登出功能的用例

pytest.main(['-m logout'])

# 运行功能成功的用例

pytest.main(['-m success'])

# 运行功能失败的用例

pytest.main(['-m failed'])

# 运行登陆功能但是不运行登陆失败的测试用例

pytest.main(['-m login and not failed'])

# 运行登出功能但是不运行登出成功的测试用例

pytest.main(['-m logout and not success'])

# 运行登陆和登出的用例

pytest.main(['-m login or logout'])

注册、管理 mark 标记:

当使用 -m 参数执行 mark 标记的用例时,pytest 会发出告警信息 “PytestUnknownMarkWarning: Unknown pytest.mark.login - is this a typo? ”,告诉你这是一个 pytest 未知的一个标记!为了消除告警,我们需要在 pytest 的配置文件中注册 mark 标记!

注册 mark 标记:

  • 首先在项目根目录创建一个文件 pytest.ini ,这个是 pytest 的配置文件; 然后在 pytest.ini 文件的
  • markers 中写入你的 mark 标记, 冒号 “:” 前面是标记名称,后面是 mark 标记的说明,可以是空字符串;
  • 注意:pytest.ini 文件中只能使用纯英文字符,绝对不能使用中文的字符(尤其是冒号和空格)!
[pytest]

markers =

    login   : 'marks tests as login'

    logout  : 'marks tests as logout'

    success : 'marks tests as success'

failed  : 'marks tests as failed'

添加之后再看,警告信息就不再有了:

规范使用 mark 标记:

注册完 mark 标记之后 pytest 便不会再告警,但是有时手残容易写错 mark 名,导致 pytest 找不到用例,一时想不开很难debug,尤其是团队协作时很容易出现类似问题,所以我们需要 “addopts = --strict” 参数来严格规范 mark 标记的使用!

在 pytest.ini 文件中添加参数 “addopts = --strict”;

[pytest]

markers =

    smoke   : 'marks tests as login'

    login   : 'marks tests as login'

    logout  : 'marks tests as logout'

    success : 'marks tests as success'

    failed  : 'marks tests as failed'

 

addopts = --strict

*注意要另起一行,不要在 markers 中添加;
添加该参数后,当使用未注册的 mark 标记时,pytest会直接报错:“ ‘xxx’ not found in markers configuration option ”,不执行测试任务;
注意:pytest.ini 配置文件不支持注释,不支持注释,不支持注释…

Pytest使用-多线程多进程运行

一、需要安装如下环境:

    pip install pytest-xdist 或  

    pip install pytest-parallel

二、对比说明:

    pytest-parallel比pytst-xdist相对好用,功能支持多。

    pytest-xdist不支持多线程,

    而pytest-parallel支持python3.6及以上版本,如果是想做多进程并发的需要在linux平台或mac上做,windows上不起作用即(workers永远=1),如果是做多线程的Linux/Mac/Windows平台都支持,进程数为workers设置的值。

三、pytest-xdist常用命令配置如下:

    -n:进程数,也就是cpu个数 可以指定个数,最大值为当前机器cpu个数,也可以设置为auto,自动识别cpu个数。

    示例:pytest -s test_multiProcess.py -n=2    #用2个进程运行py用例文件

    细节使用参照官网:https://pypi.org/project/pytest-xdist/

import pytest

import time


def testcase_01():

    time.sleep(2)

    print('这里是testcase_01')



def testcase_02():

    time.sleep(4)

    print('这里是testcase_02')



def testcase_03():

    time.sleep(5)

    print('这里是testcase_03')



if __name__ == '__main__':

    pytest.main(['-s',__file__,'-n=4'])

# 输出

......

gw0 I / gw1 I / gw2 I / gw3 I

gw0 [3] / gw1 [3] / gw2 [3] / gw3 [3]

...

============ 3 passed in 6.81s ========   

# 总结:非多进程:9.07s  1cpu:10.59s  2cpu:7.72s  3cpu:4.98s  4cpu:5.32s

四、pytest-parallel常用配置命令如下:

    –workers (optional)  *:多进程运行需要加此参数,  *是进程数。默认为1。

    –tests-per-worker (optional)  *:多线程运行, *是每个worker运行的最大并发线程数。默认为1

    使用示例:

    pytest test.py --workers 3:3个进程运行

    pytest test.py --tests-per-worker 4:4个线程运行

    pytest test.py --workers 2 --tests-per-worker 4:2个进程并行,且每个进程最多4个线程运行,即总共最多8个线程运行。

    参考:https://pypi.org/project/pytest-parallel/

代码示例:

import pytest

import time


def testcase_01():

    time.sleep(2)

    print('这里是testcase_01')



def testcase_02():

    time.sleep(4)

    print('这里是testcase_02')



def testcase_03():

    time.sleep(5)

    print('这里是testcase_03')



def testcase_04():

    time.sleep(9)

    print('这里是testcase_04')



if __name__ == '__main__':

    pytest.main(['-s', __file__,'--workers=1','--tests-per-worker=4'])  


总结:

单线程:20.08s(单个线程累计时长)

2个线程:13.06    3个线程:11.05    4个线程:9.06

【特别注意】:

    1.pytest-parallel的workers参数在windows系统下永远是1,在linux和mac下可以取不同值。

    2..pytest-parallel加了多线程处理后,最后执行时间是运行时间最长的线程的时间。

    3.在windows下想用多进程的选pytst-xdist; 想用多线程的选pytest-paralle

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐