Pytest测试框架

pytest测试框架

基本介绍

  1. 介绍:pytest是python的一种单元测试框架,同自带的Unittest测试框架类似,相比于Unittest框架使用起来更简洁,效率更高。
  2. 特点
    1. 易上手,入门简单
    2. 支持简单单元测试和复杂的功能测试
    3. 支持参数化
    4. 执行测试过程中可以将某些测试跳过,或者对某些预期失败的case标记成失败
    5. 支持重复执行失败的case
    6. 支持运行由Nose,Unittest编写的测试Case
    7. 具有第三方插件,可以自定义扩展
    8. 方便和持续集成工具集成

pytest的安装方式

  1. linux下:sudo pip3 install -U pytest
  2. windows下:pip3 install -U pytest
  3. 运行pytest --version会展示当前已安装版本

pytest的设计原则

  1. 文件名以test_*py的格式
  2. 以test_开头的函数
  3. 以Test开头的类
  4. 以test_开头的类方法
  5. 所有包必须要有__init__.py文件
    批注:为什么每个包都必须要带有__init__.py文件?
    理由:一个包是一个带有特殊文件__init__.py文件定义了包的属性和方法.其实它可以什么都不定义,可以是一个空文件,但是必须存在.如果__init__.py不存在,这个目录就仅仅是一个目录,而不是一个包,它就不能被导入或者包含其他的模块和嵌套包.

pytest的常用断言

  1. assert A: 判断A是真
  2. assert not A: 判断A不为真
  3. assert A in B: 判断B包含A
  4. assert A==B: 判断A等于B
  5. assert A!=B: 判断A不等于B

pytest的执行方法

  1. pytest
  2. py.test
  3. python -m pytest
  4. 执行目录下的所有用例 pytest 文件名/
  5. 执行某个文件下用例 pytest 脚本名.py
  6. 运行模块里面的某个函数或者某个类(加-v打印的信息更详细) pytest -v 文件名.py::TestClass::test_method
  7. 标记式表达式(将会运行用@ pytest.mark.slow装饰器修饰的所有测试) pytest -m slow
  8. 简单打印(只打印结果) pytest -q 文件名.py
  9. 详细打印 pytest -q 文件名.py
  10. 遇到错误停止测试 pytest -x test_class.py
  11. 错误达到指定数量停止测试 pytest --maxfail=1
  12. 执行用例包含http的用例 pytest -s -k http start.py
  13. 根据用例排除某些用例 pytest -s -k "not http" start.py
  14. 同时匹配不同的测试用例 pytest -s -k "method or weibo" start.py

pytest的用例运行级别

  1. 模块级别 setup_module、teardown_module
  2. 函数级别 setup_function、teardown_function,不在类中的方法
  3. 类级别 setup_class、teardown_class
  4. 方法级别 setup_method、teardown_method
  5. 方法细化级别 setup、teardown

pytest之fixture

从模块级别中来看,用例加setup和teardown可以实现在测试用例之前或之后加入一些操作,但这种是整个脚本全局生效的,如果想实现以下场景:用例1需要先登录,用例2不需要登录,用例3需要先登录,那么很显然使用setup和teardown就实现不了,于是可以自动以测试用例的预置条件

fixture的优势

fixture相对于setup和teardown来说有以下几点优势:

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

fixture参数列表

使用@pytest.fixture(scope="function",params=None,autouse=False,ids=None,name=None):装饰器来标记fuxture的功能,具体参数详解如下:

  1. scope 四个级别参数
    1. 默认的function 每一个函数或方法都会调用
    2. class 每一个类调用一次
    3. module 作用是当前整个py文件都能生效,参数写上函数名称即可
    4. session 多个文件调用一次,可以跨.py文件调用,每个.py文件就是module
  2. params 一个可选的参数列表,会导致多个参数fixture功能和所有测试使用它
  3. autouse 默认:False,需要用例手动调用该fixture;如果是True,所有作用域内的测试用例都会自动调用该fixture
  4. ids 每个字符串id的列表,每个字符串对应与params这样他们就是测试ID的一部分,如果没有提供ID它们将从params自动生成
  5. name 默认:装饰器的名称,同一模块的fixture相互调用建议写个不同的name

