Pytest框架

1、介绍

  • Pytest是Python实现的一个测试工具,可以用于所有类型和级别的软件测试
  • Pytest是一个可以自动查找到编写的测试用例运行输出结果测试框架
  • Pytest的特点:
    • 是一个命令行工具,编写用例简单,可读性强
    • 非常容易上手,入门简单,文档丰富
    • 支持单元测试和功能测试
    • 支持参数化
    • 执行测试过程中可以将某些测试跳过,或者对某些预期失败的Case标记成失败
    • 支持重复执行失败的Case
    • 支持运行由unittest编写的测试Case
    • 具有很多第三方插件,并且可以自定义扩展
    • 可以运行以test开头、test结尾的包、文件和方法以及Test开头的类
    • 可以使用assert进行断言
  • Pytest安装

2、运行pytest第一个例子

Pytest默认能够自动识别test开头的函数和方法,以及Test开头的类

import pytest
import requests


def test_one():
    r = requests.get('https://itcast.cn')
    print(r.status_code)

def test_two():
    r = requests.get('https://itcast.cn')
    print(r.encoding)

class TestTask(object):
    def test_1(self):
        print('test 1')

    def test_2(self):
        print('test 2')

if __name__ == '__main__':
    pytest.main() # pytest
View Code

Pytest运行方式

通过代码中pytest.main()可以右键直接运行,相当于python test_1.py,也可以通过命令行的方式启动运行

代码中运行的方式

  • pytest.main()
    • 会执行当前文件路径下所有以test开头的py文件都会被执行
      • pytest先获取所有文件中的测试函数后,再执行
    • 不会显示代码中的标准输出(print、logging)
  • pytest.main([“-s”])
    • 会执行当前文件路径下所有以test开头的py文件都会被执行
      • pytest先获取所有文件中的测试函数后,再执行
    • 显示代码中的标准输出(print、logging)
  • pytest.main([“test_1.py”])
    • 只会执行当前文件
    • 不会显示代码中的标准输出(print、logging)
  • pytest.main([“-s”, “test_1.py”])
    • 只会执行当前文件
    • 显示代码中的标准输出(print、logging)

命令行运行的方式(分别一一对应上面代码中运行的方式)

  • pytest
  • pytest –s
  • pytest test_1.py
  • pytest –s test_1.py

3、断言(assert)

  • 断言就是用来检测程序是否正确, 若条件为假,就中断程序执行
  • 编写代码时,总是会做出一些假设,断言就是用于在代码中捕捉这些假设
  • 断言表示为一些布尔表达式

断言使用的场景

  • 使用assert语句
  • 断言预期的异常
  • 断言预期的警告

3.1、assert断言

采用assert断言时,可以添加备注信息

当断言预测失败时,备注信息会以AssertionError抛出,并在控制台输出

assert expression [, arguments]
# expression 为 True 则 pass
# expression 为 False 则 抛出异常,有 argument 则输出 argument
'''
expression:
1)比较运算
2)逻辑运算  and | or | not
3)身份运算  is | is not
4)成员运算  in | not in
'''
View Code

示例代码:

import requests

def test_assert():
    r = requests.get('http://www.itcast.com')
    assert r.status_code == 200, "没有返回200,断言失败"
View Code

3.2、异常断言

在测试过程中,对某些方法进行测试时,预测输入某些特定数据,会抛出某种异常,若出现该异常,则用例执行通过。

对这预期会出现的异常的断言,可以采用pytest中的pytest.raises()进行处理。

3.2.1、pytest.raises()

pytest.raises(expected_exception, match=None, kwargs)
  • expected_exception 是预期异常类或者的异常类元组
  • match 用于匹配异常的消息内容
import pytest


def is_leap_year(year):
    # 先判断year是不是整型
    if isinstance(year, int) is not True:
        raise TypeError("传入的参数不是整数")
    elif year == 0:
        raise ValueError("公元元年是从公元一年开始!!")
    elif abs(year) != year:
        raise ValueError("传入的参数不是正整数")
    elif (year % 4 == 0 and year % 100 != 0) or year % 400 == 0:
        print("%d年是闰年" % year)
        return True
    else:
        print("%d年不是闰年" % year)
        return False


class TestAssert(object):
    """对判断是否是闰年的方法进行测试"""

    def test_exception_typeerror(self):
        # 使用 pytest.raises 作为上下文管理器,捕获给定异常类型TypeError
        # is_leap_year('2020')抛出TypeError,被pytest.raises捕获到,则测试用例执行通过
        with pytest.raises(TypeError):
            # 传入字符串引发类型错误
            is_leap_year('2020')
