HttpRunner 接口自动化测试的最佳工程实践 (ApiTestEngine)

debugtalk · 2017年07月10日 · 最后由 riku5596 回复于 2018年08月28日 · 4448 次阅读
本帖已被设为精华帖!

背景

当前市面上存在的接口测试工具已经非常多,常见的如PostmanJMeterRobotFramework等,相信大多数测试人员都有使用过,至少从接触到的大多数简历的描述上看是这样的。除了这些成熟的工具,也有很多有一定技术能力的测试(开发)人员自行开发了一些接口测试框架,质量也是参差不齐。

但是,当我打算在项目组中推行接口自动化测试时,搜罗了一圈,也没有找到一款特别满意的工具或框架,总是与理想中的构想存在一定的差距。

那么理想中的接口自动化测试框架应该是怎样的呢?

测试工具(框架)脱离业务使用场景都是耍流氓!所以我们不妨先来看下日常工作中的一些常见场景。

  • 测试或开发人员在定位问题的时候,想调用某个接口查看其是否响应正常;
  • 测试人员在手工测试某个功能点的时候,需要一个订单号,而这个订单号可以通过顺序调用多个接口实现下单流程;
  • 测试人员在开始版本功能测试之前,可以先检测下系统的所有接口是否工作正常,确保接口正常后再开始手工测试;
  • 开发人员在提交代码前需要检测下新代码是否对系统的已有接口产生影响;
  • 项目组需要每天定时检测下测试环境所有接口的工作情况,确保当天的提交代码没有对主干分支的代码造成破坏;
  • 项目组需要定时(30分钟)检测下生产环境所有接口的工作情况,以便及时发现生产环境服务不可用的情况;
  • 项目组需要不定期对核心业务场景进行性能测试,期望能减少人力投入,直接复用接口测试中的工作成果。

可以看到,以上罗列的场景大家应该都很熟悉,这都是我们在日常工作中经常需要去做的事情。但是在没有一款合适工具的情况下,效率往往十分低下,或者就是某些重要工作压根就没有开展,例如接口回归测试、线上接口监控等。

先说下最简单的手工调用接口测试。可能有人会说,Postman就可以满足需求啊。的确,Postman作为一款通用的接口测试工具,它可以构造接口请求,查看接口响应,从这个层面上来说,它是满足了接口测试的功能需求。但是在具体的项目中,使用Postman并不是那么高效。

不妨举个最常见的例子。

某个接口的请求参数非常多,并且接口请求要求有MD5签名校验;签名的方式为在Headers中包含一个sign参数,该参数值通过对URLMethodBody的拼接字符串进行MD5计算后得到。

回想下我们要对这个接口进行测试时是怎么做的。首先,我们需要先参照接口文档的描述,手工填写完所有接口参数;然后,按照签名校验方式,对所有参数值进行拼接得到一个字符串,在另一个MD5计算工具计算得到其MD5值,将签名值填入sign参数;最后,才是发起接口请求,查看接口响应,并人工检测响应是否正常。最坑爹的是,我们每次需要调用这个接口的时候,以上工作就得重新来一遍。这样的实际结果是,面对参数较多或者需要签名验证的接口时,测试人员可能会选择忽略不进行接口测试。

除了单个接口的调用,很多时候我们也需要组合多个接口进行调用。例如测试人员在测试物流系统时,经常需要一个特定组合条件下生成的订单号。而由于订单号关联的业务较多,很难直接在数据库中生成,因此当前业务测试人员普遍采取的做法,就是每次需要订单号时模拟下单流程,顺序调用多个相应的接口来生成需要的订单号。可以想象,在手工调用单个接口都如此麻烦的情况下,每次都要手工调用多个接口会有多么的费时费力。

再说下接口自动化调用测试。这一块儿大多接口测试框架都支持,普遍的做法就是通过代码编写接口测试用例,或者采用数据驱动的方式,然后在支持命令行(CLI)调用的情况下,就可以结合Jenkins或者crontab实现持续集成,或者定时接口监控的功能。

