Skip to content

10你的第一个API测试框架(二)

欢迎来到第 10 课时,进入"API 测试框架"的第二个部分。上一课时我们介绍了 Requests,以及测试框架核心模块 pytest,这一课时我将深入讲解 pytest 的使用,这节课的知识内容如下。

融会贯通 pytest 使用

1. pytest 运行方式详解及其参数

pytest 有两个测试运行方式,命令行运行和 pytest.main() 运行。

  • 命令行运行

pytest 支持在命令行中以如下方式运行:

python
 python -m pytest [...]
  • pytest.main() 运行

除了命令行运行方式外,pytest 还支持在程序中运行,在程序中运行的命令如下:

python
pytest.main([...])

不管是使用命令行运行或者使用 pytest.main() 的方式运行,它们支持的参数都是一样的。需要注意的是:pytest 的参数必须放在一个 list 或者 tuple 里。

pytest 参数

pytest 支持特别多的参数,具体有哪些参数可以通过如下命令查看:

python
pytest --help

在这里,我列出我们在工作中常用的几个。

-m: 用表达式指定多个标记名。

pytest 提供了一个装饰器 @pytest.mark.xxx,用于标记测试并分组(xxx是你定义的分组名),以便你快速选中并运行,各个分组直接用 and、or 来分割。

-v: 运行时输出更详细的用例执行信息

不使用 -v 参数,运行时不会显示运行的具体测试用例名称;使用 -v 参数,会在 console 里打印出具体哪条测试用例被运行。

-q: 类似 unittest 里的 verbosity,用来简化运行输出信息。

使用 -q 运行测试用例,仅仅显示很简单的运行信息, 例如:

java
.s..   [100%]
3 passed, 1 skipped in 9.60s

-k: 可以通过表达式运行指定的测试用例。

它是一种模糊匹配,用 and 或 or 区分各个关键字,匹配范围有文件名、类名、函数名。

-x: 出现一条测试用例失败就退出测试。

在调试时,这个功能非常有用。当出现测试失败时,停止运行后续的测试。

以上这些命令,我将把它们融合进日常工作场景,逐个向大家介绍。

2.运行指定文件夹下的测试用例

pytest 支持更简单的方法实现指定文件夹运行:

python
# 执行所有当前文件夹及子文件夹下的所有测试用例
pytest .
# 执行跟当前文件夹同级的tests文件夹及子文件夹下的所有测试用例
pytest ../tests

除此之外,pytest 还允许你通过更复杂的方式来挑选测试用例执行。例如,我们可以通过 -m 或者 -k 的参数,把我们的测试用例限制在某一个文件夹下,这样就实现了仅允许指定文件夹下的测试用例。

关于 -m 和 -k 的使用,请看接下来我要讲的 "动态挑选测试用例运行 --- 按 Tag""动态挑选测试用例运行 --- 按名称" 这两个部分。

3.选择测试用例执行

pytest 里选择测试用例执行有很多方法,可以按照测试文件夹、测试文件、测试类和测试方法四种。

  • 按照测试文件夹执行

在刚刚**"运行指定文件夹下的测试用例"**中已经详细说明,我不再赘述。

  • 按照测试文件执行
python
# 运行test_lagou.py下的所有的测试用例
pytest test_lagou.py
  • 按照测试类执行

按照测试类执行,必须以如下格式:

pytest 文件名 .py:: 测试类,其中"::"是分隔符,用于分割测试 module 和测试类。

python
# 运行test_lagou.py文件下的,类名是TestLaGou下的所有测试用例
pytest test_lagou.py::TestLaGou
  • 按照测试方法执行

同样的测试方法执行,必须以如下格式:

pytest 文件名 .py:: 测试类 :: 测试方法,其中 "::" 是分隔符,用于分割测试 module、测试类,以及测试方法。

python
# 运行test_lagou.py文件下的,类名是TestLaGou下的,名字为test_get_new_message的测试用例 
pytest test_lagou.py::TestLaGou::test_get_new_message
  • 不在命令行执行,在程序中执行

以上选择测试用例执行的方法,可以不在命令行,而直接在测试程序里执行,其语法为

pytest.main([模块.py::类::方法])

4.动态挑选测试用例运行 --- 按 Tag

动态挑选测试用例一直是测试框架的刚需,在 pytest 里动态挑选测试用例需要借助两个步骤。

  • 首先给测试用例打标签(mark),在 Class、method 上加上如下装饰器:
python
@pytest.mark.xxx
  • 在运行时,命令行动态指定标签运行:
python
# 同时选中带有这两个标签的所有测试用例运行
pytest -m "mark1 and mark2"
# 选中带有mark1的测试用例,不运行mark2的测试用例
pytest -m "mark1 and not mark2" 
# 选中带有mark1或 mark2标签的所有测试用例
pytest -m "mark1 or mark2"

下面来实际演示下,仍然以我们的项目 lagouTest 为例,我们更改 tests 文件夹下的两个文件,

其中对 test_baidu.py 文件的修改如下:

python
# test_baidu.py下更改
import pytest
# 在Baidu这个类上加标签baidu
@pytest.mark.baidu
class Baidu(unittest.TestCase):
# 以下代码皆不变

对 test_lagou.py 文件的修改如下:

python
# test_lagou.py下更改
import pytest
# 在TestLaGou这个类上加标签lagou
@pytest.mark.lagou
class TestLaGou:
# 以下代码皆不变

我给这两个测试类分别加上了标签 baidu 和标签 lagou,现在我们按需运行下:

java
pytest -v -m "lagou and not baidu"

可以看到标记为 lagou 下面的 2 条测试用例都被执行了,但是 baidu 标签下的 2 条都没有被执行,在 Console 中显示 "2 deselected"。

同样地,我们还可以用以下命令,运行所有包含 lagou 和 baidu 标签的测试用例。

java
pytest -v -m "lagou and baidu"

请注意,不同标签之间的分割要使用and 或者or关键字。

在这里给你布置一个课外作业:

如果给一个类打了一个标签叫作 toRun,然后给这个类下的某一个方法也打了一个标签叫作 notToRun, 如果我在命令行中输入以下命令,运行状况是什么呢?

python
pytest -v -m "toRun and not notToRun"

如果我输入以下的命令,运行情况又是怎么样呢?

python
pytest -m "notToRun and not toRun"

5.动态挑选测试用例运行 --- 按名称

pytest 中,动态挑选测试用例,除了打标签(mark)外,还有另外一种方式:

java
# -k 参数是按照文件名、类名、方法名来模糊匹配的
pytest -k xxxPattern

下面来详细演示下,我们的项目文件结构如下:

java
|--lagouAPITest
    |--tests
        |--test_baidu.py
        |--test_lagou.py
        |--__init__.py
    |--common
        |--__init__.py
    |--__init__.py

其中:

  • test_baidu.py 里定义了一个测试类 Baidu, 这个测试类下有两个测试方法 test_baidu_search 和 test_baidu_set;

  • test_lagou.py 中定义了一个测试类 TestLaGou,这个测试类下面有两个测试方法 test_visit_lagou 和 test_get_new_message。

在命令行中以如下方式运行。

  • 按照文件名称全匹配:
python
# 运行test_lagou.py下的所有的测试
pytest -k "test_lagou.py"
  • 按照文件名字部分匹配:
python
# 因为lagou能匹配上test_lagou.py,故运行test_lagou.py下所有的测试
pytest -k "lagou"
  • 按照类名匹配:
python
# 因为Baidu能匹配上test_baidu.py里定义的测试类Baidu,故运行Baidu测试类下所有的测试。 你也可以写成Bai
pytest -k "Baidu"
  • 按照方法名匹配:
python
# message只能匹配test_lagou.py中定义的测试类TestLaGou下的测试方法test_get_new_message, 故仅有test_get_new_message这个方法会执行
pytest -k "message"

6.忽略测试用例执行

有挑选测试用例执行,那么就一定会有忽略测试用例执行,忽略测试用例执行有如下 3 种方式:

  • 直接忽略测试执行

直接忽略可以使用 @pytest.mark.skip 装饰器来实现。

python
# test_lagou.py
@pytest.mark.skip(reason='skip此测试用例')
def test_get_new_message:
  # 实现方法

比如,在 test_lagou.py 里,我定义了一个测试方法 test_get_new_message,然后我给他加上装饰器 pytest.mark.skip,那么当我在命令行中执行如下语句时,test_get_new_message 将会被忽略执行:

python
pytest test_lagou.py
  • 按条件忽略测试执行 --- 使用 skipif 忽略

按 skipif 条件,当条件符合时便会忽略某条测试用例执行。

python
# test_lagou.py 
# 定义一个flag,用来指示是否要skip一个测试用例
flag = 1
 
# 此处判断flag的值,为1则忽略,0则不忽略 
@pytest.mark.skipif(flag == 1, reason='by condition')
def test_get_new_message:
  # 实现方法

当我在命令行中执行如下语句时,test_get_new_message 将会被忽略执行:

python
pytest test_lagou.py