fixture的调用

  1. 将fixture名称作为测试用例函数的输入参数(如果不指定fixture名称,那么将函数明作为参数)
  2. 测试用例加上装饰器:@pytest.mark.usefixtures(fixture_name)
  3. fixture设置autouse=True
import pytest


class Test_login:
    # 调用方式1(使用函数名作为参数传递)
    @pytest.fixture
    def login(self):
        print("我叫panda")

    def test_result(self, login):
        print("我今年20岁")

    # 调用方式1(使用fixture名称作为参数传递)
    @pytest.fixture(name="name")
    def login1(self):
        print("我叫sunmmer")

    def test_result1(self, name):
        print("我今年21岁")

    # 调用方式2(使用usefixtures)
    @pytest.mark.usefixtures("login")
    def test_result2(self):
        print("我今年22岁")

    """
    # 调用方式3(使用autouse),每个测试用例都会调用
    @pytest.fixture(autouse=True)
    def login(self):
        print("我叫sunfree")

    def test_result3(self):
        print("我今年23岁")
    """


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

注意点:

  1. 在类声明上面加@pytest.mark.usefixtures(),代表的是这个类里面所有测试用例都会调用该fixture
  2. 可以叠加多个 @pytest.mark.usefixtures() ,先执行的放底层,后执行的放上层
  3. 可以传多个fixture参数,先执行的放前面,后执行的放后面
  4. 如果fixture有返回值,用 @pytest.mark.usefixtures() 是无法获取到返回值的,必须用传参的方式(方式一)
  5. 添加了 @pytest.fixture ,如果fixture还想依赖其他fixture,需要用函数传参的方式,不能用 @pytest.mark.usefixtures() 的方式,否则会不生效
  6. scope使用module只会在第一个用例之前执行一次,如果我第一个测试用例之前不调用,那么我就在第二个测试用例调用open函数

scope+yield

# 新建test_demo.py

import pytest


@pytest.fixture(scope="module")
def open():
    print("打开浏览器,访问至百度首页")
    yield  # 编写用例时,将yield写在结束操作之前就可,然后在所有用例执行完之后执行一次
    print("这是teardown操作")
    print("关闭浏览器")


def test_case1(open):
    print("用例1")


def test_case2(open):
    print("用例2")


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

# 结果:上面的用例都调用了open()
# 操作,在所有用例执行前执行一次open,然后运行用例,最后所有用例执行完之后执行一次yield后面的结束操作
# 注:yield在用例里面充当了teardown操作。就算用例执行报错,yield还是会正常执行不会被影响

注意点:

  1. 如果yield前面的代码,即setup部分已经抛出异常了,则不会执行yield后面的teardown内容
  2. 如果测试用例抛出异常,yield后面的teardown内容还是会正常执行

终结函数

from selenium import webdriver
from page.page_login import PageLogin
import page
import time
import pytest


@pytest.fixture(scope="session")
def driver(request):
    driver = webdriver.Firefox()
    driver.maximize_window()
    driver.get(page.url)

    def close():
        time.sleep(3)
        driver.quit()

    request.addfinalizer(close)
    return driver

注意点:

  1. 如果 request.addfinalizer() 前面的代码,即setup部分已经抛出异常了,则不会执行 request.addfinalizer() 的teardown内容(和yield相似,应该是最近新版本改成一致了)
  2. 可以声明多个终结函数并调用

测试结果状态

用例执行完成后,每条用例都有自己的状态,常见的状态有

  1. passed:测试通过
  2. failed:断言失败 测试用例的代码有一场,包括主动抛出异常或代码有一场,都算failed
  3. error:用例本身写的质量不行,本身代码报错(譬如:fixture不存在,fixture里面有报错)
  4. xfail:预期失败,加了 @pytest.mark.xfail()

pytest之conftest.py配置文件