思路是没有问题的,问题在于实际项目中的推动落实情况。要说自动化测试用例最靠谱的维护方式,还是直接通过代码编写测试用例,可靠且不失灵活性,这也是很多经历过惨痛教训的老手的感悟,甚至网络上还出现了一些反测试框架的言论。但问题在于项目中的测试人员并不是都会写代码,也不是对其强制要求就能马上学会的。这种情况下,要想在具体项目中推动接口自动化测试就很难,就算我可以帮忙写一部分,但是很多时候接口测试用例也是要结合业务逻辑场景的,我也的确是没法在这方面投入太多时间,毕竟对接的项目实在太多。所以也是基于这类原因,很多测试框架提倡采用数据驱动的方式,将业务测试用例和执行代码分离。不过由于很多时候业务场景比较复杂,大多数框架测试用例模板引擎的表达能力不足,很难采用简洁的方式对测试场景进行描述,从而也没法很好地得到推广使用。

可以列举的问题还有很多,这些也的确都是在互联网企业的日常测试工作中真实存在的痛点。

基于以上背景,我产生了开发ApiTestEngine的想法。

对于ApiTestEngine的定位,与其说它是一个工具或框架,它更多的应该是一套接口自动化测试的最佳工程实践,而简洁优雅实用应该是它最核心的特点。

当然,每位工程师对最佳工程实践的理念或多或少都会存在一些差异,也希望大家能多多交流,在思维的碰撞中共同进步。

核心特性

ApiTestEngine的核心特性概述如下:

  • 支持API接口的多种请求方法,包括 GET/POST/HEAD/PUT/DELETE 等
  • 测试用例与代码分离,测试用例维护方式简洁优雅,支持YAML
  • 测试用例描述方式具有表现力,可采用简洁的方式描述输入参数和预期输出结果
  • 接口测试用例具有可复用性,便于创建复杂测试场景
  • 测试执行方式简单灵活,支持单接口调用测试、批量接口调用测试、定时任务执行测试
  • 测试结果统计报告简洁清晰,附带详尽日志记录,包括接口请求耗时、请求响应数据等
  • 身兼多职,同时实现接口管理、接口自动化测试、接口性能测试(结合Locust)
  • 具有可扩展性,便于扩展实现Web平台化

特性拆解介绍

支持API接口的多种请求方法,包括 GET/POST/HEAD/PUT/DELETE 等

个人偏好,编程语言选择Python。而采用Python实现HTTP请求,最好的方式就是采用Requests库了,简洁优雅,功能强大。

测试用例与代码分离,测试用例维护方式简洁优雅,支持YAML

要实现测试用例与代码的分离,最好的做法就是做一个测试用例加载引擎和一个测试用例执行引擎,这也是之前在做AppiumBooster框架的时候总结出来的最优雅的实现方式。当然,这里需要事先对测试用例制定一个标准的数据结构规范,作为测试用例加载引擎和测试用例执行引擎的桥梁。

需要说明的是,测试用例数据结构必须包含接口测试用例完备的信息要素,包括接口请求的信息内容(URL、Headers、Method等参数),以及预期的接口请求响应结果(StatusCode、ResponseHeaders、ResponseContent)。

这样做的好处在于,不管测试用例采用什么形式进行描述(YAML、JSON、CSV、Excel、XML等),也不管测试用例是否采用了业务分层的组织思想,只要在测试用例加载引擎中实现对应的转换器,都可以将业务测试用例转换为标准的测试用例数据结构。而对于测试用例执行引擎而言,它无需关注测试用例的具体描述形式,只需要从标准的测试用例数据结构中获取到测试用例信息要素,包括接口请求信息和预期接口响应信息,然后构造并发起HTTP请求,再将HTTP请求的响应结果与预期结果进行对比判断即可。

