pytest 是一个非常灵活且强大的测试框架,它支持简单的单元测试到复杂的功能测试。显著特点是其简洁的语法,可以无需继承 TestCase 类直接使用函数来编写测试用例,并通过 assert语句 进行断言。还支持参数化测试、插件系统以及丰富的 assertion rewriting 功能,使编写测试更加便捷和直观。我们可以使用它进行自动化测试。
PS: 本文基于pytest 8.3.3,建议使用PyCharm编写并运行测试套件,方便。
安装 pytest
在终端中安装:
pip install -U pytest
在PyCharm中安装:
测试用例
在 pytest 框架中,会识别名称前缀为 test_ 或 _test 的函数为测试用例。
# C:\PythonTest\Test\test_mod.py
list_list = [1, 2, 3, 'hello!']
def test_list_fail():
assert 'hello1!' == list_list[-1], "两个值不同"
def test_list_pass():
assert 'hello!' == list_list[-1], "两个值不同"
test_ 前缀表示定义的函数是个测试用例。
assert 用于断言。如 assert 后面的第一个值为 True(真) ,则该测试用例继续进行直至结束;如果为 False(假) ,则测试失败,并且 pytest 会停止执行该测试用例并报告错误。 assert 后面的第一个值可以是表达式、函数返回值,甚至直接是布尔值。 assert 后面的第二个值是自定义的失败描述,是一个字符串。
函数 test_answer() 就是一个测试用例,运行后结果如下:
============================= test session starts =============================
collecting ... collected 2 items
test_mod.py::test_list_fail FAILED [ 50%]
test_mod.py:3 (test_list_fail)
'hello1!' != 'hello!'
预期:'hello!'
实际:'hello1!'
<点击以查看差异>
def test_list_fail():
> assert 'hello1!' == list_list[-1], "两个值不同"
E AssertionError: 两个值不同
E assert 'hello1!' == 'hello!'
E
E - hello!
E + hello1!
E ? +
test_mod.py:5: AssertionError
test_mod.py::test_list_pass PASSED [100%]
========================= 1 failed, 1 passed in 0.04s =========================
运行后,会输出一个测试报告。 test_mod.py::test_list_fail FAILED 表示测试用例- test_answer 断言失败,测试用例未通过,失败描述是:“E AssertionError: 两个值不同” 。
而 test_mod.py::test_list_pass PASSED 表示测试用例- test_hanshu、test_list 断言成功,测试用例通过。
终端中运行 C:\PythonTest\Test\test_mod.py 中所有测试用例:
cd C:\PythonTest\Test\
pytest test_mod.py
PyCharm中运行 C:\PythonTest\Test\test_mod.py 中所有测试用例:
终端中运行 C:\PythonTest\Test\test_mod.py 中的 test_list_fail 测试用例:
cd C:\PythonTest\Test\
pytest test_mod.py::test_list_fail
PyCharm中运行 C:\PythonTest\Test\test_mod.py 中的 test_list_fail 测试用例:
实际测试中,对请求结果进行断言
首先说明请求结果内容:
[
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},
{
"id": 2,
"name": "Ervin Howell",
"username": "Antonette",
"email": "Shanna@melissa.tv",
"address": {
"street": "Victor Plains",
"suite": "Suite 879",
"city": "Wisokyburgh",
"zipcode": "90566-7771",
"geo": {
"lat": "-43.9509",
"lng": "-34.4618"
}
},
"phone": "010-692-6593 x09125",
"website": "anastasia.net",
"company": {
"name": "Deckow-Crist",
"catchPhrase": "Proactive didactic contingency",
"bs": "synergize scalable supply-chains"
}
},
{
"id": 3,
"name": "Clementine Bauch",
"username": "Samantha",
"email": "Nathan@yesenia.net",
"address": {
"street": "Douglas Extension",
"suite": "Suite 847",
"city": "McKenziehaven",
"zipcode": "59590-4157",
"geo": {
"lat": "-68.6102",
"lng": "-47.0653"
}
},
"phone": "1-463-123-4447",
"website": "ramiro.info",
"company": {
"name": "Romaguera-Jacobson",
"catchPhrase": "Face to face bifurcated interface",
"bs": "e-enable strategic applications"
}
},
{
"id": 4,
"name": "Patricia Lebsack",
"username": "Karianne",
"email": "Julianne.OConner@kory.org",
"address": {
"street": "Hoeger Mall",
"suite": "Apt. 692",
"city": "South Elvis",
"zipcode": "53919-4257",
"geo": {
"lat": "29.4572",
"lng": "-164.2990"
}
},
"phone": "493-170-9623 x156",
"website": "kale.biz",
"company": {
"name": "Robel-Corkery",
"catchPhrase": "Multi-tiered zero tolerance productivity",
"bs": "transition cutting-edge web services"
}
},
{
"id": 5,
"name": "Chelsey Dietrich",
"username": "Kamren",
"email": "Lucio_Hettinger@annie.ca",
"address": {
"street": "Skiles Walks",
"suite": "Suite 351",
"city": "Roscoeview",
"zipcode": "33263",
"geo": {
"lat": "-31.8129",
"lng": "62.5342"
}
},
"phone": "(254)954-1289",
"website": "demarco.info",
"company": {
"name": "Keebler LLC",
"catchPhrase": "User-centric fault-tolerant solution",
"bs": "revolutionize end-to-end systems"
}
},
{
"id": 6,
"name": "Mrs. Dennis Schulist",
"username": "Leopoldo_Corkery",
"email": "Karley_Dach@jasper.info",
"address": {
"street": "Norberto Crossing",
"suite": "Apt. 950",
"city": "South Christy",
"zipcode": "23505-1337",
"geo": {
"lat": "-71.4197",
"lng": "71.7478"
}
},
"phone": "1-477-935-8478 x6430",
"website": "ola.org",
"company": {
"name": "Considine-Lockman",
"catchPhrase": "Synchronised bottom-line interface",
"bs": "e-enable innovative applications"
}
},
{
"id": 7,
"name": "Kurtis Weissnat",
"username": "Elwyn.Skiles",
"email": "Telly.Hoeger@billy.biz",
"address": {
"street": "Rex Trail",
"suite": "Suite 280",
"city": "Howemouth",
"zipcode": "58804-1099",
"geo": {
"lat": "24.8918",
"lng": "21.8984"
}
},
"phone": "210.067.6132",
"website": "elvis.io",
"company": {
"name": "Johns Group",
"catchPhrase": "Configurable multimedia task-force",
"bs": "generate enterprise e-tailers"
}
},
{
"id": 8,
"name": "Nicholas Runolfsdottir V",
"username": "Maxime_Nienow",
"email": "Sherwood@rosamond.me",
"address": {
"street": "Ellsworth Summit",
"suite": "Suite 729",
"city": "Aliyaview",
"zipcode": "45169",
"geo": {
"lat": "-14.3990",
"lng": "-120.7677"
}
},
"phone": "586.493.6943 x140",
"website": "jacynthe.com",
"company": {
"name": "Abernathy Group",
"catchPhrase": "Implemented secondary concept",
"bs": "e-enable extensible e-tailers"
}
},
{
"id": 9,
"name": "Glenna Reichert",
"username": "Delphine",
"email": "Chaim_McDermott@dana.io",
"address": {
"street": "Dayna Park",
"suite": "Suite 449",
"city": "Bartholomebury",
"zipcode": "76495-3109",
"geo": {
"lat": "24.6463",
"lng": "-168.8889"
}
},
"phone": "(775)976-6794 x41206",
"website": "conrad.com",
"company": {
"name": "Yost and Sons",
"catchPhrase": "Switchable contextually-based project",
"bs": "aggregate real-time technologies"
}
},
{
"id": 10,
"name": "Clementina DuBuque",
"username": "Moriah.Stanton",
"email": "Rey.Padberg@karina.biz",
"address": {
"street": "Kattie Turnpike",
"suite": "Suite 198",
"city": "Lebsackbury",
"zipcode": "31428-2261",
"geo": {
"lat": "-38.2386",
"lng": "57.2232"
}
},
"phone": "024-648-3804",
"website": "ambrose.net",
"company": {
"name": "Hoeger LLC",
"catchPhrase": "Centralized empowering task-force",
"bs": "target end-to-end models"
}
}
]
假设我们的预期是请求结果中有 id 为 5、15 的用户信息,我们以此编写测试用例:
import requests
def user():
url = 'https://jsonplaceholder.typicode.com/users'
response = requests.get(url)
results = response.json()
id_list = []
for x in results:
id_list.append(x["id"])
return id_list
def test_user():
id_list = user()
assert 5 in id_list, "请求结果中没有id为5的用户信息"
assert 15 in id_list, "请求结果中没有id为15的用户信息"
运行结果:
============================= test session starts =============================
collecting ... collected 1 item
test_mod.py::test_user FAILED [100%]
test_mod.py:17 (test_user)
15 != [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
预期:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
实际:15
<点击以查看差异>
def test_user():
id_list = user()
assert 5 in id_list, "请求结果中没有id为5的用户信息"
> assert 15 in id_list, "请求结果中没有id为15的用户信息"
E AssertionError: 请求结果中没有id为15的用户信息
E assert 15 in [1, 2, 3, 4, 5, 6, ...]
test_mod.py:21: AssertionError
============================== 1 failed in 0.74s ==============================
”assert 15 in id_list, “请求结果中没有id为15的用户信息"“断言失败,报错: ”E AssertionError: 请求结果中没有id为15的用户信息“,测试用例 test_user 未通过。
测试用例组
在 pytest 中,可以构建测试用例组来放置某个功能、模块的所有测试用例,这个测试用例组就是类(class):
# C:\PythonTest\Test\test_mod.py
class TestClassOne:
def test_one(self):
x = "this"
assert "h" in x, "x 的字符中没有 h"
def test_two(self):
x = "check"
y = ['11', 2]
assert type(x) == type(y), "x 与 y 的类型不一致"
class TestClassTwo:
value = 0
def test_three(self):
self.value = 1
assert self.value == 1, "value 的值不是 1"
def test_four(self):
assert self.value == 1, "value 的值不是 1"
运行结果:
============================= test session starts =============================
collecting ... collected 4 items
test_mod.py::TestClassOne::test_one PASSED [ 25%]
test_mod.py::TestClassOne::test_two FAILED [ 50%]
test_mod.py:5 (TestClassOne.test_two)
<class 'str'> != <class 'list'>
预期:<class 'list'>
实际:<class 'str'>
<点击以查看差异>
self = <test.TestClassOne object at 0x000001A24A75D430>
def test_two(self):
x = "check"
y = ['11', 2]
> assert type(x) == type(y), "x 与 y 的类型不一致"
E AssertionError: x 与 y 的类型不一致
E assert <class 'str'> == <class 'list'>
E + where <class 'str'> = type('check')
E + and <class 'list'> = type(['11', 2])
test_mod.py:9: AssertionError
test_mod.py::TestClassTwo::test_three PASSED [ 75%]
test_mod.py::TestClassTwo::test_four FAILED [100%]
test_mod.py:18 (TestClassTwo.test_four)
0 != 1
预期:1
实际:0
<点击以查看差异>
self = <test.TestClassTwo object at 0x000001A24A786300>
def test_four(self):
> assert self.value == 1, "value 的值不是 1"
E AssertionError: value 的值不是 1
E assert 0 == 1
E + where 0 = <test.TestClassTwo object at 0x000001A24A786300>.value
test_mod.py:20: AssertionError
========================= 2 failed, 2 passed in 0.04s =========================
测试报告中 test_mod.py::TestClassOne::test_one 、test_mod.py::TestClassTwo::test_three 表示测试用例属于哪个测试组。
在终端中运行 C:\PythonTest\Test\test_mod.py 中的 TestClassOne 测试用例组:
cd C:\PythonTest\Test\
pytest test_mod.py::TestClassOne
在PyCharm中运行 C:\PythonTest\Test\test_mod.py 中的 TestClassOne 测试用例组:
在终端中运行 C:\PythonTest\Test\test_mod.py 中 TestClassOne 中的 test_two 测试用例:
cd C:\PythonTest\Test\
pytest test_mod.py::TestClassOne::test_two
在PyCharm中运行 C:\PythonTest\Test\test_mod.py 中 TestClassOne 中的 test_two 测试用例:
assert 断言
在 pytest 中,assert 语句用于断言某个条件是否为真(即布尔值为 True)。如果断言的条件为 True,则测试通过;如果为 False,则测试失败,并且 pytest 会报告具体的失败信息。如:
- 比较表达式
# 等于
assert 10 == 5 # False
# 不等于
assert 10 != 5 # True
# 大于
assert 10 > 5 # True
# 小于
assert 10 < 5 # False
# 大于等于
assert 10 >= 5 # True
# 小于等于
assert 10 <= 5 # False
- 逻辑表达式
# 逻辑与
assert (10 > 5) and (5 < 10) # True
# 逻辑或
assert (10 > 5) or (5 > 10) # True
# 逻辑非
assert not (10 > 5) # False
- 成员表达式
# in 操作符
assert 5 in [1, 2, 3, 4, 5] # True
# not in 操作符
assert 6 not in [1, 2, 3, 4, 5] # True
- 身份表达式
a = [1, 2, 3]
b = [1, 2, 3]
c = a
# is 操作符
assert a is c # True,因为 a 和 c 引用同一个对象
# is not 操作符
assert a is not b # True,因为 a 和 b 是不同的对象
- 布尔值的隐式转换
# 空列表、空字符串、零等会被视为 False,而非空列表、非空字符串、非零等会被视为 True
# 空列表
assert [] # False
# 非空列表
assert [1, 2, 3] # True
# 空字符串
assert "" # False
# 非空字符串
assert "Hello" # True
# 零
assert 0 # False
# 非零
assert 10 # True
- 布尔值
# True
assert True
# False
assert False
测试套件的目录结构
从本篇开始,我们可以写一个测试项目框架。虽说一个项目怎么写都可以,但合理的项目结构可以大大提高开发效率,并使项目更易于管理和维护。基于本章内容,目录结构可以如下:
Project/
│
├── Package/ # 程序目录
│ ├── __init__.py # 包初始化文件,可以定义一些变量或执行一些操作。当然里面什么都不写也可以。
│ ├── module1.py # 测试程序模块,比如连接数据库操作数据,接口请求等操作,推荐按功能封装成类
│ └── module2.py # 测试程序模块,比如连接数据库操作数据,接口请求等操作,推荐按功能封装成类
│
├── Test/ # 测试用例目录
│ ├── __init__.py # 包初始化文件
│ ├── test_module1.py # 测试 module1 的测试用例
│ └── test_module2.py # 测试 module2 的测试用例
├── app.py # 项目启动文件
├── requirements.txt # 项目依赖项列表
└── README.md # 项目说明文档
一个整体的套件,需要有个启动文件 app.py ,测试时运行 app.py 文件。 这个文件可以这样设计::
# app.py
import pytest
import sys
import logging
import argparse
# 设置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def run_tests(test_target):
# 运行测试用例
pytest_args = ["-v", test_target]
try:
# 运行测试
exit_code = pytest.main(pytest_args)
# 根据退出码判断测试是否成功
exit_messages = {
0: "全部测试用例通过",
1: "部分测试用例未通过",
2: "测试过程中有中断或其他非正常终止。",
3: "内部错误",
4: "pytest无法找到任何测试用例",
5: "pytest遇到了命令行解析错误"
}
logging.info(exit_messages.get(exit_code, "未知的退出码"))
except Exception as e:
logging.error(f"运行测试时发生错误: {e}")
return 1
return exit_code
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="使用指定的命令运行 pytest 测试。")
parser.add_argument('test_target', nargs='?', type=str, default="Test/",
help='指定运行命令。 (默认: Test/)')
args = parser.parse_args()
exit_code = run_tests(args.test_target)
sys.exit(exit_code)
argparse 可以处理命令行参数,parser.add_argument('test_target', nargs='?', type=str, default="Test/",help='指定运行命令。 (默认: Test/)')
定义一个位置参数:test_target ,允许为空,默认值为 Test/ 。当运行 app.py 无命令行参数时,会默认运行 Test/ 目录下的所有测试用例。
终端启动:
- 运行所有测试:
cd C:\PythonTest\
python app.py
- 运行特定文件:
cd C:\PythonTest\
python app.py Test/test_module1.py
- 运行特定文件中的特定测试用例:
cd C:\PythonTest\
python app.py Test/test_module1.py::test_case
- 运行特定文件中的特定测试组:
cd C:\PythonTest\
python app.py Test/test_module1.py::TestClass
- 运行特定文件中的特定测试组中的特定测试用例:
cd C:\PythonTest\
python app.py Test/test_module1.py::TestClass::test_case
PyCharm启动:
运行其它测试用例或者测试组,只需要在步骤七输入相关参数,针对不同的测试策略,可以创建不同的运行配置。
THEEND
© 转载需要保留原始链接,未经明确许可,禁止商业使用。CC BY-NC-ND 4.0
...