51Testing软件测试论坛

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

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 2574|回复: 2
打印 上一主题 下一主题

[转贴] ApiTestEngine 演化之路 (0) 开发未动,测试先行

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

    连续签到: 1 天

    [LV.1]测试小兵

    跳转到指定楼层
    1#
    发表于 2017-6-22 09:32:08 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    在《接口自动化测试的最佳工程实践(ApiTestEngine)》一文中,我详细介绍了ApiTestEngine诞生的背景,并对其核心特性进行了详尽的剖析。
    接下来,我将在《ApiTestEngine演化之路》系列文章中讲解ApiTestEngine是如何从第一行代码开始,逐步实现接口自动化测试框架的核心功能特性的。
    相信大家都有听说过TDD(测试驱动开发)这种开发模式,虽然网络上对该种开发模式存在异议,但我个人是非常推荐使用该种开发方式的。关于TDD的优势,我就不在此赘述了,我就只说下自己受益最深的两个方面。
    • 测试驱动,其实也是需求驱动。在开发正式代码之前,可以先将需求转换为单元测试用例,然后再逐步实现正式代码,直至将所有单元测试用例跑通。这可以帮助我们总是聚焦在要实现的功能特性上,避免跑偏。特别是像我们做测试开发的,通常没有需求文档和设计文档,如果没有清晰的思路,很可能做着做着就不知道自己做到哪儿了。
    • 高覆盖率的单元测试代码,对项目质量有充足的信心。因为是先写测试再写实现,所以正常情况下,所有的功能特性都应该能被单元测试覆盖到。再结合持续集成的手段,我们可以轻松保证每个版本都是高质量并且可用的。
    所以,ApiTestEngine项目也将采用TDD的开发模式。本篇文章就重点介绍下采用TDD之前需要做的一些准备工作。
    搭建API接口服务(Mock Server)接口测试框架要运行起来,必然需要有可用的API接口服务。因此,在开始构建我们的接口测试框架之前,最好先搭建一套简单的API接口服务,也就是Mock Server,然后我们在采用TDD开发模式的时候,就可以随时随地将框架代码跑起来,开发效率也会大幅提升。
    为什么不直接采用已有的业务系统API接口服务呢?
    这是因为通常业务系统的接口比较复杂,并且耦合了许多业务逻辑,甚至还可能涉及到和其它业务系统的交互,搭建或维护一套测试环境的成本可能会非常高。另一方面,接口测试框架需要具有一定的通用性,其功能特性很难在一个特定的业务系统中找到所有合适的接口。就拿最简单的接口请求方法来说,测试框架需要支持GET/POST/HEAD/PUT/DELETE方法,但是可能在我们已有的业务系统中只有GET/POST接口。
    自行搭建API接口服务的另一个好处在于,我们可以随时调整接口的实现方式,来满足接口测试框架特定的功能特性,从而使我们总是能将注意力集中在测试框架本身。比较好的做法是,先搭建最简单的接口服务,在此基础上将接口测试框架搭建起来,实现最基本的功能;后面在实现框架的高级功能特性时,我们再对该接口服务进行拓展升级,例如增加签名校验机制等,来适配测试框架的高级功能特性。
    幸运的是,使用Python搭建API接口服务十分简单,特别是在结合使用Flask框架的情况下。
    例如,我们想实现一套可以对用户账号进行增删改查(CRUD)功能的接口服务,用户账号的存储结构大致如下:
    1. users_dict = {
    2.    'uid1': {
    3.        'name': 'name1',
    4.        'password': 'pwd1'
    5.    },
    6.    'uid2': {
    7.        'name': 'name2',
    8.        'password': 'pwd2'
    9.    }
    10. }
    复制代码
    那么,新增(Create)和更新(Update)功能的接口就可以通过如下方式实现。
    1. import json
    2. from flask import Flask
    3. from flask import request, make_response

    4. app = Flask(__name__)
    5. users_dict = {}

    6. @app.route('/api/users/<int:uid>', methods=['POST'])
    7. def create_user(uid):
    8.     user = request.get_json()
    9.     if uid not in users_dict:
    10.         result = {
    11.             'success': True,
    12.             'msg': "user created successfully."
    13.         }
    14.         status_code = 201
    15.         users_dict[uid] = user
    16.     else:
    17.         result = {
    18.             'success': False,
    19.             'msg': "user already existed."
    20.         }
    21.         status_code = 500

    22.     response = make_response(json.dumps(result), status_code)
    23.     response.headers["Content-Type"] = "application/json"
    24.     return response

    25. @app.route('/api/users/<int:uid>', methods=['PUT'])
    26. def update_user(uid):
    27.     user = users_dict.get(uid, {})
    28.     if user:
    29.         user = request.get_json()
    30.         success = True
    31.         status_code = 200
    32.     else:
    33.         success = False
    34.         status_code = 404

    35.     result = {
    36.         'success': success,
    37.         'data': user
    38.     }
    39.     response = make_response(json.dumps(result), status_code)
    40.     response.headers["Content-Type"] = "application/json"
    41.     return response
    42. 限于篇幅,其它类型的接口实现就不在此赘述,完整的接口实现可以参考项目源码。
    43. 接口服务就绪后,按照Flask官方文档,可以通过如下方式进行启动:
    44. $ export FLASK_APP=test/api_server.py
    45. $ flask run
    46. * Serving Flask app "test.api_server"
    47. * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
    48. 启动后,我们就可以通过请求接口来调用已经实现的接口功能了。例如,先创建一个用户,然后查看所有用户的信息,在Python终端中的调用方式如下:
    49. $ python
    50. Python 3.6.0 (default, Mar 24 2017, 16:58:25)
    51. >>> import requests
    52. >>> requests.post('http://127.0.0.1:5000/api/users/1000', json={'name': 'user1', 'password': '123456'})
    53. <Response [201]>
    54. >>> resp = requests.get('http://127.0.0.1:5000/api/users')
    55. >>> resp.content
    56. b'{"success": true, "count": 1, "items": [{"name": "user1", "password": "123456"}]}'
    57. >>>
    复制代码
    通过接口请求结果可见,接口服务运行正常。
    在单元测试用例中使用 Mock ServerAPI接口服务(Mock Server)已经有了,但是如果每次运行单元测试时都要先在外部手工启动API接口服务的话,做法实在是不够优雅。
    推荐的做法是,制作一个ApiServerUnittest基类,在其中添加setUpClass类方法,用于启动API接口服务(Mock Server);添加tearDownClass类方法,用于停止API接口服务。由于setUpClass会在单元测试用例集初始化的时候执行一次,所以可以保证单元测试用例在运行的时候API服务处于可用状态;而tearDownClass会在单元测试用例集执行完毕后运行一次,停止API接口服务,从而避免对下一次启动产生影响。
    1. # test/base.py
    2. import multiprocessing
    3. import time
    4. import unittest
    5. from . import api_server

    6. class ApiServerUnittest(unittest.TestCase):
    7.     """
    8.     Test case class that sets up an HTTP server which can be used within the tests
    9.     """
    10.     @classmethod
    11.     def setUpClass(cls):
    12.         cls.api_server_process = multiprocessing.Process(
    13.             target=api_server.app.run
    14.         )
    15.         cls.api_server_process.start()
    16.         time.sleep(0.1)

    17.     @classmethod
    18.     def tearDownClass(cls):
    19.         cls.api_server_process.terminate()
    复制代码
    这里采用的是多进程的方式(multiprocessing),所以我们的单元测试用例可以和API接口服务(Mock Server)同时运行。除了多进程的方式,我看到locust项目采用的是gevent.pywsgi.WSGIServer的方式,不过由于在gevent中要实现异步需要先monkey.patch_all(),感觉比较麻烦,而且还需要引入gevent这么一个第三方依赖库,所以还是决定采用multiprocessing的方式了。至于为什么没有选择多线程模型(threading),是因为线程至不支持显式终止的(terminate),要实现终止服务会比使用multiprocessing更为复杂。
    不过需要注意的是,由于启动Server存在一定的耗时,因此在启动完毕后必须要等待一段时间(本例中0.1秒就足够了),否则在执行单元测试用例时,调用的API接口可能还处于不可用状态。
    ApiServerUnittest基类就绪后,对于需要用到Mock Server的单元测试用例集,只需要继承ApiServerUnittest即可;其它的写法跟普通的单元测试完全一致。
    例如,下例包含一个单元测试用例,测试“创建一个用户,该用户之前不存在”的场景。
    1. # test/test_apiserver.py
    2. import requests
    3. from .base import ApiServerUnittest

    4. class TestApiServer(ApiServerUnittest):
    5.     def setUp(self):
    6.         super(TestApiServer, self).setUp()
    7.         self.host = "http://127.0.0.1:5000"
    8.         self.api_client = requests.Session()
    9.         self.clear_users()

    10.     def tearDown(self):
    11.         super(TestApiServer, self).tearDown()

    12.     def test_create_user_not_existed(self):
    13.         self.clear_users()

    14.         url = "%s/api/users/%d" % (self.host, 1000)
    15.         data = {
    16.             "name": "user1",
    17.             "password": "123456"
    18.         }
    19.         resp = self.api_client.post(url, json=data)

    20.         self.assertEqual(201, resp.status_code)
    21.         self.assertEqual(True, resp.json()["success"])
    复制代码
    为项目添加持续集成构建检查(Travis CI)当我们的项目具有单元测试之后,我们就可以为项目添加持续集成构建检查,从而在每次提交代码至GitHub时都运行测试,确保我们每次提交的代码都是可正常部署及运行的。
    要实现这个功能,推荐使用Travis CI提供的服务,该服务对于GitHub公有仓库是免费的。要完成配置,操作也很简单,基本上只有三步:
    • 在Travis CI使用GitHub账号授权登录;
    • 在Travis CI的个人profile页面开启需要持续集成的项目;
    • 在Github项目的根目录下添加.travis.yml配置文件。
    大多数情况下,.travis.yml配置文件可以很简单,例如ApiTestEngine的配置就只有如下几行:
    1. sudo: false
    2. language: python
    3. python:
    4.   - 2.7
    5.   - 3.3
    6.   - 3.4
    7.   - 3.5
    8.   - 3.6
    9. install:
    10.   - pip install -r requirements.txt
    11. script:
    12.   - python -m unittest discover
    复制代码
    具体含义不用解释也可以很容易看懂,其中install中包含我们项目的依赖库安装命令,script中包含执行构建测试的命令。
    配置完毕后,后续每次提交代码时,GitHub就会调用Travis CI实现构建检查;并且更赞的在于,构建检查可以同时在多个指定的Python版本环境中进行。
    下图是某次提交代码时的构建结果。




    另外,我们还可以在GitHub项目的README.md中添加一个Status Image,实时显示项目的构建状态,就像下图显示的样子。




    配置方式也是很简单,只需要先在Travis CI中获取到项目Status Image的URL地址,然后添加到README.md即可。




    写在后面通过本文中的工作,我们就对项目搭建好了测试框架,并实现了持续集成构建检查机制。从下一篇开始,我们就将开始逐步实现接口自动化测试框架的核心功能特性了。

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

    使用道具 举报

    该用户从未签到

    2#
    发表于 2017-6-22 09:55:23 | 只看该作者
    哇塞,一开始就奔着完整的开源项目而去呀
    回复 支持 反对

    使用道具 举报

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

    连续签到: 1 天

    [LV.1]测试小兵

    3#
     楼主| 发表于 2017-6-22 10:05:32 | 只看该作者
    发起狠来,自己都怕!
    回复 支持 反对

    使用道具 举报

    本版积分规则

    关闭

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

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

    GMT+8, 2024-11-11 05:46 , Processed in 0.066951 second(s), 22 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

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