至于为什么明确说明支持YAML,这是因为个人认为这是最佳的测试用例描述方式,表达简洁不累赘,同时也能包含非常丰富的信息。当然,这只是个人喜好,如果喜欢采用别的方式,只需要扩展实现对应的转换器即可。

测试用例描述方式具有表现力,可采用简洁的方式描述输入参数和预期输出结果

测试用例与框架代码分离以后,对业务逻辑测试场景的描述重任就落在测试用例上了。比如我们选择采用YAML来描述测试用例,那么我们就应该能在YAML中描述各种复杂的业务场景。

那么怎么理解这个“表现力”呢?

简单的参数值传参应该都容易理解,我们举几个相对复杂但又比较常见的例子。

  • 接口请求参数中要包含当前的时间戳;
  • 接口请求参数中要包含一个16位的随机字符串;
  • 接口请求参数中包含签名校验,需要对多个请求参数进行拼接后取md5值;
  • 接口响应头(Headers)中要包含一个X-ATE-V头域,并且需要判断该值是否大于100;
  • 接口响应结果中包含一个字符串,需要校验字符串中是否包含10位长度的订单号;
  • 接口响应结果为一个多层嵌套的json结构体,需要判断某一层的某一个元素值是否为True。

可以看出,以上几个例子都是没法直接在测试用例里面描述参数值的。如果是采用Python脚本来编写测试用例还好解决,只需要通过Python函数实现即可。但是现在测试用例和框架代码分离了,我们没法在YAML里面执行Python函数,这该怎么办呢?

答案就是,定义函数转义符,实现自定义模板。

这种做法其实也不难理解,也算是模板语言通用的方式。例如,我们将${}定义为转义符,那么在{}内的内容就不再当做是普通的字符串,而应该转义为变量值,或者执行函数得到实际结果。当然,这个需要我们在测试用例执行引擎进行适配实现,最简单方式就是提取出${}中的字符串,通过eval计算得到表达式的值。如果要实现更复杂的功能,我们也可以将接口测试中常用的一些功能封装为一套关键字,然后在编写测试用例的时候使用这些关键字。

接口测试用例具有可复用性,便于创建复杂测试场景

很多情况下,系统的接口都是有业务逻辑关联的。例如,要请求调用登录接口,需要先请求获取验证码的接口,然后在登录请求中带上获取到的验证码;而要请求数据查询的接口,又要在请求参数中包含登录接口返回的session值。这个时候,我们如果针对每一个要测的业务逻辑,都单独描述要请求的接口,那么就会造成大量的重复描述,测试用例的维护也十分臃肿。

比较好的做法是,将每一个接口调用单独封装为一条测试用例,然后在描述业务测试场景时,选择对应的接口,按照顺序拼接为业务场景测试用例,就像搭积木一般。如果你之前读过AppiumBooster的介绍,应该还会联想到,我们可以将常用的功能组成模块用例集,然后就可以在更高的层面对模块用例集进行组装,实现更复杂的测试场景。

不过,这里有一个非常关键的问题需要解决,就是如何在接口测试用例之前传参的问题。其实实现起来也不复杂,我们可以在接口请求响应结果中指定一个变量名,然后将接口返回关键值提取出来后赋值给那个变量;然后在其它接口请求参数中,传入这个${变量名}即可。

测试执行方式简单灵活,支持单接口调用测试、批量接口调用测试、定时任务执行测试

通过背景中的例子可以看出,需要使用接口测试工具的场景很多,除了定时地对所有接口进行自动化测试检测外,很多时候在手工测试的时候也需要采用接口测试工具进行辅助,也就是半手工+半自动化的模式。

而业务测试人员在使用测试工具的时候,遇到的最大问题在于除了需要关注业务功能本身,还需要花费很多时间去处理技术实现细节上的东西,例如签名校验这类情况,而且往往后者在重复操作中占用的时间更多。