更改 flag 的值为 0 ,再次运行这条语句,你将发现此方法将会被执行。

  • 按条件忽略测试执行 --- 使用 -m 或者 -k 忽略

除了 skip 和 skipif 外,我们也可以通过 -m 或者 -k 的方式,把我们不需要运行的测试用例给过滤掉,从而实现测试用例的忽略执行。在刚刚的内容中便详细讲解了 -m 和 -k 的具体用法,你可详细回顾一下。

7.setUp 和 tearDown 详解

我们在"07 | 你的第一个 Web 测试框架(一)"("Test Fixture 的使用"部分)中便讲解了如何在 unittest 中使用 setUp 和 tearDown 方法实现测试执行前的准备工作和测试执行后的清理工作。

其实在 pytest 里也有类似的方法,而且在粒度上更加精细。

(1)按 module 进行 setup 和 tear down

按 module 进行 setup 和 tear down,即在某一个 module 内 setup 或者 tear down 的方法只会执行一次,pytest 里用于 module 的 set up 和 tear down 方法为:

python
import pytest
def setup_module(module):
    """
    module级别的setup,直接定义为一个module里的函数
    在本module里所有test执行之前,被调用一次
    """
    ## 你的set up代码,例如:
    print("------ set up for module ------")

def teardown_module(module):
  """
    module级别的setup,直接定义为一个module里的函数
    在本module里所有test执行之后,被调用一次
    """
    ## 你的tear down代码,例如:
    print("------ tead down for module ------")

需注意以下几个事情:

  • setup_module(module) 和 teardown_module(module) 的写法最好不要改动;

  • 当 setup_module 出错,teardown_module 不会被执行;

  • 一个 module(.py 文件)可以包括多个 Class,多个classs 下可能有多个 case,但是 setup_module 和 teardown_module 只会执行一次。

(2)按 class 进行 setup 和 tear down

在某一个测试类内,同样可以进行 set up 和 tear down。

python
class Baidu(object):
    @classmethod
    def setup_class(cls):
        """ 
        仅在当前测试类下的所有test执行之前,被调用一次
        注意它必须以@classmethod装饰
        """
        ## 你的set up代码,例如:
        print("------ set up for class------")
    @classmethod
    def teardown_class(cls):
        """ 
        仅在当前测试类下的所有test执行之后,被调用一次
        注意它必须以@classmethod装饰
        """
        ## 你的tear down代码,例如:
        print("------tear down for class------")

需注意以下几个事情:

  • setup_class(cls) 和 teardown_class(cls) 的写法最好不要改动。

  • setup_class(cls) 和 teardown_class(cls) 必须以 @classmethod 装饰。

  • 当 setup_class(cls) 出错,teardown_class(cls) 不会被执行。

(3)按 method 进行 setup 和 tear down

针对每一个测试用例,同样可以进行 set up 和 tear down。