在上面的案例中,同一个py文件,多个用例调用一个登陆功能,如果有多个py文件都需要调用这个登陆功能的话,就不能把登录写到用例里面去了,此时应该有个配置文件,单独管理一些预置的操作场景,pytest里面默认读取conftest.py里面的配置
conftest.py配置需要注意一下几点:

  1. conftest.py配置名称是固定的,不能改名称
  2. conftest.py与运行的用例要在同一个package下,并且有__init__.py文件
  3. 不需要import导入conftest.py,pytest用例会自动查找
from selenium import webdriver
import page
import time
import pytest


@pytest.fixture(scope="session")
def driver(request):
    driver = webdriver.Firefox()
    driver.maximize_window()
    driver.get(page.url)

    def close():
        time.sleep(3)
        driver.quit()

    request.addfinalizer(close)
    return driver

pytest之skip、skipif

  1. pytest.mark.skip可以标记无法在某些平台上运行的测试功能,或者您希望失败的测试功能
  2. 希望满足某些条件才执行某些测试用例,否则pytest会跳过运行该测试用例
  3. 实际常见场景:跳过非Windows平台上的仅Windows测试,或者跳过依赖于当前不可用的外部资源(例如数据库)的测试

@pytest.mark.skip()

跳过执行测试用例,可选参数reason:表示跳过的原因

import pytest


class TestLogin:
    @pytest.fixture
    def login(self):
        print("我叫panda")

    def test_result(self, login):
        print("我今年20岁")

    @pytest.mark.skip(reason="就是不想执行")
    def test_splice(self):
        print("不想进行拼接")


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

注意点:

  1. @pytest.mark.skip 可以加在函数上,类上,类方法上
  2. 如果加在类上面,类里面的所有测试用例都不会执行
  3. 以上都是针对:整个测试用例方法跳过执行

pytest.skip()

在测试用例执行期间强制跳过不在执行剩余内容

import pytest


class TestLogin:
    def test_one(self):
        print("test_one方法执行")
        if 1 == 1:
            pytest.skip("skip")  # 如果if语句为True时,执行到这里会跳过,方法后面的代码都不会执行
        assert 1 == 1

    def test_two(self):
        print("test_two方法执行")
        assert 'o' in 'love'

    def test_three(self):
        print("test_three方法执行")
        assert 3 - 2 == 1


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

@pytest.mark.skipif()

希望有条件的跳过某些测试用例

import pytest


class TestClass:
    name = 'one'

    @pytest.mark.skipif(name == 'one', reason="skip")  # 如果装饰器里面的条件满足会跳过改方法
    def test_one(self):
        print("test_one方法执行")
        assert 1 == 1

    def test_two(self):
        print("test_two方法执行")
        assert 'o' in 'love'


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

标记变量

  1. 可以将 pytest.mark.skip 和 pytest.mark.skipif 赋值给一个标记变量
  2. 在不同模块之间共享这个标记变量
  3. 若有多个模块的测试用例需要用到相同的 skip 或 skipif ,可以用一个单独的文件去管理这些通用标记,然后适用于整个测试用例集
import pytest


class TestClass:
    name = 'one'
    skipif=pytest.mark.skipif(name == 'one', reason="skip")

    @skipif  # 如果装饰器里面的条件满足会跳过改方法
    def test_one(self):
        print("test_one方法执行")
        assert 1 == 1

    def test_two(self):
        print("test_two方法执行")
        assert 'o' in 'love'


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

pytest的参数化

常规操作实例

解决测试重复操作,不同数据的情况

import pytest


class TestClass:
    # 以下参数化中
    # 参数名称可以是列表套字符串,元祖套字符串,也可以字符串套字符(字符逗号隔开)
    # 参数值可以是列表套列表,也可以列表套元祖
    # @pytest.mark.parametrize(["a","b","c"], [(1, 2, 3), (2, 3, 5), (3, 4, 7)])
    # @pytest.mark.parametrize(("a", "b", "c"), [(1, 2, 3), (2, 3, 5), (3, 4, 7)])
    # @pytest.mark.parametrize("a,b,c", [(1, 2, 3), (2, 3, 5), (3, 4, 7)])
    @pytest.mark.parametrize("a,b,c", [[1,2,3],[2,3,5]])
    def test_sum(self, a, b, c):
        assert a + b == c