View Code

3.2.2、把异常信息存储到变量

有时候我们可能需要在测试用到产生的异常信息,我们可以把异常信息存储到一个变量中,变量的类型为 异常类 ,包含异常的 type、value 或者 traceback 等信息

import pytest

def is_leap_year(year):
    # 先判断year是不是整型
    if isinstance(year, int) is not True:
        raise TypeError("传入的参数不是整数")
    elif year == 0:
        raise ValueError("公元元年是从公元一年开始!!")
    elif abs(year) != year:
        raise ValueError("传入的参数不是正整数")
    elif (year % 4 == 0 and year % 100 != 0) or year % 400 == 0:
        print("%d年是闰年" % year)
        return True
    else:
        print("%d年不是闰年" % year)
        return False

class TestAssert(object):
    """对判断是否是闰年的方法进行测试"""

    def test_exception_typeerror(self):
        # 预测到参数不符合要求,会抛出TypeError异常;若出现该异常,则测试用例执行通过
        with pytest.raises(TypeError) as err_info:
            # 传入字符串引发类型错误
            is_leap_year('2020')
        # 断言异常的类型是 TypeError,断言成功则不会抛出异常
        assert err_info.type == TypeError, '错误类型不匹配'
View Code

3.2.3、通过异常的内容捕捉异常

import pytest

def is_leap_year(year):
    # 先判断year是不是整型
    if isinstance(year, int) is not True:
        raise TypeError("传入的参数不是整数")
    elif year == 0:
        raise ValueError("公元元年是从公元一年开始!!")
    elif abs(year) != year:
        raise ValueError("传入的参数不是正整数")
    elif (year % 4 == 0 and year % 100 != 0) or year % 400 == 0:
        print("%d年是闰年" % year)
        return True
    else:
        print("%d年不是闰年" % year)
        return False


class TestAssert(object):
    """对判断是否是闰年的方法进行测试"""
    def test_exception_typeerror(self):
        # 通过异常的内容捕捉异常,但具体是何种异常不清楚
        with pytest.raises(Exception, match='从公元一年开始') as err_info:
            is_leap_year(0)
        # 断言异常的类型是 ValueError,断言成功则不会抛出异常
        assert err_info.type == ValueError
View Code

3.3、警告断言

  • pytest 中对警告进行断言采用 pytest.warns() 方法,其断言的方法与 pytest.raises() 类似。
  • pytest.warns() 除了能断言告警外,还能够捕获告警信息,并对捕获的告警信息进行分类处理,可以设定出现特定告警信息时,则用例执行失败。
  • 程序中抛出警告时,并不会中断程序的执行,一般是打印警告信息,而程序抛出异常时可能直接中断程序的运行。因此对于警告类信息我们在捕获的时候,可以捕获到多个,而异常信息只能捕获到一个。

3.3.1、pytest.warns()

pytest.warns(expected_warning, *args, match, **kwargs)
  • expected_warning 预期的警告类或者警告类元组,例如:

    • DeprecationWarning 弃用警告
    • UserWarning 用户警告
  • match:支持正则表达式,可以自定义的匹配警告内容

抛出异常方式:

import warnings

warnings.warn(message, category)

说明:

  • message:警告信息
  • category:警告类型
import pytest
import warnings

def make_warn():
    # 抛出
    warnings.warn("deprecated", DeprecationWarning)

def not_warn():
    pass

def user_warn():
    warnings.warn("user warn", UserWarning)


class TestWarns(object):
    def test_make_warn(self):
        with pytest.warns(DeprecationWarning):
            make_warn()

    def test_not_warn(self):
        # 断言not_warn函数将会抛出警告
        # 但实际是没有抛出警告的,所以无法通过测试
        with pytest.warns(Warning):
            not_warn()

    def test_user_warn(self):
        with pytest.warns(UserWarning):
            user_warn()
View Code

3.3.2、把警告信息存储到变量

将告警信息存入一个变量中,通过读取这个变量中的信息进行断言,包括:告警的个数、告警信息参数等。

# pytest_code/test_8.py
import warnings
import pytest

def warn_message():
    warnings.warn("user", UserWarning)
    warnings.warn("runtime", RuntimeWarning)

