接口测试|从0不到1的心路历程

文摘   2024-10-11 12:02   重庆  


来源:TesterHome社区

作者:耿晓


以下为作者观点:


前段时间我发布了一篇有关自己做接口测试的实践经验《接口测试 - 从0不到1的心路历程(一)》,发出后受到了很多小伙伴的关注,也收到了很多佬儿哥的指点,很是开心,TesterHome真是一个温暖的地方。在众多建议中,频率最高的就是"pytest+requests+allure"的组合了,随后我就学习了pytest和allure的相关知识,并且在项目中进行了实践。本次我就总结一下基于这个框架的阶段性成果,希望伙伴们继续纠偏指点。


书接上文 - 第三代测试框架


第三代测试框架是基于unittest+requests实现的,使用ddt库实现了数据驱动测试,使用excel管理测试数据,以及使用excel持久化接口响应的相关信息,具体细节感兴趣的小伙伴可以点击查看上篇文章:接口测试 - 从0不到1的心路历程。依据这个模式写了30个接口的case,发现如下问题:


1.编写测试数据生成函数时,不光要写数据生成逻辑的代码,还要写将测试数据写入excel的代码,导致整个函数冗长,函数职能不单一。


2.使用excel保存接口响应的相关信息,每个接口单独占据一个sheet页,这种方式一旦接口数量过多,最终查阅起来并不方便 (首先打开 excel,然后查找目标sheet,然后再查找目标case)。


学习allure之后,发现allure可以记录接口的入参及接口响应等信息,基于此就可以完美解决上述问题,测试数据生成函数仅需要编写数据生成逻辑,无需再写处理excel相关代码。接口响应等信息直接使用 allure.attach() 方法就可以记录在报告中,所以也就不用再像版本三一样编写收集接口响应信息并写入到 excel 的函数了。先上两张excel报告和allure报告的效果对比图,然后再叙述第四版框架的堆砌过程。




初识pytest


我主要是通过《pytest 测试实战》这本书学习pytest的,这是一本工具书,整书不厚,看完前四章就可以动手实战了,有兴趣的伙伴也可以看一下。pytest的优势在此就不过多赘述了,不过我认为还是很有必要了解一下pytest和unittest 的区别的,我在网上借了一张对比图,刚了解pytest的伙伴可以参考下。



在pytest使用过程中,有三点优势是我切实感受到的:


1.pytest使用assert关键字来进行断言,相比unittest的断言就四个字:简洁明了



2.pytest提供的失败重跑插件"pytest-rerunfailures"。


例举一个使用场景:case1为新建商品,新建成功后保存商品 id,case2为编辑商品,需要使用case1保存的商品id。在运行测试脚本时,case2有一定概率会报错,提示被测系统中不存在此商品id,但事后手动执行case2发现并没有问题。究其原因可能是case1的新建接口新建完商品后需要一定的时间进行数据落库等操作,但运行测试脚本时,case1和case2的衔接时间又很短,导致case2有一定概率会报错。在使用 unittest 框架时,对于这种场景,我一般会在case2发送请求前强行休眠1秒:sleep(1) ,不过像这种蠢呆呆的代码我都会用遮羞布盖住,生怕别人看见。不过接触到"pytest-rerunfailures"插件后,再也不用担心这个问题了,只能用四个字形容:极其好用!


前提条件:pytest (>=5.3) 和 python >=3.6


安装

  • pip install pytest-rerunfailures

  • pip show pytest-rerunfailures


使用

  • 命令行参数:--reruns n(重新运行次数)–reruns-delay m(等待运行秒数)

  • 使用装饰器:/@pytest.mark.flaky(reruns=5, reruns_delay=2)


注意兼容性:

  • 这个插件不可以和 class, module, package 级别的 fixture 装饰器一起使用;

  • 这个插件与 pytest-xdist 的 --looponfail 标志不兼容;

  • 这个插件与核心 --pdb 标志不兼容。



3.pytest 的参数化-@pytest.mark.parametrize(‘参数名’, list) 。