这个问题的确是没法避免的,毕竟不同系统的接口千差万别,不可能存在一款工具可以自动处理所有情况。但是我们可以尝试将接口的技术细节实现和业务参数进行拆分,让业务测试人员只需要关注业务参数部分。

具体地,我们可以针对每一个接口配置一个模板,将其中与业务功能无关的参数以及技术细节封装起来,例如签名校验、时间戳、随机值等,而与业务功能相关的参数配置为可传参的模式。

这样做的好处在于,与业务功能无关的参数以及技术细节我们只需要封装配置一次,而且这个工作可以由开发人员或者测试开发人员来实现,减轻业务测试人员的压力;接口模板配置好后,测试人员只需要关注与业务相关的参数即可,结合业务测试用例,就可以在接口模板的基础上很方便地配置生成多个接口测试用例。

测试结果统计报告简洁清晰,附带详尽日志记录,包括接口请求耗时、请求响应数据等

测试结果统计报告,应该遵循简洁而不简单的原则。“简洁”,是因为大多数时候我们只需要在最短的时间内判断所有接口是否运行正常即可。而“不简单”,是因为当存在执行失败的测试用例时,我们期望能获得接口测试时尽可能详细的数据,包括测试时间、请求参数、响应内容、接口响应耗时等。

之前在读locust源码时,其对HTTP客户端的封装方式给我留下了深刻的印象。它采用的做法是,继承requests.Session类,在子类HttpSession中重写覆盖了request方法,然后在request方法中对requests.Session.request进行了一层封装。

request_meta = {}

# set up pre_request hook for attaching meta data to the request object
request_meta["method"] = method
request_meta["start_time"] = time.time()

response = self._send_request_safe_mode(method, url, **kwargs)

# record the consumed time
request_meta["response_time"] = int((time.time() - request_meta["start_time"]) * 1000)

request_meta["content_size"] = int(response.headers.get("content-length") or 0)

HttpLocust的每一个虚拟用户(client)都是一个HttpSession实例,这样每次在执行HTTP请求的时候,既可充分利用Requests库的强大功能,同时也能将请求的响应时间、响应体大小等原始性能数据进行保存,实现可谓十分优雅。

受到该处启发,要保存接口的详细请求响应数据也可采用同样的方式。例如,要保存ResponseHeadersBody只需要增加如下两行代码:

request_meta["response_headers"] = response.headers
request_meta["response_content"] = response.content

身兼多职,同时实现接口管理、接口自动化测试、接口性能测试(结合Locust)

其实像接口性能测试这样的需求,不应该算到接口自动化测试框架的职责范围之内。但是在实际项目中需求就是这样,又要做接口自动化测试,又要做接口性能测试,而且还不想同时维护两套代码。

多亏有了locust性能测试框架,接口自动化和性能测试脚本还真能合二为一。

前面也讲了,HttpLocust的每一个虚拟用户(client)都是一个HttpSession实例,而HttpSession又继承自requests.Session类,所以HttpLocust的每一个虚拟用户(client)也是requests.Session类的实例。

同样的,我们在用Requests库做接口测试时,请求客户端其实也是requests.Session类的实例,只是我们通常用的是requests的简化用法。

以下两种用法是等价的。

resp = requests.get('http://debugtalk.com')

# 等价于
client = requests.Session()
resp = client.get('http://debugtalk.com')

有了这一层关系以后,要在接口自动化测试和性能测试之间切换就很容易了。在接口测试框架内,可以通过如下方式初始化HTTP客户端。

def __init__(self, origin, kwargs, http_client_session=None):
self.http_client_session = http_client_session or requests.Session()

默认情况下,http_client_sessionrequests.Session的实例,用于进行接口测试;当需要进行性能测试时,只需要传入locustHttpSession实例即可。

具有可扩展性,便于扩展实现Web平台化