def test_warn_match():
    with pytest.warns((UserWarning, RuntimeWarning)) as record:
        warn_message()
    assert len(record) == 2
    assert str(record[0].message) == "user"
    assert str(record[1].message) == "runtime"
    assert record[0].category == UserWarning
    assert record[1].category == RuntimeWarning
View Code

3.3.3、通过警告内容捕捉警告

# pytest_code/test_9.py
import warnings
import pytest


def make_warn():
    # 抛出
    warnings.warn("deprecated", DeprecationWarning)


def not_warn():
    pass

def user_warn():
    warnings.warn("user warn", UserWarning)


class TestWarns(object):
    def test_make_warn(self):
        # 捕获警告内容为deprecated的警告
        with pytest.warns(Warning, match='deprecated'):
            make_warn()
View Code


4、setup和teardown函数

  • pytest中有分别针对模块、类、函数的setup和teardown,分别在执行模块、类、函数之前和之后运行
  • pytest提供的这些方法名称都是固定的

4.1、函数级别

  • setup_function()teardown_function()函数定义在类外,会对test开头的非类方法(普通测试用例函数)生效

  • 作用域单个测试用例,每次在单个用例执行前后都执行

import pytest

def setup_function():
    print("setup_function(): 每个方法之前执行")


def teardown_function():
    print("teardown_function(): 每个方法之后执行")


def test_01():
    print("正在执行test1")


def test_02():
    print("正在执行test2")

class TestClass(object):
    def test_03(self):
        print("正在执行 类方法 test3")


def normal_func():
    print("正在执行normal_func")
View Code

4.2、类级别

  • 必须在类中定义,只作用于本类

  • setup_classteardown_class 会在在类执行前后执行一次

  • setup_methodteardown_method 会在每个测试方法前后执行一次
import pytest


class TestMethod(object):
    @classmethod
    def setup_class(cls):
        print("setup_class(self):每个类之前执行一次")

    @classmethod
    def teardown_class(cls):
        print("teardown_class(self):每个类之后执行一次")

    def setup_method(self):
        print("setup_method(self):在每个方法之前执行")

    def teardown_method(self):
        print("teardown_method(self):在每个方法之后执行\n")

    def test_01(self):
        print("正在执行test1")


    def test_02(self):
        print("正在执行test2")

    def normal_func(self):
        print("正在执行normal_func")
View Code

4.3、模块级别

  • 定义在类外,作用域本模块

  • setup_moduleteardown_module 会模块内只执行一次,和函数级别,类级别不冲突

import pytest

def setup_module():
    print("setup_module():在模块最之前执行")


def teardown_module():
    print("teardown_module:在模块之后执行")


def setup_function():
    print("setup_function():每个方法之前执行")


def teardown_function():
    print("teardown_function():每个方法之后执行")

def test_outside_1():
    print('正在执行test_outside_1')

def test_outside_2():
    print('正在执行test_outside_2')

class TestMethod(object):
    @classmethod
    def setup_class(cls):
        print("setup_class(self):每个类之前执行一次")

    @classmethod
    def teardown_class(cls):
        print("teardown_class(self):每个类之后执行一次")

    def setup_method(self):
        print("setup_method(self):在每个方法之前执行")

    def teardown_method(self):
        print("teardown_method(self):在每个方法之后执行\n")

    def test_01(self):
        print("正在执行test1")

    def test_02(self):
        print("正在执行test2")

    def normal_func(self):
        print("正在执行normal_func")
View Code

小结

  • 若用例没被执行或失败,则不会执行teardown
  • 对非test开头的函数不生效
  • setup_module()
    • 在当前模块运行之前执行,必须定义在类外
  • teardown_module()
    • 在当前模块运行的最后执行,必须定义在类外
  • setup_class(self)
    • 所有类方法运行之前执行一次,必须在类中定义,只作用于本类
  • teardown_class(self)
    • 所有类方法运行之后执行一次,必须在类中定义,只作用于本类
  • setup_method(self)
    • 在每个类方法运行之前执行,必须定义在类中,只作用于本类
  • teardown_method(self)
    • 在每个类方法运行之后执行,必须定义在类中,只作用于本类
  • setup_function()
    • 会在每个方法之前执行,对类方法不生效,对非test开头的函数不生效,必须定义在类外
  • teardown_function()
    • 会在每个方法之后执行,对类方法不生效,对非test开头的函数不生效,必须定义在类外

