Pytest 测试框架

1.pytest命名规则

1. * 文件             test_开头或者_test结尾            
2. * 类               Test_开头    
3. * 方法/函数         test_开头

2.类级别的用例示例

class TestXXX:
    """
    测试类
    """
    def setup(self):
        """
        资源准备
        :return:
        """
        pass

    def teardown(self):
        """
        资源销毁
        :return:
        """
        pass

    def test_xxx(self):
        """

        :return:
        """
        assert self.ActualResult == self.ExpectedResult

3.用例断言

def test_a():
    assert True

def test_b():
    a = 1
    b = 1
    c = 2
    assert a + b == c, f"{a}+{b}=={c}, 结果为真"

def test_c():
    a = 1
    b = 1
    c = 2
    assert 'abc' in "abcd"

import sys
def test_plat():
    assert ('linux' in sys.platform), "该代码只能在 Linux 下执行"

4.pytest测试框架结构

setup_module/teardown_module      全局模块级,模块中只运行一次
setup_class/teardown_class             类级,只在类中前后运行一次
setup_function/teardown_function    函数级,在类外,每个函数执行前后
setup_method/teardown_method     方法级,类中的每个方法执行前后(与setup/teardown作用一致)
setup/teardown                        在类中,运行调用方法的前后(重点)

5.pytest参数化用例

**参数化:单参数**
search_list = ['appium','selenium','pytest']

@pytest.mark.parametrize('name', ['appium','selenium'])
def test_search(name):
    assert name in search_list

**参数化:多参数**
# 数据放在元组中
@pytest.mark.parametrize("test_input,expected",[("3+5",8),("2+5",7),("7+5",12)])
def test_mark_more(test_input,expected):
    assert eval(test_input) == expected
# 数据放在列表中
@pytest.mark.parametrize("test_input,expected",[["3+5",8],["2+5",7],["7+5",12]])
def test_mark_more(test_input,expected):
    assert eval(test_input) == expected


***参数化:笛卡尔积***
两组数据
a=[1,2,3]
b=[a,b,c]
对应有几种组合形势 ?
(1,a),(1,b),(1,c)
(2,a),(2,b),(2,c)
(3,a),(3,b),(3,c)
@pytest.mark.parametrize("b",["a","b","c"])
@pytest.mark.parametrize("a",[1,2,3])
def test_param1(a,b):
    print(f"笛卡积形式的参数化中 a={a} , b={b}")

结果:
1.笛卡积形式的参数化中a=1,b=a
2.笛卡积形式的参数化中a=1,b=b
3.笛卡积形式的参数化中a=1,b=c
4.笛卡积形式的参数化中a=2,b=a
5.笛卡积形式的参数化中a=2,b=b
6.笛卡积形式的参数化中a=2,b=c
7.笛卡积形式的参数化中a=3,b=a
8.笛卡积形式的参数化中a=3,b=b
9.笛卡积形式的参数化中a=3,b=c


***用例重命名***
**通过ids参数,将别名放在列表中**
@pytest.mark.parametrize("test_input,expected",[("3+5",8),("2+5",7),("7+5",12)],
ids=['add_3+5=8','add_2+5=7','3和5相加等于12'])
def test_mark_more(test_input,expected):
    assert eval(test_input) == expected
# 别名需要显示中文时,创建conftest.py 文件 ,将下面内容添加进去,运行脚本
def pytest_collection_modifyitems(items):
    """
    测试用例收集完成时,将收集到的用例名name和用例标识nodeid的中文信息显示在控制台上
    """
    for i in items:
        i.name=i.name.encode("utf-8").decode("unicode_escape")
        i._nodeid=i.nodeid.encode("utf-8").decode("unicode_escape")

6.Mark:标记测试用例

* 场景:只执行符合要求的某一部分用例 可以把一个web项目划分多个模块,然后指定模块名称执行。
* 解决: 在测试用例方法上加 @pytest.mark.标签名
* 执行: -m 执行自定义标记的相关用例
  * `pytest -s test_mark_zi_09.py -m=webtest`
  * `pytest -s test_mark_zi_09.py -m apptest`
  * `pytest -s test_mark_zi_09.py -m "not ios"`

