51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 3753|回复: 5
打印 上一主题 下一主题

[转贴] ApiTestEngine 演化之路 (1) 搭建基础框架

[复制链接]
  • TA的每日心情
    无聊
    2024-7-12 13:16
  • 签到天数: 1 天

    连续签到: 1 天

    [LV.1]测试小兵

    跳转到指定楼层
    1#
    发表于 2017-6-28 10:44:49 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    在《ApiTestEngine 演化之路(0)开发未动,测试先行》一文中,我对ApiTestEngine项目正式开始前的准备工作进行了介绍,包括构建API接口服务(Mock Server)、搭建项目单元测试框架、实现持续集成构建检查机制(Travis CI)等。

    接下来,我们就开始构建ApiTestEngine项目的基础框架,实现基本功能吧。
    接口测试的核心要素

    既然是从零开始,那我们不妨先想下,对于接口测试来说,最基本最核心的要素有哪些?

    事实上,不管是手工进行接口测试,还是自动化测试平台执行接口测试,接口测试的核心要素都可以概括为如下三点:

    发起接口请求(Request)
    解析接口响应(Response)
    检查接口测试结果
    这对于任意类型的接口测试也都是适用的。

    在本系列文章中,我们关注的是API接口的测试,更具体地,是基于HTTP协议的API接口的测试。所以我们的问题就进一步简化了,只需要关注HTTP协议层面的请求和响应即可。

    好在对于绝大多数接口系统,都有明确的API接口文档,里面会定义好接口请求的参数(包括Headers和Body),并同时描述好接口响应的内容(包括Headers和Body)。而我们需要做的,就是根据接口文档的描述,在HTTP请求中按照接口规范填写请求的参数,然后读取接口的HTTP响应内容,将接口的实际响应内容与我们的预期结果进行对比,以此判断接口功能是否正常。这里的预期结果,应该是包含在接口测试用例里面的。

    由此可知,实现接口测试框架的第一步是完成对HTTP请求响应处理的支持。

    HTTP客户端的最佳选择

    ApiTestEngine项目选择Python作为编程语言,而在Python中实现HTTP请求,毫无疑问,Requests库是最佳选择,简洁优雅,功能强大,可轻松支持API接口的多种请求方法,包括GET/POST/HEAD/PUT/DELETE等。

    并且,更赞的地方在于,Requests库针对所有的HTTP请求方法,都可以采用一套统一的接口。

    requests.request(method, url, **kwargs)
    其中,kwargs中可以包含HTTP请求的所有可能需要用到的信息,例如headers、cookies、params、data、auth等。

    这有什么好处呢?

    好处在于,这可以帮助我们轻松实现测试数据与框架代码的分离。我们只需要遵循Requests库的参数规范,在接口测试用例中复用Requests参数的概念即可。而对于框架的测试用例执行引擎来说,处理逻辑就异常简单了,直接读取测试用例中的参数,传参给Requests发起请求即可。

    如果还感觉不好理解,没关系,直接看案例。

    测试用例描述

    在我们搭建的API接口服务(Mock Server)中,我们想测试“创建一个用户,该用户之前不存在”的场景

    在上一篇文章中,我们也在unittest中对该测试场景实现了测试脚本。
    1. def test_create_user_not_existed(self):
    2.    self.clear_users()

    3.    url = "%s/api/users/%d" % (self.host, 1000)
    4.    data = {
    5.        "name": "user1",
    6.        "password": "123456"
    7.    }
    8.    resp = self.api_client.post(url, json=data)

    9.    self.assertEqual(201, resp.status_code)
    10.    self.assertEqual(True, resp.json()["success"])
    复制代码
    在该用例中,我们实现了HTTP POST请求,api_client.post(url, json=data),然后对响应结果进行解析,并检查resp.status_code、resp.json()["success"]是否满足预期。

    可以看出,采用代码编写测试用例时会用到许多编程语言的语法,对于不会编程的人来说上手难度较大。更大的问题在于,当我们编写大量测试用例之后,因为模式基本都是固定的,所以会发现存在大量相似或重复的脚本,这给脚本的维护带来了很大的问题。

    那如何将测试用例与脚本代码进行分离呢?

    考虑到JSON格式在编程语言中处理是最方便的,分离后的测试用例可采用JSON描述如下:
    1. {
    2.    "name": "create user which does not exist",
    3.    "request": {
    4.        "url": "http://127.0.0.1:5000/api/users/1000",
    5.        "method": "POST",
    6.        "headers": {
    7.            "content-type": "application/json"
    8.        },
    9.        "json": {
    10.            "name": "user1",
    11.            "password": "123456"
    12.        }
    13.    },
    14.    "response": {
    15.        "status_code": 201,
    16.        "headers": {
    17.            "Content-Type": "application/json"
    18.        },
    19.        "body": {
    20.            "success": true,
    21.            "msg": "user created successfully."
    22.        }
    23.    }
    24. }
    复制代码
    不难看出,如上JSON结构体包含了测试用例的完整描述信息。

    需要特别注意的是,这里使用了一个讨巧的方式,就是在请求的参数中充分复用了Requests的参数规范。例如,我们要POST一个JSON的结构体,那么我们就直接将json作为request的参数名,这和前面写脚本时用的api_client.post(url, json=data)是一致的。

    测试用例执行引擎

    在如上测试用例描述的基础上,测试用例执行引擎就很简单了,以下几行代码就足够了。
    1. def run_single_testcase(testcase):
    2.    req_kwargs = testcase['request']

    3.    try:
    4.        url = req_kwargs.pop('url')
    5.        method = req_kwargs.pop('method')
    6.    except KeyError:
    7.        raise exception.ParamsError("Params Error")

    8.    resp_obj = requests.request(url=url, method=method, **req_kwargs)
    9.    diff_content = utils.diff_response(resp_obj, testcase['response'])
    10.    success = False if diff_content else True
    11.    return success, diff_content
    复制代码
    可以看出,不管是什么HTTP请求方法的用例,该执行引擎都是适用的。

    只需要先从测试用例中获取到HTTP接口请求参数,testcase['request']:
    1. {
    2.   "url": "http://127.0.0.1:5000/api/users/1000",
    3.   "method": "POST",
    4.   "headers": {
    5.       "content-type": "application/json"
    6.   },
    7.   "json": {
    8.       "name": "user1",
    9.       "password": "123456"
    10.   }
    11. }
    复制代码
    然后发起HTTP请求:
    1. requests.request(url=url, method=method, **req_kwargs)
    复制代码
    最后再检查测试结果:
    1. utils.diff_response(resp_obj, testcase['response'])
    复制代码
    在测试用例执行引擎完成后,执行测试用例的方式也很简单。同样是在unittest中调用执行测试用例,就可以写成如下形式:
    1. def test_run_single_testcase_success(self):
    2.    testcase_file_path = os.path.join(os.getcwd(), 'test/data/demo.json')
    3.    testcases = utils.load_testcases(testcase_file_path)
    4.    success, _ = self.test_runner.run_single_testcase(testcases[0])
    5.    self.assertTrue(success)
    复制代码

    可以看出,模式还是很固定:加载用例、执行用例、判断用例执行是否成功。如果每条测试用例都要在unittest.TestCase分别写一个单元测试进行调用,还是会存在大量重复工作。

    所以比较好的做法是,再实现一个单元测试用例生成功能;这部分先不展开,后面再进行详细描述。

    结果判断处理逻辑

    这里再单独讲下对结果的判断逻辑处理,也就是diff_response函数。

    1. def diff_response(resp_obj, expected_resp_json)
    2.     diff_content = {}
    3.     resp_info = parse_response_object(resp_obj)

    4.     # 对比 status_code,将差异存入 diff_content
    5.     # 对比 Headers,将差异存入 diff_content
    6.     # 对比 Body,将差异存入 diff_content

    7.     return diff_content
    复制代码

    其中,expected_resp_json参数就是我们在测试用例中描述的response部分,作为测试用例的预期结果描述信息,是判断实际接口响应是否正常的参考标准。

    而resp_obj参数,就是实际接口响应的Response实例,详细的定义可以参考requests.Response描述文档。

    为了更好地实现结果对比,我们也将resp_obj解析为与expected_resp_json相同的数据结构。

    1. def parse_response_object(resp_obj):
    2.     try:
    3.         resp_body = resp_obj.json()
    4.     except ValueError:
    5.         resp_body = resp_obj.text

    6.     return {
    7.         'status_code': resp_obj.status_code,
    8.         'headers': resp_obj.headers,
    9.         'body': resp_body
    10.     }
    复制代码

    那么最后再进行对比就很好实现了,只需要编写一个通用的JSON结构体比对函数即可。

    1. def diff_json(current_json, expected_json):
    2.     json_diff = {}

    3.     for key, expected_value in expected_json.items():
    4.         value = current_json.get(key, None)
    5.         if str(value) != str(expected_value):
    6.             json_diff[key] = {
    7.                 'value': value,
    8.                 'expected': expected_value
    9.             }

    10.     return json_diff
    复制代码

    这里只罗列了核心处理流程的代码实现,其它的辅助功能,例如加载JSON/YAML测试用例等功能

    总结

    经过本文中的工作,我们已经完成了ApiTestEngine基础框架的搭建,并实现了两项最基本的功能:

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

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

    然而,在实际项目中的接口通常比较复杂,例如包含签名校验等机制,这使得我们在配置接口测试用例时还是会比较繁琐。



    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    该用户从未签到

    3#
    发表于 2017-6-28 16:21:22 | 只看该作者
    这篇不错啊,json数据如何维护(比如有一天某个字段发生了变化所有case都要改),你们直接手写json还是做了图形界面?或者有批量修改用的脚本?
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    无聊
    前天 09:07
  • 签到天数: 11 天

    连续签到: 2 天

    [LV.3]测试连长

    4#
    发表于 2017-6-28 16:22:02 | 只看该作者
    先了解,还没有开始接口测试
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    5#
    发表于 2017-6-28 16:22:40 | 只看该作者
    请问你一下,你这个运行结果有输出报告吗?
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    无聊
    2024-7-12 13:16
  • 签到天数: 1 天

    连续签到: 1 天

    [LV.1]测试小兵

    6#
     楼主| 发表于 2017-6-28 16:23:30 | 只看该作者
    小爸爸 发表于 2017-6-28 16:22
    请问你一下,你这个运行结果有输出报告吗?

    会有的,不过当前还没有完成这块儿
    回复 支持 反对

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-9-21 05:30 , Processed in 0.066031 second(s), 22 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表