5、pytext.fixture()

  • setup和teardown函数能够在测试用例之前或者之后添加一些操作,但这种是整个脚本全局生效的。
  • 如果我们想实现以下场景:用例1需要登录,用例2不需要登录,用例3需要登录,这就无法直接用setup和teardown来同一个类中实现,却可以通过pytest.fixture实现。

5.1、基本使用

fixture是pytest特有的功能,它以装饰器形式定义在函数上面, 在编写测试函数的时候,可以将被fixture装饰的函数的名字做为测试函数的参数运行测试脚本时,执行测试函数时就会自动传入被fixture装饰的函数的返回值

import pytest
import requests

# 0.@pytest.fixture装饰函数
@pytest.fixture()
def get_web_url():
    print('get_web_url')
    return 'https://www.baidu.com'

# 1. 把上面函数名作为测试用例的参数
def test_web(get_web_url):
    # 2. 测试用例调用前,需要先确定形参get_web_url,就是调用get_web_url
    print('test_web')
    print(get_web_url) # 测试用例内部使用get_web_url,就是使用它返回值
    r = requests.get(get_web_url)
    assert r.status_code == 200, '测试失败'
View Code

5.2、conftest.py文件——共享fixture函数

  • 如果在测试中多个测试文件中用例用到同一个的fixture函数,则可以将其移动到conftest.py文件中,所需的fixture对象会自动被pytest发现,而不需要再每次导入
  • conftest.py文件名固定

  • 在conftest.py文件中实现共用的fixture函数

conftest.py代码:

# pytest_fixture/conftest.py 文件名不能改变,否则无效
import pytest

# 默认是function级别的
@pytest.fixture()
def login_fixture():
    """可以把函数作为参数传递"""
    print("\n公用的登陆方法")
View Code

测试用例代码:

# pytest_fixture/test_fixture1.py
import pytest

def test_get_carts():
    """购物车不需要登陆"""
    print("\n测试查询购物车,无需登录")


class TestFixtures(object):
    """需要登陆的信息"""
    def test_get_user_info(self, login_fixture):
        print("获取用户信息")

    def test_order_info(self, login_fixture):
        print("查询订单信息")

def test_logout(login_fixture):
    """登出"""
    print("退出登录")

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

5.3、fixture参数

pytest.fixture(scope='function', params=None, autouse=False, ids=None, name=None)
  • scope: 被标记方法的作用域, 可以传入以下四个值;
    • "function": 默认值,每个测试用例都要执行一次 fixture 函数
    • "class": 作用于整个类, 表示每个类只运行一次 fixture 函数
    • "module": 作用于整个模块, 每个 module 的只执行一次 fixture 函数
    • "session": 作用于整个 session , 一次 session 只运行一次 fixture
  • params: list 类型,默认 None, 接收参数值,对于 param 里面的每个值,fixture 都会去遍历执行一次。
  • autouse: 是否自动运行,默认为 false, 为 true 时此 session 中的所有测试函数都会调用 fixture

5.3.1、scope

  • function:设置为function,表示每个测试方法都要执行一次
# function:设置为function,表示每个测试方法都要执行一次
import pytest

@pytest.fixture(scope='function')
# @pytest.fixture() # 和上面等价
def foo():
    print('foo')


def test_1(foo):
    print('普通测试用例111111')

def test_2():
    print('普通测试用例22222')


class TestClass(object):
    def test_one(self, foo):
        print('类实例方法测试用例111111')

    def test_two(self, foo):
        print('类实例方法测试用例22222')
View Code
  • class:设置为 class 时代表这个类中只会执行一次
import pytest

@pytest.fixture(scope='class')
def foo():
    print('foo')


def test_1(foo):
    print('普通测试用例111111')

def test_2(foo):
    print('普通测试用例22222')


class TestClass(object):
    def test_one(self, foo):
        print('类实例方法测试用例111111')

    def test_two(self, foo):
        print('类实例方法测试用例22222')
View Code
  • module:设置为 module 时代表这个模块中只会执行一次
  • session:整个 session 都只会执行一次
# module:只会在最开始的时候传入参数执行1次
# session:只会在session开始传入参数的时候执行1次
import pytest

@pytest.fixture(scope='module')
# @pytest.fixture(scope='session')
def foo():
    print('foo')


def test_1(foo):
    print('普通测试用例111111')

def test_2(foo):
    print('普通测试用例22222')


class TestClass(object):
    def test_one(self, foo):
        print('类实例方法测试用例111111')

    def test_two(self, foo):
        print('类实例方法测试用例22222')