这个装饰器的使用很简单,大致为:第一个参数是字符串,多个参数中间用逗号隔开;第二个参数是 list。我觉得比较有意思的一点是可以使用多个@pytest.mark.parametrize(‘参数名’, list) ,最终效果就是每一个参数化装饰器中可选值的笛卡尔积,在上篇文章的讨论区中,有小伙伴就问过,参数组合的场景中,想做全面的笛卡尔积应该怎么做,喏,用@pytest.mark.parametrize(‘参数名’, list) 就可以解决你的问题了。让我们演示一下 (方法虽好,可不要过于贪恋,非必要我们还是要慎用笛卡尔积的,毕竟也要考虑用例冗余带来的效能问题):



初识allure


allure是一个灵活的轻量级多语言测试报告工具。在使用 allure之前,需要安装"allure 命令行工具"和"allure-pytest"插件,需要注意的是,allure需要依赖jdk,我想it人电脑上jdk应该是标配吧,所以在此就不过多介绍jdk的安装详情。


allure命令行工具安装步骤:
1.下载安装包,我选择的是"allure-2.20.1.zip",allure 安装包下载地址
2.解压缩
3.配置环境变量 path,我的是:D:\Gxy\allure-2.20.1\allure-2.20.1\bin
4.检查是否配置成功,在 cmd 命令窗口输入: allure --version



"allure-pytest"插件安装:
1.pip install allure-pytest


allure 运行命令:
1.pytest --alluredir ./report ./test_dir # 生成测试报告所需要的数据,数据在 report 目录里。
2.allure serve report # 会自动使用默认浏览器打开 allure 报告,默认是英文的,我们可以在页面左下角切换成中文显示。
3.如果我们想每次的报告不掺杂历史数据,那我们可以在运行测试命令后面加"--clean-alluredir"。这样最终的命令就是:
"pytest --alluredir ./report ./test_dir --clean-alluredir"


项目实践


首先介绍一下我的项目目录,相比之前版本的测试框架,去除了TestRunner 文件夹 (用于生成测试报告),项目的根目录下多了"conftest.py"、"requirements.txt"和"result"文件,具体如下:



在实践过程中,我有思考以下问题:


问题1:pytest怎么控制用例的执行顺序?

问题1的解答:
在unittest 框架中,默认按照ACSII码的顺序加载测试用例并执行,顺序为:09、AZ、a~z,测试目录、测试模块、测试类、测试方法/测试函数都按照这个规则来加载测试用例。


pytest 默认执行顺序:测试目录、测试模块,按照排序顺序执行。
pytest 也自定义执行顺序:需要安装 pytest-ordering 插件。

# 需要使用 @pytest.mark.run()@pytest.mark.run(order=2)def test_a():    print("test_a")@pytest.mark.run(order=1)def test_2():    print("test_2")


因为我这套测试脚本,用例间还是很讲究运行顺序的,所以我就遵循pytest按照排序顺序执行的特点顺序编写测试脚本。起初我并没有给case进行编号,只是单单的按照排列顺序写,可在查看allure测试报告时,发现报告中case显示的顺序并不是按照脚本中case排列顺序展示的,并且我手动点击allure报告中排序规则后,还是没能按照脚本中case的顺序进行展示,这就令我很恼火,于是我还是按照unittest中根据名称排序的习惯,将所有case的标题都改了一遍,果然,allure报告中case的顺序被我成功拿捏住了。效果如下:



问题2:有接口数据依赖时,怎么判断依赖数据被成功赋值

场景描述:还是以新增和编辑接口举例,首先我会在 config.py 文件中设置一个商品id,初始值为-1,然后新增接口的 case 断言通过后会重新赋值这个商品 id,最后编辑接口case运行前,需要先判断config.py 文件中的商品 id 是否被成功赋值,成功赋值则继续运行case后续代码,如果没有成功赋值则不运行 case2后续代码。


我在第三版测试框架中是这样做的:



如果我在第四版中依然沿用这个思路,那一旦依赖参数没有被成功赋值,则在 allure 报告中会显示本条 case 失败,可其实本条 case 并没有被运行,所以感觉被置为失败不确切。


def test_getGuideItem(self, case, expect, desc):    if config.id_subjectId[0] != -1:        assert 1 == 2, '未获取到科室id,所以无法获取手术项目'


那我将 “assert 1 == 2” 改成"raise KeyError()"效果怎么样呢?

def test_getGuideItem(self, case, expect, desc):    if config.id_subjectId[0] != -1:        raise KeyError('未获取到科室id,所以无法获取手术项目')