示例:
@pytest.mark.webtest
def test_mark_more():
    print('123')

注意,需要添加pytest.ini文件,否则执行会有警告
文件内容
[pytest]
markers = webtest
          apptest
          ios

7.pytest 设置跳过、预期失败

## Mark:跳过(Skip)及预期失败(xFail)
* 这是 pytest 的内置标签,可以处理一些特殊的测试用例,不能成功的测试用例
* skip - 始终跳过该测试用例
* skipif - 遇到特定情况跳过该测试用例
* xfail - 遇到特定情况,产生一个“期望失败”输出

## Skip 使用场景
* 调试时不想运行这个用例
* 标记无法在某些平台上运行的测试功能
* 在某些版本中执行,其他版本中跳过
* 比如:当前的外部资源不可用时跳过
  * 如果测试数据是从数据库中取到的,
  * 连接数据库的功能如果返回结果未成功就跳过,因为执行也都报错
* 解决 1:添加装饰器
  * `@pytest.mark.skip`
  * `@pytest.mark.skipif`
* 解决 2:代码中添加跳过代码
  * `pytest.skip(reason)`

示例
#执行时跳过该用例
pytest.mark.skip
def test_mark_more():
    print('123')

#如果是在windows平台,则执行时跳过该用例
pytest.mark.skipif(sys.platform == 'win', reason='does not run on windows')
def test_mark_more():
    print('123')

#在方法中跳过某些步骤
#如果是在windows平台,则执行时跳过if后面的语句,只打印start,不打印end
def test_function():
    print('start')
    if sys.platform == 'win':
             pytest.skip('does not run on windows')
    print('end')


## xfail 使用场景
* 与 skip 类似 ,预期结果为 fail ,标记用例为 fail
* 用法:添加装饰器`@pytest.mark.xfail`
pytest.mark.xfail
def test_mark_more():
    print('123')
输出结果为xfail,没有实质性的作用,用于标记一些功能未完成,或者其他原因引起失败的用例。也可以标记xpass(用于区分开这些特殊案例和真正失败、通过的案例)

8.pytest运行多条用例

##运行多条用例
* 执行包下所有的用例:`pytest/py.test [包名]`
* 执行单独一个 pytest 模块:`pytest 文件名.py`
* 运行某个模块里面某个类:`pytest 文件名.py::类名`
* 运行某个模块里面某个类里面的方法:`pytest 文件名.py::类名::方法名`

##运行结果分析
* 常用的:fail/error/pass
* 特殊的结果:warning/deselect(后面会讲)

9.pytest测试用例调度与运行

## 命令行参数-使用缓存状态
* `--lf(--last-failed)` 只重新运行故障。
* `--ff(--failed-first)` 先运行故障然后再运行其余的测试

示例
#-vs打印详细信息
pytest --lf -vs
pytest --ff -vs

10.pytest命令行常用参数

—help 
-x   用例一旦失败(fail/error),就立刻停止执行
--maxfail=num 用例达到
-m  标记用例(后面详细介绍)
-k  执行包含某个关键字的测试用例
-v 打印详细日志
-s 打印输出日志(一般-vs一块儿使用)
—collect-only(测试平台,pytest 自动导入功能 )

示例
pytest —collect-only(只收集所以用例,不执行)
pytest a.py -x
pytest a.py -k "str"(要用双引号)
pytest a.py -vs
pytest a.py --maxfail=3

11. Python 代码执行 pytest

* 使用 main 函数
if __name__ == '__main__':
    # 1、运行当前目录下所有符合规则的用例,包括子目录(test_*.py 和 *_test.py)
    pytest.main()
    # 2、运行test_mark1.py::test_dkej模块中的某一条用例
    pytest.main(['test_mark1.py::test_dkej','-vs'])
    # 3、运行某个 标签
    pytest.main(['test_mark1.py','-vs','-m','dkej'])