当要将测试平台推广至更广阔的用户群体(例如产品经理、运营人员)时,对框架实现Web化就在所难免了。在Web平台上查看接口测试用例运行情况、对接口模块进行配置、对接口测试用例进行管理,的确会便捷很多。

不过对于接口测试框架来说,Web平台只能算作锦上添花的功能。我们在初期可以优先实现命令行(CLI)调用方式,规范好数据存储结构,后期再结合Web框架(如Flask)增加实现Web平台功能。

写在后面

以上便是我对ApiTestEngine特性的详细介绍,也算是我个人对接口自动化测试最佳工程实践的理念阐述。

当前,ApiTestEngine还处于开发过程中,代码也开源托管在GitHub上,欢迎Star关注。

GitHub项目地址:https://github.com/debugtalk/ApiTestEngine

参考

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 31 条回复 时间 点赞

从个人经验看,接口测试的最佳工程实践应该包含从开发的接口定义到测试脚本(至少是单接口的参数遍历脚本)全自动化生成,这样才能保证接口测试的准确性、覆盖率和运作效率。

xubin98246 回复

嗯,能实现这样的话更好,但是在实际项目中实施起来比较难,除非是项目立项之初就有比较强的推动力

同意楼上并补充(我个人认为还算合格的接口平台或者引擎,不喜勿喷)

  1. web化,有搭配合理的ui,进行配置。
  2. 接口文档标准化,可以自动化导入平台,并根据字段类型和算法生成用例。
  3. 满足业务,各种脚本组,依赖解决,加密,token认证等问题的合理解决,api提供Jenkins等持续集成调用。
  4. mock api,可以给客户端和前端同学提供mock引擎,支持并行开发。
  5. 接口性能评估,支持并发,线程组。产出吞吐率报告等相关指标。
gxdprivate 回复

1,web化的确会大大提升易用性,要最终将测试平台推广到更多的项目,web也是必要的;但是单纯对于一款接口测试框架来说,这更多的应该算是锦上添花的功能,所以前期可以考虑将数据结构规范化,快速地在项目中应用起来,在后期再扩展为Web平台;
2、接口文档标准化,很多时候真的是可遇而不可求,特别是在中途接手一个质量不是很高的项目的时候,接口文档滞后或甚至是没有。实现自动生成测试用例这块儿,不知道你们有没有成功实践经验,可否分享下?
3、赞同,也是这么做的。
4、mock api这个,感觉更应该是专门的Mock平台的职责,要放到一个接口测试平台感觉比较牵强。
5、性能测试对于接口测试平台来说也是锦上添花的功能;不过经过设计(我是复用了Locust),也可以实现接口测试用例和性能测试的复用,一份用例实现多个用途,也是不错的。

现在市面上的确是已经有了很多接口测试平台,但是不同公司不同项目的情况差异巨大,所以的确很难说哪款测试工具或平台是最好的。终究还是回到那句话,适合的才是最好的😂

debugtalk 回复

2、接口文档标准化,很多时候真的是可遇而不可求,特别是在中途接手一个质量不是很高的项目的时候,接口文档滞后或甚至是没有。实现自动生成测试用例这块儿,不知道你们有没有成功实践经验,可否分享下?

—— 看下swagger吧,一种标准的接口定义规范,可通过代码注解或者postman collection自动转化生成(当然也可手写^^),而且也是yaml格式,挺适合你这框架的

gxdprivate 回复

说得很全,不过认同 @debugtalk 的观点,第三个是核心,其它是锦上添花。

其实如果接口都是比较统一,而且用了类似 swagger 这类同时定义了规范和实现的框架,接口用例自动生成、mock 这些都会很好做。然而目前接触的实际项目中用了 swagger 的真心不多,更多的是由于各种历史问题,不同系统用不同的接口编写方式,甚至用不同的接口协议。接口框架还是先得能适应这样的实际情况。

xubin98246 回复