if __name__ == '__main__':
    pytest.main(["-s", "fixture.py"])
# 实际的web UI自动化的开发场景,登录框
1. 需要测试账号空、密码空、账号密码都为空、账号不存在等情况
2. 这些用例的区别就在于输入的测试数据和对应的交互结果
3. 所以只需要写一条登录测试用例,然后把多组测试数据和期望结果参数化,节省很多代码量

源码分析

def parametrize(self,argnames, argvalues, indirect=False, ids=None, scope=None)\

  1. argnames 参数名字
    1. 存放字符串,以逗号隔开
    2. 存放list
    3. 存放dict
  2. ids 用例的ID,传入一个字符串列表,用来标识每一个测试用例
  3. indirect 如果设置成True,则把传入的参数当做函数执行,而不仅仅指的是参数

结合fixture

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

request传入一个参数

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

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


@pytest.fixture(scope="module")
def login(request):
    user = request.param
    return user


@pytest.mark.parametrize("login", test_user_data, indirect=True)    
#添加indirect=True参数是为了把login当成一个函数去执行,而不是一个参数
def test_login(login):
    '''登录用例'''
    a = login
    print("测试用例中login的返回值:%s" % a)
    assert a != ""


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

reuquest传入两个参数

如果用到@pytest.fixture里面用2个参数情况,可以把多个参数用一个字典去存储,这样最终还是只传一个参数不同的参数再从字典里面取对应key值就行,如: user = request.param["user"]

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

# 测试账号数据
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", "sdfdsf.py"])

多个fixture

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

# test_04.py
# 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"])

pytest之pytest-rerunfailures

  1. 安装 pip install pytest-rerunfailures
  2. 执行方法(重新运行与等待时间可以结合使用)
    1. 命令行参数 --reruns n(重新运行次数),--reruns-delay m(等待运行秒数)
    2. 装饰器参数 reruns=n(重新运行次数),reruns_delay=m(等待运行秒数)
  3. 相关操作
    1. 重新运行所有失败的用例 pytest --reruns 5 -s
    2. 重试之间增加延迟时间 pytest --reruns 5 --reruns-delay 10 -s
    3. 重新运行指定测试用例 单个测试用例添加flaky装饰器 @pytest.mark.flaky(reruns=5),在测试失败时自动重新运行
    4. 重新运行并增加延迟时间 @pytest.mark.flaky(reruns=5, reruns_delay=2)

pytest的测试报告

  1. 安装插件 pip install pytest-html
  2. 执行方法 pytest --html=report.html
  3. 指定路径执行 pytest --html=./report/report.html
  4. 将--html=report/report.html追加到pytest.ini的addopts后
  5. 报告独立显示,产生的报告css是独立的,分享报告的时候样式会丢失,为了更好的分享发邮件展示报告,可以把css样式合并到html里 pytest --html=report.html --self-contained-html

pytest之pytest-repeat

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

  1. 安装插件 pip install pytest-repeat
  2. 重复测试直至失败,将pytest的 -x 选项与pytest-repeat结合使用 py.test --count=1000 -x test_file.py
  3. 在代码中将某些测试用例标记为执行重复多次,可以使用 @pytest.mark.repeat(count)
  4. 覆盖默认的测试用例执行顺序,类似fixture的scope参数 --repeat-scope 例:pytest -s --count=2 --repeat-scope=class repeat.py
    1. function:默认,范围针对每个用例重复执行,再执行下一个用例
    2. class:以class为用例集合单位,重复执行class里面的用例,再执行下一个
    3. module:以模块为单位,重复执行模块里面的用例,再执行下一个
    4. session:重复整个测试会话,即所有测试用例的执行一次,然后再执行第二次

pytest之pytest.ini

pytest的配置文件通常放到测试目录下,名称为pytest.ini,命令行运行时会使用该配置文件中的配置,在开头添加[pytest],添加剩余的内容如下:

  1. 配置pytest命令行运行参数 addopts=-s
  2. 配置测试搜索的路径 testpaths=./scripts
  3. 配置测试搜索的文件名 python_files=test_*.py
  4. 配置测试搜索的测试类名 python_classes=Test*