运行方式
  `python test_*.py `

* 使用 python -m pytest 调用 pytest(jenkins 持续集成用到)
python -m pytest test_mark1.py

12.pytest异常处理

## 常用的异常处理方法
* try…except
* pytest.raises()

## 异常处理方法 try …except
try:
    可能产生异常的代码块
except [ (Error1, Error2, ... ) [as e] ]:
    处理异常的代码块1
except [ (Error3, Error4, ... ) [as e] ]:
    处理异常的代码块2
except  [Exception]:
    处理其它异常

## 异常处理方法 pytest.raise()
* 可以捕获特定的异常
* 获取捕获的异常的细节(异常类型,异常信息)
* 发生异常,后面的代码将不会被执行
使用场景:正常的异常提示,比如计算器输入非法数字时(查验提示是否正确)
def test_raise():
    with pytest.raises(ValueError, match='must be 0 or None'):
        raise ValueError("value must be 0 or None")

def test_raise1():
    with pytest.raises(ValueError) as exc_info:
        raise ValueError("value must be 42")
    assert exc_info.type is ValueError
    assert exc_info.value.args[0] == "value must be 42"

13. Pytest 结合数据驱动 YAML

##yaml文件介绍
* 对象:键值对的集合,用冒号 “:” 表示
* 数组:一组按次序排列的值,前加 “-”
* 纯量:单个的、不可再分的值
  * 字符串
  * 布尔值
  * 整数
  * 浮点数
  * Null
  * 时间
  * 日期

# 编程语言
languages:
  - PHP
  - Java
  - Python
{languages:[PHP, Java, Python]}

book:
  Python入门: # 书籍名称
    price: 55.5
    author: Lily
    available: True
    repertory: 20
    date: 2018-02-17
  Java入门:
    price: 60
    author: Lily
    available: False
    repertory: Null
    date: 2018-05-11

{book:[{
     python入门:[{
           price:60
                       },
                         {
           author: Lily
                         }
                       ]
          },
           {
     java入门:[{
           price:55.5
                       },
                         {
           author: Lily
                         }
                       ]
          }
      ]
}


##yaml文件使用
* 查看 yaml 文件
  * pycharm
  * txt 记事本
* 读取 yaml 文件
  * 安装:`pip install pyyaml`
  * 方法:`yaml.safe_load(f)`配置文件读取,返回python字典
  * 方法:`yaml.safe_dump(f)`返回python生成器对象

import yaml
file_path = './my.yaml'
with open(file_path, 'r', encoding='utf-8') as f:
    data = yaml.safe_load(f)

## 工程目录结构
* data 目录:存放 yaml 数据文件
* func 目录:存放被测函数文件
* testcase 目录:存放测试用例文件
.
├── data
│   └── data.yaml
├── func
│   ├── __init__.py
│   └── operation.py
└── testcase
    ├── __init__.py
    └── test_add.py

##测试准备
* 被测对象:`operation.py`
* 测试用例:`test_add.py`
* 测试数据:`data.yaml`
# operation.py 文件内容
def my_add(x, y):
    result = x + y
    return result
# test_add.py 文件内容
class TestWithYAML:
  @pytest.mark.parametrize('x,y,expected', [[1, 1, 2]])
  def test_add(self, x, y, expected):
    assert my_add(int(x), int(y)) == int(expected)

# data.yaml 文件内容
-
  - 1
  - 1
  - 2
-
  - 3
  - 6
  - 9
-
  - 100
  - 200
  - 300
[[1,1,2], [3,6,9], [100,200,300]]

## Pytest 数据驱动结合 yaml 文件
# 读取yaml文件
def get_yaml():
    """
    获取json数据
    :return: 返回数据的结构:[[1, 1, 2], [3, 6, 9], [100, 200, 300]]
    """
    with open('../datas/data.yaml', 'r') as f:
        data = yaml.safe_load(f)
        return data

14.Pytest 结合数据驱动 Excel

## 读取 Excel 文件
* 第三方库
  * `xlrd`
  * `xlwings`
  * `pandas`
