本文为霍格沃兹测试开发学社学员学习笔记分享
原文链接:https://ceshiren.com/t/topic/26755
类型 | 规则 |
---|---|
文件 | test_开头 或者 _test 结尾 |
类 | Test 开头 |
方法/函数 | test_开头 |
注意:测试类中不可以添加__init__ 构造函数 |
注意:pytest对于测试包的命名没有要求
方法:类中定义的函数
函数:类外面定义的函数
谷歌风格开源项目风格指南:
https://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/python_style_rules/ 2
断言的概念
断言(assert),是一种在程序中的一阶逻辑(如:一个结果为真或假的逻辑判断式),目的为了表示与验证软件开发者预期的结果。当程序执行到断言的位置时,对应的断言应该为真。若断言不为真时,程序会中止执行,并给出错误信息。
断言的用法
断言写法
assert <表达式>
assert <表达式>,<描述>
assert <bool expression>;
assert <bool expression> : <message>;
示例一
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 下执行"
类型 | 规则 |
---|---|
setup_module/teardown_module | 全局模块级 |
setup_class/teardown_class | 类级,只在类中前后运行一次 |
setup_function/teardown_function | 函数级,在类外 |
setup_method/teardown_method | 方法级,类中的每个方法执行前后 |
setup/teardown | 在类中,运行在调用方法的前后(重点) |
search_list = ['appium','selenium','pytest']
@pytest.mark.parametrize('name',search_list)
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
@pytest.mark.parametrize("test_input,expected",[
("3+5",8),("2+5",7),("7+5",12)
],ids=['add_3+5=8','add_2+5=7','add_3+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)
],ids=["3和5相加","2和5相加","7和5相加"])
def test_mark_more(test_input,expected):
assert eval(test_input) == expected
ids不支持中文,默认是unicode编码格式,可以用如下方法转换
# 在项目(最末一级)下创建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")
接口测试中用的较多,因为接口的值很多
两组数据
对应有几种组合形势 ?
@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}")
执行方向:由近致远
场景:只执行符合要求的某一部分用例 可以把一个web项目划分多个模块,然后指定模块名称执行。
解决: 在测试用例方法上加 @pytest.mark.标签名
执行: -m 执行自定义标记的相关用例
pytest -s 文件名 -m=webtest
pytest -s test_mark_zi_09.py -m apptest
pytest -s test_mark_zi_09.py -m "not ios"
(必须要是双引号)如何解决warnings问题
[pytest]
# 在项目目录下创建一个pytest.ini放标签,这样这些标签就不会warning。而且要换行写,也不要顶头写,会被认为是key
# 在这里注册好标签名后,pytest可以识别
markers = str
bignum
float
int
minus
zero
调试时不想运行这个用例
标记无法在某些平台上运行的测试功能
在某些版本中执行,其他版本中跳过
比如:当前的外部资源不可用时跳过
解决 1:添加装饰器
@pytest.mark.skip
@pytest.mark.skipif
解决 2:代码中添加跳过代码
pytest.skip(reason)
# 形式一:跳过这个方法
@pytest.mark.skip(reason="存在bug")
def test_double_str():
print("代码未开发完")
assert 'aa' == 'aa'
# 形式二:跳过这个方法
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on win32")
def test_case():
print(sys.platform)
assert True
# 形式二:在代码中跳过代码
def check_login():
return False
def test_double_str():
print("start")
if not check_login():
pytest.skip("未登录,不进行下去")
print("end")
# ============================= 1 skipped in 0.02s ======
用于标记此用例可能会失败,当脚本失败时,测试报告也不会打印错误追踪,只是会显示xfail状态。xfail的主要作用是比如在进行测试提前时,当产品某功能尚未开发完成而进行自动化脚本开发,当然此时也可以把这些脚本注释起来,但这不是pytest推荐的做法,pytest推荐使用xfail标记,如此则虽然产品功能尚未开发完成,但是自动化脚本已经可以跑起来了,只不过在测试报告中会显示xfail而已。
@pytest.mark.xfail
@pytest.mark.xfail
def test_case():
print("test_xfail 方法执行")
assert 2 == 2
# XPASS [100%]test_xfail
@pytest.mark.xfail
def test_case():
print("test_xfail 方法执行")
assert 1 == 2
# XFAIL [100%]test_xfail 方法执行
xfail = pytest.mark.xfail
@xfail(reason="bug 110")
def test_case():
print("test_xfail 方法执行")
assert 1 == 2
# XFAIL (bug 110) [100%]test_xfail 方法执行
如果要进入某个文件所在的目录终端,可以右键文件->选择open in terminal
执行包下所有的用例:pytest/py.test [包名]
执行单独一个 pytest 模块:pytest 文件名.py
运行某个模块里面某个类:pytest 文件名.py::类名
运行某个模块里面某个类里面的方法:pytest 文件名.py::类名::方法名
加-v可以具体展示,如pytest -v 文件名.py::类名::方法名 (-v在前在后都行)
前面已经介绍了几种执行用例的方法,一个是点击代码方法或类的左侧绿色箭头,一个是右键测试用例,一个是终端pytest解释器执行,我们也可以用python解释器执行
方法一:使用 main 函数
方法二:使用 python -m pytest 调用 pytest(jenkins 持续集成用到),相当于在原来pytest 用例前加了python -m。
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 `
try:
可能产生异常的代码块
except [ (Error1, Error2, ... ) [as e] ]:
处理异常的代码块1
except [ (Error3, Error4, ... ) [as e] ]:
处理异常的代码块2
except [Exception]:
处理其它异常
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"
这样可以选择一个异常
def test_raise():
with pytest.raises((ZeroDivisionError,ValueError)):
raise ZeroDivisionError("value must be 0 or None")
什么是数据驱动?
应用:
对象:键值对的集合,用冒号 “:” 表示
数组:一组按次序排列的值,前加 “-”
纯量:单个的、不可再分的值
# 编程语言
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
相当于:
languages:['PHP', 'Java', 'Python'] # languages是key值
查看 yaml 文件
读取 yaml 文件
pip install pyyaml
yaml.safe_load(f)
(yaml->python)yaml.safe_dump(f)
(python->yaml)import yaml
file_path = './my.yaml'
with open(file_path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
# 工程目录结构
.
├── 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
# 读取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
第三方库
xlrd
xlwings
pandas
openpyxl
pip install openpyxl
import 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
# 工程目录结构
.
├── data
│ └── params.excel
├── func
│ ├── __init__.py
│ └── operation.py
└── testcase
├── __init__.py
└── test_add.py
operation.py
test_add.py
# operation.py 文件内容
def my_add(x, y):
result = x + y
return result
# test_add.py 文件内容
class TestWithEXCEL:
@pytest.mark.parametrize('x,y,expected', get_excel())
def test_add(self, x, y, expected):
assert my_add(int(x), int(y)) == int(expected)
params.xlsx
注意:.xlsx文件要在外面创建,不要在编辑器里创建
# 读取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
Linux从入门到高级,linux,¥5000
web自动化测试进阶,python,¥3000
app自动化测试进阶,python,¥6000
Docker容器化技术,linux,¥5000
测试平台开发与实战,python,¥8000
读取数据
open()
csv
方法:csv.reader(iterable)
# 读取csv文件内容
def get_csv():
with open('demo.csv', 'r') as file:
raw = csv.reader(file)
for line in raw:
print(line)
# 工程目录结构
.
├── data
│ └── params.csv
├── func
│ ├── __init__.py
│ └── operation.py
└── testcase
├── __init__.py
└── test_add.py
operation.py
test_add.py
params.csv
# operation.py 文件内容
def my_add(x, y):
result = x + y
return result
# test_add.py 文件内容
class TestWithCSV:
@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)
# params.csv 文件内容
1,1,2
3,6,9
100,200,300
# 读取 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
json 是 JS 对象
全称是 JavaScript Object Notation
是一种轻量级的数据交换格式
json 结构
{"key": value}
[value1, value2 ...]
{
"name:": "hogwarts ",
"detail": {
"course": "python",
"city": "北京"
},
"remark": [1000, 666, 888]
}
查看 json 文件
读取 json 文件
json.loads()
json.dumps()
# 读取json文件内容
def get_json():
with open('demo.json', 'r') as f:
data = json.loads(f.read())
print(data)
operation.py
test_add.py
params.json
# operation.py 文件内容
def my_add(x, y):
result = x + y
return result
# test_add.py 文件内容
class TestWithJSON:
@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)
# 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())
场景:
测试⽤例执⾏时,有的⽤例需要登陆才能执⾏,有些⽤例不需要登陆。
setup 和 teardown ⽆法满⾜。fixture 可以。默认 scope(范围)function
步骤:
# test_fixture.py
import pytest
# 定义登录的fixture
@pytest.fixture()
def login():
print("完成登录操作")
def test_search():
print("搜索")
def test_cart(login): #不需要把login放在函数里面,只要传参就可以
print("购物车")
def test_order(login):
print("下单")
取值 | 范围 | 说明 |
---|---|---|
function | 函数级 | 每一个函数或方法都会调用 |
class | 类级别 | 每个测试类只运行一次 |
module | 模块级 | 每一个.py 文件调用一次 |
package | 包级 | 每一个 python 包只调用一次(暂不支持) |
session | 会话级 | 每次会话只需要运行一次,会话内所有方法及类,模块都共享这个方法 |
注:整个项目就是一个会话
import pytest
# 定义登录的fixture
@pytest.fixture(scope="class")
def login():
print("完成登录操作")
def test_search(login):
print("搜索")
def test_cart(login):
print("购物车")
def test_order(login):
print("下单")
class TestDemo:
def test_case1(self,login):
print("case1")
def test_case2(self,login):
print("case2")
你已经可以将测试⽅法【前要执⾏的或依赖的】解决了,
测试⽅法后销毁清除数据的要如何进⾏呢?
通过在 fixture 函数中加⼊ yield 关键字,yield 是调⽤第⼀次返回结果,
第⼆次执⾏它下⾯的语句返回。
在@pytest.fixture(scope=module)。
在登陆的⽅法中加 yield,之后加销毁清除的步骤
import pytest
# 定义登录的fixture
@pytest.fixture(scope="class")
def login():
# setup 操作
print("完成登录操作")
token = "abcd"
username = "hogwarts"
yield token, username # 相当于return
# teardown 操作
print("完成登出操作")
def test_search(login):
token, username = login
print(f"token:{token},name:{username}")
print("搜索")
def test_cart(login):
print("购物车")
def test_order(login):
print("下单")
class TestDemo:
def test_case1(self,login):
print("case1")
def test_case2(self,login):
print("case2")
你与其他测试⼯程师合作⼀起开发时,公共的模块要在不同⽂件中,要在⼤家都访问到的地⽅。
使⽤ conftest.py 这个⽂件进⾏数据共享,并且他可以放在不同位置起着不同的范围共享作⽤。
前提:
执⾏:
步骤:
场景:
不想原测试⽅法有任何改动,或全部都⾃动实现⾃动应⽤,
没特例,也都不需要返回值时可以选择⾃动应⽤
解决:
使⽤ fixture 中参数 autouse=True 实现
步骤:
在⽅法上⾯加 @pytest.fixture(autouse=True)
比如要实现fixture时session级别的,就要每个用例都添加fixture方法。可以通过自动应用来避免。
问题:那yield返回参数的怎么办?
场景:
测试离不开数据,为了数据灵活,⼀般数据都是通过参数传的
解决:
fixture 通过固定参数 request 传递
步骤:
在 fixture 中增加@pytest.fixture(params=[1, 2, 3, ‘linda’])
在⽅法参数写 request,方法体里面使用 request.param 接收参数
# test_fixture_param.py
import pytest
@pytest.fixture(params=[["selenium",123], ["appium",123]])
def login(request):
print(f"用户名:{request.param}")
return request.param
def test_demo1(login):
print(f"demo1 case:数据为{login}")