View Code

5.3.2、params参数

  • pytest.fixture(params=None) 的params参数接收list类型的参数
  • 对于param里面的每个值,fixture函数都会去遍历执行一次
  • 相应的每次都会驱动使用fixture函数的测试函数执行一次。

案例一:

import pytest


def check_password(password):
    """
    检查密码是否合法

    :param password: 长度是 8 到 16
    :return:
    """
    pwd_len = len(password)
    if pwd_len < 8:
        return False
    elif pwd_len > 16:
        return False
    else:
        return True


@pytest.fixture(params=['1234567', '12345678', '123456789', '123456789012345', '1234567890123456', '12345678901234567'])
def password(request):
    return request.param


def test_check_password(password):
    print(password)
    print(check_password(password))
View Code

案例二:

import pytest


@pytest.fixture(params=['admin', 'zhangsan', 'lisi'])
def username(request):
    return request.param


@pytest.fixture(params=['1234567', '12345678', '123456789', '123456789012345', '1234567890123456', '12345678901234567'])
def password(request):
    return request.param


def test_check_regist(username, password):
    print(username, '=====', password)

if __name__ == '__main__':
    pytest.main(['-s', 'test_14_params2.py'])
View Code

5.3.3、autouse参数

pytest.fixture(autouse=False) 的autouse参数默认为False, 不会自动执行;设置为True时,当前运行的所有测试函数在运行前都会执行fixture函数

import pytest


@pytest.fixture(autouse=True)
def before():
    print('\nbefore each test')

class Test2:
    def test_1(self):
        print('test_5')

    def test_2(self):
        print('test_6')
View Code

6、pytest.mark标记

pytest.mark下提供了标记装饰器,以下是一些常用的标记装饰器

装饰器 作用
pytest.mark.xfail() 将测试函数标记为预期失败
pytest.mark.skip() 无条件地跳过测试函数
pytest.mark.skipif() 有条件地跳过测试函数

pytest.mark.parametrize()

参数化Fixture方法和测试函数

pytest.mark.usefixtures()

使用类、模块或项目中的Fixture方法

6.1、pytest.mark.usefixtures

可以使用@pytest.mark.usefixtures('fixture函数名字符串')来装饰测试类和测试方法

测试用例:

import pytest

def test_get_carts():
    """购物车不需要登陆"""
    print("\n测试查询购物车,无需登录")

@pytest.mark.usefixtures('login_fixture')
class TestFixtures(object):
    """需要登陆的信息"""
    def test_get_user_info(self):
        print("获取用户信息")

    def test_order_info(self):
        print("查询订单信息")

@pytest.mark.usefixtures('login_fixture')
def test_logout():
    """登出"""
    print("退出登录")

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

注意:这种方法无法使用fixture装饰的函数的返回值,将被fixture装饰的函数的名字作为测试用例函数的参数这种方式可以使用fixture装饰的函数的返回值

6.2、pytest.mark.xfail()——标志预期失效

  • 要测试的功能或者函数还没有实现,这个时候执行测试一定是失败的。通过 xfail 来标记某个测试方法一定会失败
  • xfail(condition=True, reason=None, raises=None, run=True, strict=False)
    • condition:标记预期失败的条件,如果条件为 False,那么这个标记无意义,测试可以通过;如果设置为True,测试会执行,但状态会是X
    • reason:标记预期失败的原因说明

import pytest

class Test_ABC:
    def setup_class(self):
        print("\nsetup")

    def teardown_class(self):
        print("\nteardown")

    def test_a(self):
        print("\ntest_a")

    @pytest.mark.xfail(condition=False, reason="预期不会失败")
    def test_b(self):
        print("\ntest_b")
        assert 0

    @pytest.mark.xfail(condition=True, reason="预期失败")
    def test_c(self):
        print("\ntest_c")
        assert 0

if __name__ == '__main__':
    pytest.main(['-s', 'test_22.py'])
View Code

6.3、pytest.mark.skip()——无条件跳过

使用场景: 根据特定条件、不执行标识的测试函数

skip(reason=None)

  • reason: 标注原因

import pytest
import pytest


class Test_ABC:
    def setup_class(self):
        print("\nsetup")

    def teardown_class(self):
        print("\nteardown")

    def test_a(self):
        print("test_a")

    # 开启跳过标记
    @pytest.mark.skip(reason="无条件跳过不执行,就是任性顽皮")
    def test_b(self):
        print("test_b")