相关代码:

[pytest]
# 添加命令行参数 添加生成报告快捷命令
addopts = -s --html=report/report.html
# 搜索哪个文件夹
testpaths = ./scripts
# 函数名
python_functions=test_*
# 文件名
python_files = test_*.py
# 类名
python_classes = Test*

pytest之pytest-ordering

函数修饰符的方式标记被测函数执行的顺序

安装方式:

  插件名称:使用命令行    pip3 install pytest-ordering

使用方法:

标记于被测试函数,@pytest.mark.run(order=x)   
order的优先级    0>较小的正数>较大的正数>无标记>较小的负数>较大的负数
根据order传入的参数来结局运行顺序
order值全为证书或权威负数时,运行顺序:值越小,优先级越高
正数和负数同时存在:正数优先级高
默认情况下,pytest默认从上往下执行,可以通过第三方插件包改变其运行顺序.

pytest之@pytest.mark.xfail

当用例a失败的时候,如果用例b和用例c都是依赖于第一个用例的结果,那可以直接跳过用例b和c的测试,直接给他标记失败xfail用到的场景,登录是第一个用例,登录之后的操作b是第二个用例,登录之后操作c是第三个用例,很明显三个用例都会走到登录,如果登录都失败,那后面个用例就没有测试的必要了,并且标记为失败用例

标记测试函数为失败函数

方法:xfail(condition=None,reason=None,raises=None,run=true,strict=False)

常用参数:

condition:预期失败的条件,必传参数    如果传True表示确认设定预期失败,传False表示设定预定成功
reason:失败的原因,必传参数    reason="done",可以传任何参数值,我们设定为done
使用方法:@pytest.mark.xfail(condition,reason="xx")

pytest之mark标记

pytest可以支持自动以标记,自动以标记可以把一个web项目划分为多个模块,然后指定模块名称执行,一个大项目自动化用例时,可以划分多个模块,也可以使用标记功能,表明哪些是模块1,哪些是模块2,运行代码时指定mark名称运行就可以
以下用例,标记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=webtest"])
只运行用webtest标记的测试,在运行的时候,加个-m参数,指定参数值webtest    pytest -v -m webtest

如果不想执行标记webtest的用例,那就用"not webtest"    pytest -v -m "not webtest"

如果想指定运行某个.py模块下,类里面的一个用例,如:TestClass里面test_method用例,每个test_开头(或_test结尾)的用例,函数(或方法)的名称就是用例的节点id,指定节点id运行用-v 参数,当然也可以选择运行整个class pytest -v test_server.py::TestClass

if __name__ == "__main__":
    pytest.main(["-v", "test_server.py::TestClass::test_method"])
 

if __name__ == "__main__":
    pytest.main(["-v", "test_server.py::TestClass", "test_server.py::test_send_http"])

pytest之运行上次失败用例

80%的bug集中在20%的模块,越是容易出现bug的模块,bug是越改越多,当开发修复完bug后,一般是重点测上次失败的用例,那么自动化测试也是一样,所以为了节省时间,可以值测试上次失败的用例

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

pytest之pytest-xdist

优点:节约时间
安装插件:pip install pytest-xdist

并行测试

直接加个-n参数即可,后面num参数就是并行数量,比如num设置为3 pytest -n 3
使用pytest-xdist插件也能生成html报告,完美支持pytest-html插件 pytest -n 3 --html=report.html --self-contained-html

分布式测试(使用nginx实现)

知识盲区,后续补充...

pytest之pytest-assume

pytest断言失败后,后面的代码就不会执行了,通常一个用例会写多个断言,有时候我们希望第一个断言失败后,后面能继续断言,那么pytest-assume插件可以解决断言失败后继续断言的问题
安装 pip install 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也可以使用上下文管理器去断言,需要注意的是每个with块只能有一个断言,如果一个with下有多个断言,当第一个断言失败的时候,后面的断言就不会起作用

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("测试完成!")
posted @ 2020-11-09 09:04  SunFree  阅读(505)  评论(0编辑  收藏  举报