对于swagger,之前有简单了解过,但没有实际使用过。想问一下,如果是没有接口文档的系统,或者接口文档采用其它形式描述的系统,要转为使用swagger的话成本高么?其实从QA的角度,的确也应该去推动做这些事情,但就还是得考虑实际的投入产出比。

chenhengjie123 回复

嗯,也许这就是软件工程跟计算机科学的差异所在吧😂

Ningxw 关于接口自动化的那些事 - 基于 Python 中提及了此贴 07月12日 14:45
debugtalk 回复

可以参考ApiMatic,能实现各种借口定义之间的转化,我们项目使用了Postman collection -> Swagger的方式,配合chrome到postman中的导流功能,接口定义生成本身的成本不高。

xubin98246 回复

感谢推荐,我去了解下

文中说的关于测试用例的模板作用,意思是测试开发人员需要在接口测试用例上把需要的库或者参数配置好,让业务测试人员修改传参的参数即可。但每个接口传参都不一样,因此引用的库也不一样,校验返回值也不一样,那测试开发人员就需要在模板上针对接口的不同去引用不同的库和不同的校验值,业务测试人员或许懂得修改校验值但不一定懂得需要引入哪些库,这样的话,还是需要测试开发人员定义好很多配置的东西,既然都做了这么多,那自己传参岂不是更方便些?这样的话把接口交由业务测试人员好像也不大需要了?不知道我的理解对不对,还请指教下~🙋 🙋

测试用例执行失败后报告中没有将具体的请求和响应展示出来,这块对问题分析很重要,我看你文档描述里说有做这个功能,但是运行结果中好像没有

gzs4850 回复

当前的确是没有展示到html报告里面。运行测试时,添加--log-level debug,在Terminal中可以看到。

Lihuazhang 将本帖设为了精华贴 08月13日 16:54

我试用了一下,效果还不错,但是对于加密的接口(http body使用了AES加密)怎么处理呢

ghost62184 回复

可以将加解密算法实现到debugtalk.py中,然后在测试用例中调用加解密函数。这方面之前写过好多了,可以找到相关文章看下。

debugtalk 回复

好的,我先看看,谢谢啦

可以web化,并且提供在线函数编程,按需扩招,也不会失去coding的机会。

自动化案例生成与遍历,我们这边实现过,利用VB宏在EXCEL里面生成,然后用我们自己的自动化产品调用。但感觉不是很好用,其实就是根据接口参数然后自动执行了打印结果,维护比较麻烦,这都是其次,主要是在实际过程中,单调接口是不符合业务逻辑的,很多接口的调用需要前面数据的支撑,然后在这个时候跑自动的不好区分

(顺带说一句,我们自己的产品和你的思想根本就是一模一样,连名字都一样,不过没集成性能和WEB,其它已经全部实现。包括都数据库连接,自定义方法,自定义函数,参数以及各种验证,支持C、java、SQL)

shenshen 回复

你们的产品是商业工具?

debugtalk 回复

算是吧,在今年年初申请了软件著作权,现在研发部还在不断的升级。几年前投入使用的,不过没有用这个来收费,只是作为我们的辅助工具,其实都是自己研发自己用。

请教楼主,extract方法可以抽取前一个接口response header中的数据吗?

可以的

debugtalk 回复

是通过content.header.XXX来获取吗?

janice1027 回复

headers.XXX

debugtalk 回复

刚刚在您的另一篇文章上看到了,很惭愧。你这么无私的分享开源框架,还要解答很多小白问题,真的从心底里佩服你。

我想请教一下,extract提取参数这里,可以提取header的内容么,我看了一下,好像没有的样子~~😂

hellomm 回复

可以。

extract:
- content_type: headers.Content-Type
debugtalk 回复

thanks

这个帖子是论坛上关于接口测试最有深度的

cyj86 回复

实在是过奖了哈,这款框架也是逐步迭代优化出来的 😂

没有办法更新了

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册