if __name__ == '__main__':
    pytest.main(['-s', 'test_23.py'])
View Code

6.4、pytest.mark.skipif()——有条件跳过

使用场景: 根据特定条件、不执行标识的测试函数

  • skipif(condition, reason=None)
    • condition: 跳过的条件,必传参数
    • reason: 标注原因

import pytest

class Test_ABC:
    def setup_class(self):
        print("\nsetup")

    def teardown_class(self):
        print("\nteardown")

    def test_a(self):
        print("test_a")

    # 开启跳过标记
    @pytest.mark.skipif(condition=1, reason="有条件跳过不执行,依旧任性顽皮")
    # @pytest.mark.skipif(condition=0, reason="条件跳不成立,无法跳过")
    def test_b(self):
        print("test_b")


if __name__ == '__main__':
    pytest.main(['-s', 'test_24.py'])
View Code

6.5、pytest.mark.parametrize()——参数化

  • 使用场景:需要测试一组不同的数据,而测试过程是一样的,这种情况下我们可以写一个测试方法,并且测试方法通过参数接受数据。通过遍历数据并且调用测试方法来完成测试。

  • 作用: 参数化fixture方法和测试函数, 方便测试函数对测试属性的获取

  • parametrize(argnames, argvalues, indirect=False, ids=None, scope=None)

    • argnames:参数名, 以逗号分隔的字符串,表示一个或多个参数名称,或参数字符串的列表/元组. 参数名为几个,就会运行几次。
    • argvalues:
      • 参数对应值,类型必须为 list
      • 当参数为一个时,参数格式:[value1,value2,...]
      • 当参数个数大于一个时,格式为: [(param_value1,param_value2),...]

import pytest


class Test_ABC:
    def setup_class(self):
        print("setup")

    def teardown_class(self):
        print("teardown")

    def test_a(self):
        print("test_a")

    @pytest.mark.parametrize("a", [3, 6])
    def test_b(self, a):
        print(f"test_b data:a={a}")


    @pytest.mark.parametrize(["a","b"],[(1,2),(3,4)])
    def test_c(self, a, b):
        print(f"test_c a: {a}; b: {b}")

if __name__ == '__main__':
    pytest.main(['-s', 'test_25.py'])
View Code

7、配置文件

pytest可以自定义配置文件,通常放在运行目录下,名称固定为pytest.ini,命令运行时会使用该配置文件中的配置

创建pytest.ini文件,文件名固定

配置文件参数详解

[pytest]
addopts = -s test_12.py test_13.py
testpaths = ./scripts
python_files = test_*.py
python_classes = Test_*
python_functions = test_*
;在ini文件中注释语句是以分号开始的, 所有的注释语句不管多长都是独占一行直到结束的
View Code

具体参数解读

  • addopts - 配置pytest命令行运行参数
    • 空格分隔,可添加多个命令行参数
  • testpaths - 配置测试搜索的路径
    • 当前目录下的测试脚本文件夹 可自定义
  • python_files - 配置测试搜索的文件名
    • 当前目录下的测试脚本文件夹下,以test_开头,以.py结尾的所有文件 可自定义
  • python_classes - 配置测试搜索的测试类名
    • 当前目录下的测试脚本文件夹下,以Test_开头的类 可自定义
  • 配置测试搜索的测试函数名
    • 当前目录下的测试脚本文件夹下,以test_开头的⽅法 可自定义

8、pytest常用插件

pytest所有插件的完整列表 http://plugincompat.herokuapp.com/

8.1、生成测试报告插件

pip install pytest-html
  • 运行方式1:
    • 修改pytest_code/pytest.ini配置文件
[pytest]
addopts = -s test_14.py --html=./report.html
    • pytest_code/路径下运行
pytest


  • 运行方式2:
    • 命令行运行
pytest -s test_14.py --html=./report.html

执行结果:

  • report.html 就是生成的测试报告
  • assets 文件夹中保存的是测试报告的静态文件

8.2、控制函数执行顺序插件

pip install pytest-ordering
import pytest

class Test_ABC:
    def setup_class(self): print("setup_class")
    def teardown_class(self): print("teardown_class")

    @pytest.mark.run(order=2) # order=2 后运行
    def test_a(self):
        print("test_a")
        assert 1

    @pytest.mark.run(order=1) # order=1 先运行
    def test_b(self):
        print("test_b")
        assert 1
View Code

8.3、失败重试插件

