python +pytest 最强自动化
pytest 是测试最牛的一个测试框架,废话不多说,直接上干货。
1 .Pytest核心功能:
- 非常容易上手,入门简单,文档丰富,文档中有很多实例可以参考。
- 能够支持简单的单元测试和复杂的功能测试。
- 支持参数化。
- 能够执行全部测试用例,也可以挑选部分测试用例执行,并能重复执行失败的用例。
- 支持并发执行,还能运行由nose, unittest编写的测试用例。
- 方便、简单的断言方式。
- 能够生成标准的Junit XML格式的测试结果。
- 具有很多第三方插件,并且可以自定义扩展。
- 方便的和持续集成工具集成。
Pytest的安装方法与安装其他的python软件无异,直接使用pip安装即可。
pip install pytest
安装完成后,可以通过下面方式验证是否安装成功:
py.test --help
如果能够输出帮助信息,则表示安装成功了。
接下来,通过开发一个API自动化测试项目,详细介绍以上这些功能是如何使用的
2. 创建测试项目
我们打开pycharm,新建工程api_test,并新建以下文件config(配置文件)data(放测试数据)tests目录(用来存放测试脚本)utils目录(存放工具)
如下图我们的工程目录:
备注:一定修改运行器:pytest。
接下来,我们可以先写一个简单代码,调试一下。
import pytest
备注:函数命名一定使用test开头或结尾;即和文件命名一样。
def test_a():
a = 1
b = 1
assert a == b
if __name__ == '__main__':
pytest.main('-q test_class.py')
我们可以看到 我们的断言是OK的,本条case就是测试通过。
3 . 编写测试用例
在这部分,我们以测试豆瓣电影列表API和电影详情API为例,编写测试用例。
这两个API信息如下:
电影列表接口:http://api.douban.com/v2/movie/in_theaters?apikey=0df993c66c0c636e29ecbb5344252a4a&start=0&count=10
电影详情:https://api.douban.com/v2/movie/subject/30261964?apikey=0df993c66c0c636e29ecbb5344252a4a
我们先写电影列表API的自动化测试用例,设置3个校验点:
1 . 验证请求中的start与响应中的start一致。
2 . 验证请求中的count与响应中的count一致。
3 . 验证响应中的title是"正在上映的电影-上海"。
接下来我们就可以在test_request中编写代码
import requests
import pytest
def test_in_theaters(self):
host = "http://api.douban.com"
path = "/v2/movie/in_theaters"
params = {"apikey": "0df993c66c0c636e29ecbb5344252a4a",
"start": 0,
"count": 10
}
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
}
r = requests.request("GET", url=host + path, headers=headers, params=params)
response = r.json()
assert response["count"] == params["count"]
assert response["start"] == params["start"]
assert response["title"] == "正在上映的电影-上海", "实际的标题是:{}"
if __name__ == '__main__':
pytest.main('-q test_class.py')
不会接口请求的请看我之前的request文章
这就是测试用例了,接下来我们就要完成数据分离。
4. 数据与脚本分离
我们上一节将测试数据和测试代码放到了同一个py文件中,而且是同一个测试方法中,产生了紧耦合,会导致修改测试数据或测试代码时,可能会相互影响,不利于测试数据和测试脚本的维护。比如,为测试用例添加几组新的测试数据,除了准备测试数据外,还要修改测试代码,降低了测试代码的可维护性。
另外接口测试往往是数据驱动的测试,测试数据和测试代码放到一起也不方便借助Pytest做参数化。
接下来我们把分离的数据放到data文件中,在data/目录下创建一个用于存放测试数据的Yaml文件test_in_theaters.yaml,内容如下:(使用表格的也可以)
---
tests:
- case: 验证响应中start和count与请求中的参数一致
http:
method: GET
path: /v2/movie/in_theaters
headers:
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36
params:
apikey: 0df993c66c0c636e29ecbb5344252a4a
start: 0
count: 10
expected:
response:
title: 正在上映的电影-上海
count: 10
start: 0
熟悉Yaml格式的同学,应该很容易看懂上面测试数据文件的内容。这个测试数据文件中,有一个数组tests,里面包含的是一条完整的测试数据。一个完整的测试数据由三部分组成:
- case,表示测试用例名称。
- http,表示请求对象。
- expected,表示预期结果。
http这个请求对象包含了被测接口的所有参数,包括请求方法、请求路径、请求头、请求参数。
expected表示预期结果,上面的测试数据中,只列出了对请求响应的预期值,实际测试中,还可以列出对数据库的预期值。
测试脚本也要做相应的改造,需要读取test_in_theaters.yaml文件获取请求数据和预期结果,然后通过requests发出请求。修改后的测试代码如下:
import requests
import yaml
def get_test_data(test_data_path):
case = [] # 存储测试用例名称
http = [] # 存储请求对象
expected = [] # 存储预期结果
with open(test_data_path) as f:
dat = yaml.load(f.read(), Loader=yaml.SafeLoader)
test = dat['tests']
for td in test:
case.append(td.get('case', ''))
http.append(td.get('http', {}))
expected.append(td.get('expected', {}))
parameters = zip(case, http, expected)
return case, parameters
cases, parameters = get_test_data("/Users/chunming.liu/learn/api_pytest/data/test_in_theaters.yaml")
list_params=list(parameters)
class TestInTheaters(object):
def test_in_theaters(self):
host = "http://api.douban.com"
r = requests.request(list_params[0][1]["method"],
url=host + list_params[0][1]["path"],
headers=list_params[0][1]["headers"],
params=list_params[0][1]["params"])
response = r.json()
assert response["count"] == list_params[0][2]['response']["count"]
assert response["start"] == list_params[0][2]['response']["start"]
assert response["total"] == len(response["subjects"])
assert response["title"] == list_params[0][2]['response']["title"], "实际的标题是:{}".format(response["title"])
注意,读取Yaml文件,需要安装PyYAML包。
测试脚本中定义了一个读取测试数据的函数get_test_data,通过这个函数从测试数据文件test_in_theaters.yaml中读取到了测试用例名称case,请求对象http和预期结果expected。这三部分分别是一个列表,通过zip将他们压缩到一起。
测试方法test_in_theaters并没有太大变化,只是发送请求所使用的测试数据不是写死的,而是来自于测试数据文件了。
通常情况下,读取测试数据的函数不会定义在测试用例文件中,而是会放到utils包中,比如放到utils/commonlib.py中。至此,整个项目的目录结构应该是如下所示:
这样,我们修改测试脚本,就修改test_in_theaters.py,变更测试数据,就修改test_in_theaters.yaml。但是目前看,感觉好像并没有真正看到测试数据和脚本分离的厉害之处,或者更加有价值的地方,那么我们接着往下看。
5. 参数化
上面我们将测试数据和测试脚本相分离,如果要为测试用例添加更多的测试数据,往tests数组中添加更多的同样格式的测试数据即可。这个过程叫作参数化。
参数化的意思是对同一个接口,使用多种不同的输入对其进行测试,以验证是否每一组输入参数都能得到预期结果。Pytest提供了pytest.mark.paramtrize这种方式来进行参数化,我们先看下官方网站提供的介绍pytest.mark.paramtrize用法的例子:
# content of tests/test_time.py
import pytest
from datetime import datetime, timedelta
testdata = [
(datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)),
(datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),
]
@pytest.mark.parametrize("a,b,expected", testdata)
def test_timedistance_v0(a, b, expected):
diff = a - b
assert diff == expected
执行上面的脚本将会得到下面的输出,测试方法test_timedistance_v0被执行了两遍,第一遍执行用的测试数据是testdata列表中的第一个元组,第二遍执行时用的测试数据是testdata列表中的第二个元组。这就是参数化的效果,同一个脚本可以使用不同的输入参数执行测试。
照猫画虎,对我们自己的测试项目中的测试脚本进行如下修改。
import pytest
import requests
from utils.commlib import get_test_data
cases, list_params = get_test_data("/Users/chunming.liu/learn/api_pytest/data/test_in_theaters.yaml")
class TestInTheaters(object):
@pytest.mark.parametrize("case,http,expected", list(list_params), ids=cases)
def test_in_theaters(self, case, http, expected):
host = "http://api.douban.com"
r = requests.request(http["method"],
url=host + http["path"],
headers=http["headers"],
params=http["params"])
response = r.json()
assert response["count"] == expected['response']["count"]
assert response["start"] == expected['response']["start"]
assert response["title"] == expected['response']["title"], "实际的标题是:{}".format(response["title"])
在测试方法上面添加了一个装饰器@pytest.mark.parametrize,装饰器会自动对list(list_params)解包并赋值给装饰器的第一参数。装饰器的第一个参数中逗号分隔的变量可以作为测试方法的参数,在测试方法内就可以直接获取这些变量的值,利用这些值发起请求和进行断言。装饰器还有一个参数叫ids,这个值作为测试用例的名称将打印到测试结果中。
在执行修改后的测试脚本前,我们在测试数据文件再增加一组测试数据,现在测试数据文件中,包含了两组测试数据:
---
tests:
- case: 验证响应中start和count与请求中的参数一致
http:
method: GET
path: /v2/movie/in_theaters
headers:
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36
params:
apikey: 0df993c66c0c636e29ecbb5344252a4a
start: 0
count: 10
expected:
response:
title: 正在上映的电影-上海
count: 10
start: 0
- case: 验证响应中title是"正在上映的电影-北京"
http:
method: GET
path: /v2/movie/in_theaters
headers:
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36
params:
apikey: 0df993c66c0c636e29ecbb5344252a4a
start: 1
count: 5
expected:
response:
title: 正在上映的电影-北京
count: 5
start: 1
现在我们执行一下测试脚本,看看效果:
$ export PYTHONPATH=/Users/chunming.liu/learn/api_pytest
$ py.test tests/test_in_theaters.py
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/chunming.liu/learn/api_pytest, inifile: pytest.ini
collected 2 items
tests/test_in_theaters.py F. [100%]
============================================================ FAILURES ============================================================
___________________________________ TestInTheaters.test_in_theaters[验证响应中start和count与请求中的参数一致] ___________________________________
self = <test_in_theaters.TestInTheaters object at 0x102659510>, case = '验证响应中start和count与请求中的参数一致'
http = {'headers': {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chr...T', 'params': {'apikey': '0df993c66c0c636e29ecbb5344252a4a', 'count': 10, 'start': 0}, 'path': '/v2/movie/in_theaters'}
expected = {'response': {'count': 10, 'start': 0, 'title': '正在上映的电影-上海'}}
@pytest.mark.parametrize("case,http,expected", list(list_params), ids=cases)
def test_in_theaters(self, case, http, expected):
host = "http://api.douban.com"
r = requests.request(http["method"],
url=host + http["path"],
headers=http["headers"],
params=http["params"])
response = r.json()
assert response["count"] == expected['response']["count"]
assert response["start"] == expected['response']["start"]
> assert response["title"] == expected['response']["title"], "实际的标题是:{}".format(response["title"])
E AssertionError: 实际的标题是:正在上映的电影-北京
E assert '正在上映的电影-北京' == '正在上映的电影-上海'
E - 正在上映的电影-上海
E ? ^^
E + 正在上映的电影-北京
E ? ^^
tests/test_in_theaters.py:20: AssertionError
==================================================== short test summary info =====================================================
FAILED tests/test_in_theaters.py::TestInTheaters::test_in_theaters[\u9a8c\u8bc1\u54cd\u5e94\u4e2dstart\u548ccount\u4e0e\u8bf7\u6c42\u4e2d\u7684\u53c2\u6570\u4e00\u81f4]
================================================== 1 failed, 1 passed in 0.69s ===================================================
从结果看,Pytest收集到了2个items,测试脚本执行了两遍,第一遍执行用第一组测试数据,结果是失败(F),第二遍执行用第二组测试数据,结果是通过(.)。执行完成后的summary info部分,看到了一些Unicode编码,这里其实是ids的内容,因为是中文,所以默认这里显示Unicode编码。为了显示中文,需要在测试项目的根目录下创建一个Pytest的配置文件pytest.ini,在其中添加如下代码:
[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
再次执行测试脚本,在测试结果的summary_info部分,则会显示正确中文内容了。
FAILED tests/test_in_theaters.py::TestInTheaters::test_in_theaters[验证响应中start和count与请求中的参数一致] - AssertionError: ...
按照这种参数化的方法,如果想修改或者添加测试数据,只需要修改测试数据文件即可。
现在,自动化测试项目的目录结构应该是如下这样:
6. 测试配置管理
05小节的自动化测试代码中,host是写在测试脚本中的,这种硬编码方式显然是不合适的。这个host在不同的测试脚本都会用到,应该放到一个公共的地方来维护。如果需要对其进行修改,那么只需要修改一个地方就可以了。根据我的实践经验,将其放到config文件夹中,是比较好的。
除了host外,其他与测试环境相关的配置信息也可以放到config文件夹中,比如数据库信息、kafka连接信息等,以及与测试环境相关的基础测试数据,比如测试账号。很多时候,我们会有不同的测试环境,比如dev环境、test环境、stg环境、prod环境等。我们可以在config文件夹下面创建子目录来区分不同的测试环境。因此config文件夹,应该是类似这样的结构:
├── config
│ ├── prod
│ │ └── config.yaml
│ └── test
│ └── config.yaml
在config.yaml中存放不同环境的配置信息,以前面的例子为例,应该是这样:
host:
douban: http://api.douban.com
将测试配置信息从脚本中拆分出来,就需要有一种机制将其读取到,才能在测试脚本中使用。Pytest提供了fixture机制,通过它可以在测试执行前执行一些操作,在这里我们利用fixture提前读取到配置信息。我们先对官方文档上的例子稍加修改,来介绍fixture的使用。请看下面的代码:
import pytest
备注:fixture是前置
@pytest.fixture
def smtp_connection():
import smtplib
connection = smtplib.SMTP_SSL("smtp.163.com", 465, timeout=5)
yield connection
print("teardown smtp")
connection.close()
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert 0
这段代码中,smtp_connection被装饰器@pytest.fixture装饰,表明它是一个fixture函数。这个函数的功能是连接163邮箱服务器,返回一个连接对象。当test_ehlo的最后一次测试执行完成后,执行print(“teardown smtp”)和connection.close()断开smtp连接。
fixture函数名可以作为测试方法test_ehlo的参数,在测试方法内部,使用fixture函数名这个变量,就相当于是在使用fixture函数的返回值。
回到我们读取测试配置信息的需求上,在自动化测试项目tests/目录中创建一个文件conftest.py,定义一个fixture函数env:
@pytest.fixture(scope="session")
def env(request):
config_path = os.path.join(request.config.rootdir,
"config",
"test",
"config.yaml")
with open(config_path) as f:
env_config = yaml.load(f.read(), Loader=yaml.SafeLoader)
return env_config
conftest.py文件是一个plugin文件,里面可以实现Pytest提供的Hook函数或者自定义的fixture函数,这些函数只在conftest.py所在目录及其子目录中生效。scope="session"表示这个fixture函数的作用域是session级别的,在整个测试活动中开始前执行,并且只会被执行一次。除了session级别的fixture函数,还有function级别、class级别等。
env函数中有一个参数request,其实request也是一个fixture函数。在这里用到了它的request.config.rootdir属性,这个属性表示的是pytest.ini这个配置文件所在的目录,因为我们的测试项目中pytest.ini处于项目的根目录,所以config_path的完整路径就是:
/Users/chunming.liu/learn/api_pytest/config/test/config.yaml
将env作为参数传入测试方法test_in_theaters,将测试方法内的host改为env[“host”][“douban”]:
class TestInTheaters(object):
@pytest.mark.parametrize("case,http,expected", list(list_params), ids=cases)
def test_in_theaters(self, env, case, http, expected):
r = requests.request(http["method"],
url=env["host"]["douban"] + http["path"],
headers=http["headers"],
params=http["params"])
response = r.json()
这样就达到了测试配置文件与测试脚本相互分离的效果,如果需要修改host,只需要修改配置文件即可,测试脚本文件就不用修改了。修改完成后执行测试的方法不变。
上面的env函数实现中,有点点小缺憾,就是读取的配置文件是固定的,读取的都是test环境的配置信息,我们希望在执行测试时,通过命令行选项,可指定读取哪个环境的配置,以便在不同的测试环境下开展测试。Pytest提供了一个叫作pytest_addoption的Hook函数,可以接受命令行选项的参数,写法如下:
def pytest_addoption(parser):
parser.addoption("--env",
action="store",
dest="environment",
default="test",
help="environment: test or prod")
pytest_addoption的含义是,接收命令行选项–env选项的值,存到environment变量中,如果不指定命令行选项,environment变量默认值是test。将上面代码也放入conftest.py中,并修改env函数,将os.path.join中的"test"替换为request.config.getoption(“environment”),这样就可以通过命令行选项来控制读取的配置文件了。比如执行test环境的测试,可以指定–env test:
$ py.test --env test tests/test_in_theaters.py
如果不想每次都在命令行上指定–env,还可以将其放入pyest.ini中:
[pytest]
addopts = --env prod
命令行上的参数会覆盖pyest.ini里面的参数。
7. 测试的准备与收尾
很多时候,我们需要在测试用例执行前做数据库连接的准备,做测试数据的准备,测试执行后断开数据库连接,清理测试脏数据这些工作。通过06小节大家对于通过env这个fixture函数,如何在测试开始前的开展准备工作有所了解,本小节将介绍更多内容。
@pytest.fixture函数的scope可能的取值有function,class,module,package 或 session。他们的具体含义如下:
- function,表示fixture函数在测试方法执行前和执行后执行一次。
- class,表示fixture函数在测试类执行前和执行后执行一次。
- module,表示fixture函数在测试脚本执行前和执行后执行一次。
- package,表示fixture函数在测试包(文件夹)中第一个测试用例执行前和最后一个测试用例执行后执行一次。
- session,表示所有测试的最开始和测试结束后执行一次。
通常,数据库连接和断开、测试配置文件的读取等工作,是需要放到session级别的fixture函数中,因为这些操作针对整个测试活动只需要做一次。而针对测试数据的准备,通常是function级别或者class级别的,因为测试数据针对不同的测试方法或者测试类往往都不相同。
在TestInTheaters测试类中,模拟一个准备和清理测试数据的fixture函数preparation,scope设置为function:
@pytest.fixture(scope="function")
def preparation(self):
print("在数据库中准备测试数据")
test_data = "在数据库中准备测试数据"
yield test_data
print("清理测试数据")
在测试方法中,将preparation作为参数,通过下面的命令执行测试:
$ pipenv py.test -s -q --tb=no tests/test_in_theaters.py
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/chunming.liu/learn/api_pytest, inifile: pytest.ini
collected 2 items
tests/test_in_theaters.py 在数据库中准备测试数据
F清理测试数据
在数据库中准备测试数据
.清理测试数据
==================================================== short test summary info =====================================================
FAILED tests/test_in_theaters.py::TestInTheaters::test_in_theaters[验证响应中start和count与请求中的参数一致] - AssertionError: ...
通过输出可以看到在每一条测试用例执行前后,各执行了一次“在数据库中准备测试数据”和“清理测试数据”。如果scope的值改为class,执行测试用例的输出信息将是下面这样:
tests/test_in_theaters.py 在数据库中准备测试数据
F.清理测试数据
在测试类执行前后各执行一次“在数据库中准备测试数据”和“清理测试数据”。
8. 标记与分组
通过pytest.mark可以给测试用例打上标记,常见的应用场景是:针对某些还未实现的功能,将测试用例主动跳过不执行。或者在某些条件下,测试用例跳过不执行。还有可以主动将测试用例标记为失败等等。针对三个场景,pytest提供了内置的标签,我们通过具体代码来看一下:
import sys
import pytest
class TestMarks(object):
@pytest.mark.skip(reason="not implementation")
def test_the_unknown(self):
"""
跳过不执行,因为被测逻辑还没有被实现
"""
assert 0
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
def test_skipif(self):
"""
低于python3.7版本不执行这条测试用例
:return:
"""
assert 1
@pytest.mark.xfail
def test_xfail(self):
"""
Indicate that you expect it to fail
这条用例失败时,测试结果被标记为xfail(expected to fail),并且不打印错误信息。
这条用例执行成功时,测试结果被标记为xpassed(unexpectedly passing)
"""
assert 0
@pytest.mark.xfail(run=False)
def test_xfail_not_run(self):
"""
run=False表示这条用例不用执行
"""
assert 0
下面来运行这个测试:
$ py.test -s -q --tb=no tests/test_marks.py
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/chunming.liu/learn/api_pytest, inifile: pytest.ini
collected 4 items
tests/test_marks.py s.xx
============================================ 1 passed, 1 skipped, 2 xfailed in 0.06s ============================================
从结果中可以看到,第一条测试用例skipped了,第二条测试用例passed了,第三条和第四条测试用例xfailed了。
除了内置的标签,还可以自定义标签并加到测试方法上:
@pytest.mark.slow
def test_slow(self):
"""
自定义标签
"""
assert 0
这样就可以通过-m过滤或者反过滤,比如只执行被标记为slow的测试用例:
$ py.test -s -q --tb=no -m "slow" tests/test_marks.py
$ py.test -s -q --tb=no -m "not slow" tests/test_marks.py
对于自定义标签,为了避免出现PytestUnknownMarkWarning,最好在pytest.ini中注册一下:
[pytest]
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
9. 并发执行测试
如果自动化测试用例数量成千上万,那么并发执行它们是个很好的主意,可以加快整体测试用例的执行时间。
pyest有一个插件pytest-xdist可以做到并发执行,安装之后,执行测试用例通过执行-n参数可以指定并发度,通过auto参数自动匹配CPU数量作为并发度。并发执行本文的所有测试用例:
$ py.test -s -q --tb=no -n auto tests/
====================================================== test session starts =======================================================
platform darwin -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /Users/chunming.liu/learn/api_pytest, inifile: pytest.ini
plugins: xdist-1.31.0, forked-1.1.3
gw0 [10] / gw1 [10] / gw2 [10] / gw3 [10] / gw4 [10] / gw5 [10] / gw6 [10] / gw7 [10]
s.FxxF..F.
==================================================== short test summary info =====================================================
FAILED tests/test_marks.py::TestMarks::test_slow - assert 0
FAILED tests/test_smtpsimple.py::test_ehlo - assert 0
FAILED tests/test_in_theaters.py::TestInTheaters::test_in_theaters[验证响应中start和count与请求中的参数一致] - AssertionError: ...
======================================= 3 failed, 4 passed, 1 skipped, 2 xfailed in 1.91s ========================================
可以非常直观的感受到,并发执行比顺序执行快得多。但是并发执行需要注意的是,不同的测试用例之间不要有测试数据的相互干扰,最好不同的测试用例使用不同的测试数据。
这里提一下,pytest生态中,有很多第三方插件很好用,更多的插件可以在这里https://pypi.org/search/?q=pytest-查看和搜索,当然我们也可以开发自己的插件。
10. 测试报告
Pytest可以方便的生成测试报告,通过指定–junitxml参数可以生成XML格式的测试报告,junitxml是一种非常通用的标准的测试报告格式,可以用来与持续集成工具等很多工具集成:
$ py.test -s -q --junitxml=./report.xml tests/
现在应用更加广泛的测试报告是Allure,可以方便的与Pytest集成,大家可以参考后续文章。