在allure报告中显示故障好像也不太恰当,分析后其实这种情况应该属于case 入参没拿到,所以跳过本条用例,那自然而然的就想到了pytest跳过测试用例相关知识。pytest跳过测试用例大致分为方法外跳过 (@pytest.mark.skip()) 和方法内跳过 (pytest.skip()),分别演示:


# 使用方法外跳过@allure.story('获取手术项目列表')@pytest.mark.skipif(config.id_subjectId == -1, reason='未获取到科室id,所以无法获取手术项目')def test_getGuideItem(self, case, expect, desc):    ....

经实践后发现,方法外跳过行不通,因为测试脚本运行时会先加载所有用例的装饰器,然后才会执行用例本身,而我们的依赖参数 (config.id_subjectId) 是在运行用例过程中进行赋值的,所以方法外跳过的装饰器读取的是原值,读不到变更后的值。


# 使用方法内跳过@allure.story('A1a3_获取手术项目列表')def test_A1a3_getGuideItem(self, keys, token):    if config.id_subjectId == -1:        pytest.skip(msg='未获取到科室id,所以无法获取手术项目')    url = config.HOST_LAPI + 'getGuideItem'    ......


经实践后发现,使用方法内调过可以达到期望效果。如果依赖参数没有被赋值,那么在 allure 报告中会显示此用例被跳过。这个结果就很符合实际情况了。



项目总结


在第四版测试框架中,我一共写了101个接口,共计342条 case,这些是我从公司业务中梳理出来的重要流程中涉及到的所有接口,因为团队中的接口文档并不是很完善,所以我主要通过fiddler抓包工具收集涉及到的接口及其入参出参,工作量真心不小,但好在第四版的测试框架编写起case来相对简洁,让我很快就完成了所有工作。为了更直观的体现第四版的简洁,让我们来对比一下第四版和第三版编写相同 case 时需要的代码量 (一条case由case本身和造数函数两部分组成):





从代码量来看,实现相同的效果,不论代码逻辑,就单单代码数量就大约减少了三分之一,确实很不错!


从运行时间来看,整个脚本运行大概需要110秒左右的时间,如果这些核心流程人工点点点的话,业务熟练如我的人也需要至少半个小时,从运行时间上也能很好的体现出接口测试确实是一把利器,值得测试人员为之投入。


第四版框架的实践总结就写到这里吧,虽然磕磕绊绊的实现了,但自知其中绝对还有很大的优化空间,刚接触 pytest+allure不久,理论和实操经验都很是欠缺,就好比在刚接触pytest的时候,很多资料显示pytest的核心之一是 fixture、conftest等,而我却压根没用到这些,对其也是一知半解,显然我这pytest用的就很皮毛了 ,所以期望社区中的佬儿哥、佬儿姐们能够对发现的问题纠偏赐教,也希望和我一样刚接触pytest的伙伴能够相互讨论一下自己的踩坑史。


一切并非顺风顺水


其实在第四版框架堆砌的过程中,踩坑无数,我将一些最令人头大的坑罗列一下,希望能够帮助一样在摸索的伙伴们。


踩坑 1.切换办公电脑后,项目的Python运行虚拟环境无法使用。


场景描述:由于疫情原因,一直居家办公,项目是在家里电脑 (电脑A) 创建的,创建时是用pycharm创建的虚拟环境。之后疫情稍微稳定,返回公司办公。我就用电脑A将项目上传到了git上,然后用公司的电脑 (电脑B) 克隆项目。最后使用电脑B运行项目发现Python运行环境无法使用。(现在知道问题是由于用电脑A上传git时,将项目根目录下的venv文件夹 (虚拟环境及项目依赖文件) 一同上传到了git 上,然后用电脑B克隆项目,由于项目在不同电脑上的路径不一样,导致电脑A的venv文件夹在电脑B无法正常使用)(但是当时并不知道),然后当时就只能在电脑B上重新创建虚拟环境,然后再重新安装项目依赖,下班时再用电脑B上传项目到git上,回家后用电脑A拉取项目,同样在重新创建虚拟环境,然后再重新安装项目依赖。反复几次后,实在受不了自己这种 stupid donkey的行为。决定先解决这个问题。


解决方案1:不使用虚拟环境,改用全局环境;但可能出现不同项目间依赖包版本冲突的问题,所以弃用方案1。