pip install pytest-rerunfailures
  • 修改pytest_code/pytest.ini配置文件
[pytest]
addopts = -s test_16.py --html=./report.html --reruns 2
  • pytest_code/路径下运行:pytest

8.4、按名称取消插件

通过在启动参数中添加-p no:插件名的方式取消插件

  • 修改pytest_code/pytest.ini配置文件
[pytest]
;addopts = -s test_16.py --html=./report.html --reruns 2
addopts = -s test_16.py -p no:ordering -p no:html -p no:rerunfailures
  • pytest_code/路径下运行:pytest

9、Yaml

9.1、Yaml介绍

Yaml 是一种所有编程语言可用的友好的数据序列化标准,类似于 json。其语法和其他高阶语言类似, 并且可以简单表达列表、字典、标量等资料形式。

语法规则

  1. 大小写敏感

  2. 使用缩进表示层级关系

  3. 缩进时不允许使用tab键,只允许使用空格

  4. 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可

支持的数据结构

  • 字典: 键值对的集合
  • 数组: 一组按照次序排列的值, 又称序列(sequence)、列表
  • 纯量: 单个的、不可再分的值, 包括 字符串、布尔值、整数、浮点数、null、日期

9.2、数据结构使用

YAML、YML在线编辑器:http://old.qqe2.com/jsontool/yaml.php

对象

  • 值为字符
animal: pets # 注意冒号后面必须要有一个空格
#  转换为python代码
#     {'animal': 'pets'}
  • 值为字典
animal: {"ke1":"pets","key2":"app"} # python字典
#  转换为python代码
#    {animal: {"ke1":"pets","key2":"app"}} #嵌套字典结构


数组

方式一:

animal:

    - data1
    - data2

#  转换为python代码
#    {'animal': ['data1', 'data2']}

方式二:

animal: ['data1', 'data2']
#  转换为python代码
#    {'animal': ['data1', 'data2']} * 字典嵌套列表

纯量

主要包括: 字符串、布尔值、整数、浮点数、null、日期

value: "hello" # 字符串
value: true
value: false
value: 12
value: 12.129
value: ~ # 空值,python 的 None
value: 2020-10-11 18:18:18

锚点和引用

标注一个内容,用于被其他地方引用

data: &imp # imp就是锚点 以&开头
    value: 456
name:
    valuex: 123
    <<: *imp # 将 data 的数据合并到当前位置 <<:*imp

# 对应 python 数据是
# {'data': {'value': 456}, 'name': {'value': 456, 'valuex': 123}}

9.3、python处理yaml文件

9.3.1、PyYaml库安装

pip3 install -U PyYAML

9.3.2、读取yaml文件

yaml.load(stream, Loader=Loader)

stream 是等待读取的文件对象

示例:

yaml文件:

Search_Data:
  search_test_001:
    value: 456
    expect: [4,5,6]
  search_test_002:
    value: "你好"
    expect: {"value":"你好"}
View Code

python

import yaml

with open("./search_page.yaml", "r") as f:
  data = yaml.load(f, Loader=yaml.FullLoader)
  print(data)

9.3.3、写入文件内容

yaml.dump(data, stream, **kwds)

  • Data: 等待写入的数据, 类型为字典
  • stream: 打开文件对象
  • encodig: utf-8, 设置写入的编码格式
  • allow_unicode: True/False

测试数据:

import yaml
data={'Search_Data': {
          'search_test_002': {'expect': {'value': '你好'}, 'value': '你好'},
          'search_test_001': {'expect': [4, 5, 6], 'value': 456}
                        }
}
# 要设置编码格式,否则会出现中文乱码
with open('./yaml_hello.yaml', 'w', encoding='utf-8') as f:
    yaml.dump(data, f,allow_unicode=True)
View Code

结果:

Search_Data:
  search_test_001:
    expect:

    - 4
    - 5
    - 6

    value: 456
  search_test_002:
    expect:
      value: 你好
    value: 你好  # 设置编码后不出现乱码
View Code

10、综合案例

10.1、测试基本流程

  1. 接口文档
  2. 准备测试用例
  3. 准备测试数据
  4. 执行测试用例
  5. 生成测试报告

10.2、搭建测试框架

测试目录结构如下

image

10.3、数据准备

data/server.yaml:

schema: http
host: 127.0.0.1
port: 8000
View Code

data/users/users.yaml:

user1:
  username: 'mike123333'
  password: 'chuanzhi12345'
  remembered: true