* openpyxl
  * 官方文档: https://openpyxl.readthedocs.io/en/stable/

## openpyxl 库的操作
import openpyxl
# 获取工作簿
book = openpyxl.load_workbook('../data/params.xlsx')
# 读取工作表
sheet = book.active
# 读取单个单元格
cell_a1 = sheet['A1']
cell_a3 = sheet.cell(column=1, row=3)  # A3
# 读取多个连续单元格
cells = sheet["A1":"C3"]
# 获取单元格的值
cell_a1.value


## Pytest 数据驱动结合 Excel 文件
# 读取Excel文件
import openpyxl
import pytest

def get_excel():
    # 获取工作簿
    book = openpyxl.load_workbook('../data/params.xlsx')

    # 获取活动行(非空白的)
    sheet = book.active

    # 提取数据,格式:[[1, 2, 3], [3, 6, 9], [100, 200, 300]]
    # 先获取第一行的数据,再获取第二行的数据,以此类推
    values = []
    for row in sheet:
        line = []
        for cell in row:
            line.append(cell.value)
        values.append(line)
    return values

15.Pytest 结合数据驱动 csv

## csv 文件使用
* 读取数据
  * 内置函数:`open()`
  * 内置模块:`csv`
* 方法:`csv.reader(iterable)`
  * 参数:iterable ,文件或列表对象
  * 返回:迭代器,每次迭代会返回一行数据。

# 读取csv文件内容
def get_csv():
    with open('demo.csv', 'r') as file:
        raw = csv.reader(file)
        for line in raw:
            print(line)

## Pytest 数据驱动结合 csv 文件
# 读取 data目录下的 params.csv 文件
import csv

def get_csv():
    """
    获取csv数据
    :return: 返回数据的结构:[[1, 1, 2], [3, 6, 9], [100, 200, 300]]
    """
    with open('../data/params.csv', 'r') as file:
        raw = csv.reader(file)
        data = []
        for line in raw:
            data.append(line)
    return data

16.Pytest 结合数据驱动 json

## json 文件使用
* 查看 json 文件
  * pycharm
  * txt 记事本
* 读取 json 文件
  * 内置函数 open()
  * 内置库 json
  * 方法:`json.loads()`
  * 方法:`json.dumps()`

# 读取json文件内容
def get_json():
    with open('demo.json', 'r') as f:
        data = json.loads(f.read())
        print(data)

## Pytest 数据驱动结合 json 文件
# params.json 文件内容
{
  "case1": [1, 1, 2],
  "case2": [3, 6, 9],
  "case3": [100, 200, 300]
}

# 读取json文件
def get_json():
    """
    获取json数据
    :return: 返回数据的结构:[[1, 1, 2], [3, 6, 9], [100, 200, 300]]
    """
    with open('../data/params.json', 'r') as f:
        data = json.loads(f.read())
        return list(data.values())

17. Fixture 用法

## Fixture 特点及优势
* 1、命令灵活:对于 setup,teardown,可以不起这两个名字
* 2、数据共享:在 conftest.py 配置⾥写⽅法可以实现数据共享,不需要 import 导⼊。可以跨⽂件共享
* 3、scope 的层次及神奇的 **yield** 组合相当于各种 setup 和 teardown
* 4、实现参数化

## Fixture 在自动化中的应用- 基本用法
* **场景:**
测试⽤例执⾏时,有的⽤例需要登陆才能执⾏,有些⽤例不需要登陆。
setup 和 teardown ⽆法满⾜。fixture 可以。默认 scope(范围)function
* **步骤:**
  * 1.导⼊ pytest
  * 2.在登陆的函数上⾯加@pytest.fixture()
  * 3.在要使⽤的测试⽅法中传⼊(登陆函数名称),就先登陆
  * 4.不传⼊的就不登陆直接执⾏测试⽅法。

示例
@pytest.fixture()
def login():
      print("完成登录")

def test_search():
      print("搜索")

def test_order(login):
      print("下单")