解决方案2:创建.gitignore文件,忽略venv文件;
经过一系列实践,发现方案2可行。最终项目中的.gitignore 文件内容如下:


但为了快捷的安装项目依赖,又学到了 requirements.txt 相关知识。具体如下:
在项目终端运行:pip freeze -> requirements.txt
这条语句可以将项目依赖信息保存为 txt 文档。


然后再在新的运行环境中运行命令:pip install -r requirements.txt
这样就可以快速的安装项目依赖了。
过程中又联想到另一个分支问题,为什么使用 pip freeze 而不是常见的 pip list?
随后我又使用 pip list -> list.txt 命令导出了 list 文件,两个文件对比如下:


发现pip list比pipfreeze多了"pip"和"setuptools"两个文件。


度娘之后得到如下结论:
"pip , wheel , setuptools等包,是自带的而无法 (un) install的。考虑到pip freeze的用途,所以这些包并没有显示。如果一定要用pip freeze来显示所有包,可以加上参数-all,即pip freeze -all"


踩坑2:项目路径中含有中文,导致在terminal运行语句:pytest --alluredir report test_dir 时报错 "pytest: error: unrecognized arguments: --alluredir"。


这个问题只有在我家里电脑上才会出现,在公司电脑上不会出现。


起初我以为是家里电脑Python的运行环境有问题,结果重新创建运行环境后,仍然存在此问题。


然后我又怀疑是家里pycharm有问题,然后我就将家里电脑的pycharm卸载,重新安装成和公司电脑一样版本的pycharm,运行后仍然存在问题。(现在想想有点搞笑,terminal报错,跟pycharm有什么关系呢
 )


最后百度寻找答案,其中有一条结论说"虚拟环境地址不能有中文",然后我看一下自己项目的路径 (G:\耿晓野的口袋\PycharmProjects\pytest_requests_allure_v1),确实有中文,然后我就把项目路径更改成 (G:\gxyPocket\PycharmProjects\pytest_requests_allure_v1),由于项目路径变更,我不得不重新创建项目虚拟环境并且重新安装项目依赖,随后再次在 terminal 运行语句:pytest --alluredir report test_dir,成功了!

由此也解答了为什么办公室电脑运行就没问题,因为项目直接放在了办公室电脑上的 D 盘下。突然想起上学时 Java 老师说路径中尽量不要有中文,不然会引发很奇怪的问题,喏,这回被我切实体会了一次。


知识点 3:在使用 pytest 过程中,用到了大量的装饰器,我就在想,函数上方罗列这么多装饰器有没有前后顺序要求?多个装饰器的执行顺序是怎样的?


百度及实践后得到以下结论:
装饰器函数的执行顺序是分为(被装饰函数)定义阶段和(被装饰函数)执行阶段的,装饰器函数在被装饰函数定义好后立即执行;
在函数定义阶段:执行顺序是从最靠近函数的装饰器开始,自内而外的执行;
在函数执行阶段:执行顺序由外而内,一层层执行;



本次实践过程中掉坑和爬坑过程就写这么多吧,如果你也有有趣的爬坑经验,也可以写在留言区,大家相互讨论下


其实这次我不光重构了第四版测试框架,还买了个服务器,并且在服务器上搭建了jenkins,最终实现了jenkins每天定时使用git拉取代码,自动构建项目,并且把测试报告发送到我邮箱的效果。我打算也将这个过程总结输出一下,可能更多是每一步的操作步骤以及一些注意事项,应该会类似于CI/CD创建说明书。如果感兴趣的伙伴可以继续关注一下~


点击文末“阅读原文”进入社区帖子,与作者交流~




1.A股牛市?盘点软件测试领域的上市公司

2.微信鸿蒙原生版开启内测邀请

3.已开源!一款支持HarmonyOS NEXT系统的UI自动化框架hmdriver2发布

4.测试团队FastGPT实战,解锁AI大模型知识库搭建秘籍

5.AI测试|自己搭一个AI Agent玩玩

6.MTSC2024上海大会,现场录播视频


TesterHome社区
测试之家(TesterHome)由一线测试工程师发起和运营的测试技术社区,社区主旨是公益、开源、分享、落地,紧跟前沿技术趋势,致力于推进软件质量保障与安全,是软件质量保障领域的风向标。我们的理念:Coding Share Show Cool
 最新文章