user2:
  username: 'admin'
  password: 'chuanzhi12345'
  remembered: true
user3:
  username: 'itcast'
  password: 'chuanzhi12345'
  remembered: true
View Code

配置文件pytest.ini:

[pytest]
addopts = -s  --html=report/report.html
testpaths = ./tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
View Code

10.4、基础工具方法类

utils/data.py:

# 1. 封装接口,传入文件名,自动返回读取到的文件信息
# 2. 内部处理路径
import yaml
import os

class Data(object):
    # 类属性,获取project的绝对路径
    # os.path.dirname()返回去掉文件名后的绝对路径
    # os.path.abspath(__file__) 当前文件的绝对路径
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

    @classmethod
    def read_info(cls, file_name):
        # 拼接路径  /home/python/code/unittest_project
        file_path = os.path.join(cls.BASE_DIR, 'data', file_name)
        # print(file_path)
        # 打开文件
        with open(file_path, 'r') as f:
            # 读取内容
            data = yaml.load(f, Loader=yaml.FullLoader)

        return data
View Code

utils/ask.py:

"""
1. 获取服务器的信息,读取文件,拆包
2. 封装get请求,post请求
"""
# 从哪里运行,从哪里开始导包

import requests
from requests import Session
from .data import Data


class Ask(object):
    def __init__(self):
        # 实例化一个session
        self.session = Session()

    @staticmethod
    def get_url():
        """http://locast:8080"""
        # { schema: 'http', host: '127.0.0.1', port: 8000 }
        server = Data.read_info('server.yaml')
        return f"{server['schema']}://{server['host']}:{server['port']}"


    @classmethod
    def get(cls, path, params=None, **kwargs):
        """
        :param path: 路径参数
        :return: 响应对象
        """
        resp = requests.get(cls.get_url()+path, params=params, **kwargs)
        return resp

    @classmethod
    def post(cls, path, data=None, json=None, **kwargs):
        """
        :param path: 路径参数
        :return: 响应对象
        """
        resp = requests.post(cls.get_url()+path, data=data, json=json, **kwargs)
        return resp

    def session_get(self, path, params=None, **kwargs):
        """
        :param path: 路径参数
        :return: 响应对象
        """
        resp = self.session.get(self.get_url()+path, params=params, **kwargs)
        return resp

    def session_post(self, path, data=None, json=None, **kwargs):
        """
        :param path: 路径参数
        :return: 响应对象
        """
        resp = self.session.post(self.get_url()+path, data=data, json=json, **kwargs)
        return resp

    def session_delete(self, path, **kwargs):
        """
        :param path: 路径参数
        :return: 响应对象
        """
        resp = self.session.delete(self.get_url()+path, **kwargs)
        return resp
View Code

10.5、测试用例编写

tests/test_user.py:

from utils.ask import Ask
from utils.data import Data
import pytest

"""
1. 读取配置文件的信息,提取用户信息
2. 组包 [用户信息1, 用户信息2,……],列表中元素是用户字典字典
3. 将上面内容返回
"""
def get_user_info():
    users_info = Data.read_info('users/users.yaml')
    # print(users_info)
    args_list = []
    for k, v in users_info.items():
        args_list.append(v)

    # print(args_list)
    return args_list


class TestUser(object):
    def setup_method(self):
        self.s = Ask()  # 实例属性
        print('setUp')

    def teardown_method(self):
        del self.s
        print('tearDown')

    @pytest.mark.parametrize('user_info', get_user_info())
    def test_user(self, user_info):
        # print(user_info)
        self.do_login(user_info)  # 登陆
        self.do_info()  # 查看用户信息
        self.do_logout()  # 登出

    def do_login(self, user_info):
        # print(user_info)
        print('登陆')
        resp = self.s.session_post('/login/', json=user_info)
        ret = resp.json()
        assert ret['code'] == 0, ret['errmsg']

    def do_info(self):
        # 查询登陆用户信息
        print('查询登陆用户信息')
        resp = self.s.session_get('/info/')
        ret = resp.json()
        # print(ret)
        assert ret['code'] == 0, ret['errmsg']

    def do_logout(self):
        # 登出
        print('登出')
        resp = self.s.session_delete('/logout/')
        ret = resp.json()
        assert ret['code'] == 0, ret['errmsg']
View Code

10.6、执行测试用例

开启Django服务后,再执行测试用例

posted on 2020-10-21 18:54  yycnblog  阅读(280)  评论(0)    收藏  举报

导航