## 特性拆解介绍 > 支持API接口的多种请求方法,包括 GET/POST/HEAD/PUT/DELETE 等 个人偏好,编程语言选择Python。而采用Python实现HTTP请求,最好的方式就是采用[`Requests`][Requests]库了,简洁优雅,功能强大。 > 测试用例与代码分离,测试用例维护方式简洁优雅,支持`YAML` 要实现测试用例与代码的分离,最好的做法就是做一个测试用例加载引擎和一个测试用例执行引擎,这也是之前在做[`AppiumBooster`][AppiumBooster]框架的时候总结出来的最优雅的实现方式。当然,这里需要事先对测试用例制定一个标准的数据结构规范,作为测试用例加载引擎和测试用例执行引擎的桥梁。 需要说明的是,测试用例数据结构必须包含接口测试用例完备的信息要素,包括接口请求的信息内容(URL、Headers、Method等参数),以及预期的接口请求响应结果(StatusCode、ResponseHeaders、ResponseContent)。 这样做的好处在于,不管测试用例采用什么形式进行描述([`YAML`][YAML]、JSON、CSV、Excel、XML等),也不管测试用例是否采用了业务分层的组织思想,只要在测试用例加载引擎中实现对应的转换器,都可以将业务测试用例转换为标准的测试用例数据结构。而对于测试用例执行引擎而言,它无需关注测试用例的具体描述形式,只需要从标准的测试用例数据结构中获取到测试用例信息要素,包括接口请求信息和预期接口响应信息,然后构造并发起HTTP请求,再将HTTP请求的响应结果与预期结果进行对比判断即可。 至于为什么明确说明支持[`YAML`][YAML],这是因为个人认为这是最佳的测试用例描述方式,表达简洁不累赘,同时也能包含非常丰富的信息。当然,这只是个人喜好,如果喜欢采用别的方式,只需要扩展实现对应的转换器即可。 > 测试用例描述方式具有表现力,可采用简洁的方式描述输入参数和预期输出结果 测试用例与框架代码分离以后,对业务逻辑测试场景的描述重任就落在测试用例上了。比如我们选择采用[`YAML`][YAML]来描述测试用例,那么我们就应该能在[`YAML`][YAML]中描述各种复杂的业务场景。 那么怎么理解这个“表现力”呢? 简单的参数值传参应该都容易理解,我们举几个相对复杂但又比较常见的例子。 - 接口请求参数中要包含当前的时间戳; - 接口请求参数中要包含一个16位的随机字符串; - 接口请求参数中包含签名校验,需要对多个请求参数进行拼接后取md5值; - 接口响应头(Headers)中要包含一个`X-ATE-V`头域,并且需要判断该值是否大于100; - 接口响应结果中包含一个字符串,需要校验字符串中是否包含10位长度的订单号; - 接口响应结果为一个多层嵌套的json结构体,需要判断某一层的某一个元素值是否为True。 可以看出,以上几个例子都是没法直接在测试用例里面描述参数值的。如果是采用Python脚本来编写测试用例还好解决,只需要通过Python函数实现即可。但是现在测试用例和框架代码分离了,我们没法在[`YAML`][YAML]里面执行Python函数,这该怎么办呢? 答案就是,定义函数转义符,实现自定义模板。 这种做法其实也不难理解,也算是模板语言通用的方式。例如,我们将`${}`定义为转义符,那么在`{}`内的内容就不再当做是普通的字符串,而应该转义为变量值,或者执行函数得到实际结果。当然,这个需要我们在测试用例执行引擎进行适配实现,最简单方式就是提取出`${}`中的字符串,通过`eval`计算得到表达式的值。如果要实现更复杂的功能,我们也可以将接口测试中常用的一些功能封装为一套关键字,然后在编写测试用例的时候使用这些关键字。 > 接口测试用例具有可复用性,便于创建复杂测试场景 很多情况下,系统的接口都是有业务逻辑关联的。例如,要请求调用登录接口,需要先请求获取验证码的接口,然后在登录请求中带上获取到的验证码;而要请求数据查询的接口,又要在请求参数中包含登录接口返回的session值。这个时候,我们如果针对每一个要测的业务逻辑,都单独描述要请求的接口,那么就会造成大量的重复描述,测试用例的维护也十分臃肿。 比较好的做法是,将每一个接口调用单独封装为一条测试用例,然后在描述业务测试场景时,选择对应的接口,按照顺序拼接为业务场景测试用例,就像搭积木一般。如果你之前读过[`AppiumBooster`][AppiumBooster]的介绍,应该还会联想到,我们可以将常用的功能组成模块用例集,然后就可以在更高的层面对模块用例集进行组装,实现更复杂的测试场景。 不过,这里有一个非常关键的问题需要解决,就是如何在接口测试用例之前传参的问题。其实实现起来也不复杂,我们可以在接口请求响应结果中指定一个变量名,然后将接口返回关键值提取出来后赋值给那个变量;然后在其它接口请求参数中,传入这个`${变量名}`即可。 > 测试执行方式简单灵活,支持单接口调用测试、批量接口调用测试、定时任务执行测试 通过背景中的例子可以看出,需要使用接口测试工具的场景很多,除了定时地对所有接口进行自动化测试检测外,很多时候在手工测试的时候也需要采用接口测试工具进行辅助,也就是`半手工+半自动化`的模式。 而业务测试人员在使用测试工具的时候,遇到的最大问题在于除了需要关注业务功能本身,还需要花费很多时间去处理技术实现细节上的东西,例如签名校验这类情况,而且往往后者在重复操作中占用的时间更多。 这个问题的确是没法避免的,毕竟不同系统的接口千差万别,不可能存在一款工具可以自动处理所有情况。但是我们可以尝试将接口的技术细节实现和业务参数进行拆分,让业务测试人员只需要关注业务参数部分。 具体地,我们可以针对每一个接口配置一个模板,将其中与业务功能无关的参数以及技术细节封装起来,例如签名校验、时间戳、随机值等,而与业务功能相关的参数配置为可传参的模式。 这样做的好处在于,与业务功能无关的参数以及技术细节我们只需要封装配置一次,而且这个工作可以由开发人员或者测试开发人员来实现,减轻业务测试人员的压力;接口模板配置好后,测试人员只需要关注与业务相关的参数即可,结合业务测试用例,就可以在接口模板的基础上很方便地配置生成多个接口测试用例。 > 测试结果统计报告简洁清晰,附带详尽日志记录,包括接口请求耗时、请求响应数据等 测试结果统计报告,应该遵循简洁而不简单的原则。“简洁”,是因为大多数时候我们只需要在最短的时间内判断所有接口是否运行正常即可。而“不简单”,是因为当存在执行失败的测试用例时,我们期望能获得接口测试时尽可能详细的数据,包括测试时间、请求参数、响应内容、接口响应耗时等。 之前在读`locust`源码时,其对[`HTTP`客户端](https://github.com/locustio/locust/blob/master/locust/clients.py )的封装方式给我留下了深刻的印象。它采用的做法是,继承`requests.Session`类,在子类`HttpSession`中重写覆盖了`request`方法,然后在`request`方法中对`requests.Session.request`进行了一层封装。 ```python 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`][Requests]库的强大功能,同时也能将请求的响应时间、响应体大小等原始性能数据进行保存,实现可谓十分优雅。 受到该处启发,要保存接口的详细请求响应数据也可采用同样的方式。例如,要保存`Response`的`Headers`、`Body`只需要增加如下两行代码: ```python 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]库做接口测试时,请求客户端其实也是`requests.Session`类的实例,只是我们通常用的是`requests`的简化用法。 以下两种用法是等价的。 ```python resp = requests.get('http://debugtalk.com') # 等价于 client = requests.Session() resp = client.get('http://debugtalk.com') ``` 有了这一层关系以后,要在接口自动化测试和性能测试之间切换就很容易了。在接口测试框架内,可以通过如下方式初始化`HTTP`客户端。 ```python def __init__(self, origin, kwargs, http_client_session=None): self.http_client_session = http_client_session or requests.Session() ``` 默认情况下,`http_client_session`是`requests.Session`的实例,用于进行接口测试;当需要进行性能测试时,只需要传入`locust`的`HttpSession`实例即可。 > 具有可扩展性,便于扩展实现Web平台化 当要将测试平台推广至更广阔的用户群体(例如产品经理、运营人员)时,对框架实现Web化就在所难免了。在Web平台上查看接口测试用例运行情况、对接口模块进行配置、对接口测试用例进行管理,的确会便捷很多。 不过对于接口测试框架来说,`Web平台`只能算作锦上添花的功能。我们在初期可以优先实现命令行(CLI)调用方式,规范好数据存储结构,后期再结合Web框架(如Flask)增加实现Web平台功能。 [AppiumBooster]: https://github.com/debugtalk/AppiumBooster [Requests]: http://docs.python-requests.org/en/master/ [YAML]: http://pyyaml.org/