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,搜索"测试框架"查看。