python
def setup_method(self, method):
        """ 
        在当前测试类里,每一个test执行之前,被调用一次
        """
        ## 你的set up代码,例如:
        print("------set up for method------)

    def teardown_method(self, method):
        """ 
        在当前测试类里,每一个test执行之前,被调用一次
        """
        ## 你的tear down代码,例如:
        print("------tear down for method------)

需注意以下两个事情:

  • setup_method(self, method) 和 teardown_method(self, method) 的写法最好不要改动。

  • 当 setup_method(self, method)用例执行失败时,teardown_method(self, method) 不会被执行。

setup 和 teardown 在我们测试开始和结束后准备/清理测试数据,系统状态时非常有用。

8.使用 pytest.ini 文件破除默认 pattern,灵活命名测试文件

我在上一课时的"pytest 查找测试用例的原则"部分说过,pytest 查找测试用例,会根据测试用例名,仅默认查找前缀以 _tes开头或者后缀以 _test 结尾的测试文件;而查找测试方法,仅查找测试类以 Test 开头,测试方法以 test 开头。

那有没有办法破除这一依赖呢?当然有,那就是使用 pytest.ini 文件。

pytest.ini 是 pytest 的主配置文件,可以改变 pytest 的默认行为。在项目根目录 lagoutAPITest 下新创建一个文件 pytest.ini,我们的项目结构就变成这样:

dart
|--lagouAPITest
    |--tests
        |--test_baidu.py
        |--test_lagou.py
        |--__init__.py
    |--common
        |--__init__.py
    |--__init__.py
    |--__pytest.ini

pytest.ini 的内容如下:

java
[pytest]
python_classes = *
python_files= *
python_functions = test*

其中:

  • python_classes,表示要匹配的测试类的 pattern,*匹配所有。

  • python_files,表示要匹配的测试文件。

  • python_functions,表示要匹配的测试方法。

在这里我把 python_files 和 python_classes 均设置为*,表示任何 *.py 都将被认为是测试文件。任何名字的测试类都将被认为是测试类。

下面我再次在根目录 lagouAPITest 下建立一个测试文件 sample.py,里面的内容如下:

python
class Sample(object):
    def test_equal(self):
        assert 1 == 1
    def not_equal(self):
        assert 1 != 0

我定义了一个 Sample 类,其中包括两个测试方法 test_equal 和 not_equal。然后我切换到项目根目录下,在命令行运行如下命令:

java
D:\_Automation\lagouAPITest>pytest

结果如下:

java
tests/sample.py::Sample::test_equal PASSED

可以看到,sample.py 这个文件被当作了测试文件,并且 Sample 这个类也被 pytest 视为测试类,因为他们符合 pytest.ini 里的配置。

而 Sample 类的两个测试方法中,test_equal 被执行,而 not_equal 没有被执行,因为我在 pytest.ini 里关于 python_functions 的配置是必须以 test 开头。

使用 pytest.ini 可以针对 pytest 做更多配置,做法如下所示。

  • 注册标签:
html
[pytest]
markers = 
    smoke: run smoke test
    sanity: run sanity test

你可以在 pytest.ini 里注册标签,并统一管理。

在 pytest.ini 文件里注册标签并不意味着你不需要在测试类/测试方法里忽略 @pytest.mark.xxx 装饰器,你仍需要在你的测试用例上加上标签。

python
# 例如在sample.py文件里的Sample类上加标签
@pytest.mark.smoke
Class Sample:
  # 测试方法
  • 指定测试目录
html
[pytest]
testpaths=tests

此方法把 testpaths 指定为 tests,当你在命令行中直接执行 pytest 时,会自动查询 testpaths 文件夹,如果没有设置 testpaths,则会自动从当前文件夹找起。

除了以上这些,pytest.ini 还有其他很多用处,例如更改默认命令行参数、指定 pytest 最低版本号、指定忽略某些目录禁用 XPASS 等功能,大家有兴趣可以自行查询。

另外,pytest 里查找测试用例的 pattern 是采用的 Glob 这个库,你可以进入 Python 官网查看更多关于如何匹配 pattern 的内容。

9.数据驱动

pytest 支持数据驱动,关于数据驱动,我将在后续的章节第 13、14 讲"DDT:博采众长,数据驱动的秘诀"中为你详细讲解。

10.失败重跑测试用例

在测试执行中,偶尔会出现由于环境不稳定,或者网络不稳定造成的测试运行失败的情况,如果第一次运行就报错,那么势必会增加我们排查的工作量。

所以 pytest 支持错误失败重跑,使用失败重跑机制的步骤如下:

  • 安装
python
pip install -U pytest-rerunfailures
  • 命令行执行失败重跑次数
python
# 语法:
--reruns Num。 其中Num是重跑的次数

下面我们来实际看一个 re-run 的例子,我更改 sample.py 文件如下:

python
import pytest

@pytest.mark.smoke
class Sample(object):
    def test_equal(self):
        # 在这里,我让这个case失败,来演示re-run
        assert 1 == 0
    def not_equal(self):
        assert 1 != 0

然后我们在命令行执行如下命令:

python
# sample.py目录为 D:\_Automation\lagouAPITest\tests
# 需切换至此目录执行
pytest sample.py --reruns 2

执行后观察测试输出,会发现如下信息"1 failed,2 rerun":

11.并发运行测试用例集

当你的测试用例比较多时,最好可以通过并发测试来减少测试整体的运行时间。pytest 支持并发测试,并且有不同的并发测试库,其中如下两个比较著名:

  • pytest-parallel

安装

python
pip install pytest-parallel

运行:使用 pytest-parallel 运行,需要指定参数。

--workers (optional) X

多进程运行, X 是进程数,默认值 1。

--tests-per-worker (optional) X

多线程运行, X 是每个 worker 运行的最大并发线程数, 默认值1。

注意:这个插件仅仅支持 python 3.6 版本及以上,而且如果你想多进程并发,必须跑在 Unix 或者 Mac 机器上,windows 环境仅仅支持多线程运行。

运行命令如下:

python
pytest --workers 2 #指定2个进程并发
#指定2个进程并发,每个进程最多运行3个线程
pytest --workers 2 --test-per-worker 3
  • pytest-xdist

安装

python
pip install pytest-xdist

运行

python
# 语法:
pytest -n NUMCPUS
# 以下为2个进程并行运行
pytest -n 2
#使用与CPU内核一样多的进程来并发
pytest -n auto

从理论上来说,pytest-parallel 要更好一些,因为 pytest-xdist 有以下缺点:

  • 非线程安全

  • 多线程时性能不佳

  • 需要状态隔离

但是实际应用中,pytest-parallel 有时会出现如下运行错误:

java
BrokenPipeError: [WinError 109] 管道已结束

而且这个错误发生的原因不确定,官方暂时没有修复, 如果你在测试中发现这个错误,那么可以使用 pytest-xdist 来进行并发测试。

pytest 集成测试报告

在 pytest 中集成测试报告也有两个方式:

1.pytest-html

安装:

java
pip install pytest-html

使用:

python
pytest --html=report.html

以我们的项目 lagouAPITest 为例,在项目根目录下执行

python
pytest --html=report.html

然后在项目根目录下会生成一个 report.html 文件。

pytest-html 也可以支持 pytest-xdist,也就是说,如果你使用了pytest-xdist来进行并发测试, pytest-html也可以收集并发测试结果了。两者结合使用的命令如下:

python
pytest -n 3 --html=report.html --self-contained-html

运行结束后查看结果如下:

pytest-html 还支持错误重试,使用如下命令运行

python
pytest -n 3 --html=report.html --self-contained-html --reruns 2

运行结束你会发现,错误的测试用例被运行了 2 次。

2.allure

使用 allure 生成测试报告的步骤如下。

  • 安装 allure

在不同操作系统上安装 allure 的步骤是不同的。

MacOS:

java
brew install allure

Linux:

java
sudo apt-add-repository ppa:qameta/allure
sudo apt-get update 
sudo apt-get install allure

Windows:

在 Windows 上安装 allure,首先要安装 Scoop,Scoop 的安装步骤如下:

java
# 以Win10为例:
1. 使用快捷键 Win + R 调起运行提示框
2. 输入"cmd"进入到命令行
3. 输入"powershell"进入到powershell模式(此时你的命令提示应该以PS开始)
4. 确保你的PowerShell版本大于5.0,命令如下:
$psversiontable.psversion.major # 这个运行后出现的值应该>=5.0
5. 允许PowerShell 执行本地脚本:
set-executionpolicy remotesigned -scope currentuser
6. 安装Scoop
Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')

使用上述方式安装好后,你应该可以看到如下界面:

安装好 Scoop 后,不要关闭 powershell,直接输入如下命令安装 allure:

java
PS C:\Users\Admin>scoop install allure

安装好后,你将看到如下界面:

如果你之前安装过 allure,也可以通过如下方式更新:

java
PS C:\Users\Admin>scoop update allure

你还可以查看当前使用的 allure 版本:

java
PS C:\Users\Admin>allure --version
  • 执行 pytest 命令,并指定 allure 报告目录

可以直接在命令行里执行:

python
# 直接执行lagouAPITest项目下的所有测试用例,并将测试报告文件夹allure_reports放在项目根目录下
D:\_Automation\lagouAPITest>pytest --alluredir=./allure_reports

还可以在程序里执行:

python
# 执行所有的标记为smoke的测试用例,并且报告文件夹设置为allure_reports
pytest.main(["-m", "smoke", 
             "--alluredir=./allure_reports"])
  • 打开 allure 报告

测试执行完成后,通过以下方式打开生成的 allure 报告:

html
# 以Win10为例:
1. 使用快捷键 Win + R 调起运行提示框
2. 输入"cmd"进入到命令行
3. 切换目录到项目根目录,本例中在D:\_Automation\lagouAPITest>:
4. 输入命令生成allure报告:
allure serve allure_reports

需要注意的是,allure_reports 文件夹就是我们在第 2 步中定义的 allure 报告文件夹所在的位置,命令执行后,会自动打开 allure 测试报告:

更多关于测试报告的技巧,我将在 "23 | 对外输出,让测试报告自己说话" 一节中向你详细介绍。

至此,我们的第一个 API 接口自动化测试框架已经全部完成。

总结

"模块二:项目实战,搭建自动化测试框架"这一部分便结束了,完成学习的你已经可以胜任 Web 自动化测试框架,API 自动化测试框架的搭建工作了。

本章节实践较多,希望大家能够把所讲的内容亲自实践一下,尽可能多的掌握测试框架搭建基本功,从下一章开始,我将带领大家进入能力修炼,全面掌握多项技能。


更多关于测试框架的知识,请关注测试公众号 iTesting,搜索"测试框架"查看。