在《接口自动化测试的最佳工程实践(ApiTestEngine)》一文中,我详细介绍了ApiTestEngine
诞生的背景,并对其核心特性进行了详尽的剖析。
接下来,我将在《ApiTestEngine 演进之路》系列文章中讲解ApiTestEngine
是如何从第一行代码开始,逐步实现接口自动化测试框架的核心功能特性的。
相信大家都有听说过TDD
(测试驱动开发
)这种开发模式,虽然网络上对该种开发模式存在异议,但我个人是非常推荐使用该种开发方式的。关于TDD
的优势,我就不在此赘述了,我就只说下自己受益最深的两个方面。
所以,ApiTestEngine
项目也将采用TDD
的开发模式。本篇文章就重点介绍下采用TDD
之前需要做的一些准备工作。
接口测试框架要运行起来,必然需要有可用的 API 接口服务。因此,在开始构建我们的接口测试框架之前,最好先搭建一套简单的 API 接口服务,也就是Mock Server
,然后我们在采用TDD
开发模式的时候,就可以随时随地将框架代码跑起来,开发效率也会大幅提升。
为什么不直接采用已有的业务系统 API 接口服务呢?
这是因为通常业务系统的接口比较复杂,并且耦合了许多业务逻辑,甚至还可能涉及到和其它业务系统的交互,搭建或维护一套测试环境的成本可能会非常高。另一方面,接口测试框架需要具有一定的通用性,其功能特性很难在一个特定的业务系统中找到所有合适的接口。就拿最简单的接口请求方法来说,测试框架需要支持GET/POST/HEAD/PUT/DELETE
方法,但是可能在我们已有的业务系统中只有GET/POST
接口。
自行搭建 API 接口服务的另一个好处在于,我们可以随时调整接口的实现方式,来满足接口测试框架特定的功能特性,从而使我们总是能将注意力集中在测试框架本身。比较好的做法是,先搭建最简单的接口服务,在此基础上将接口测试框架搭建起来,实现最基本的功能;后面在实现框架的高级功能特性时,我们再对该接口服务进行拓展升级,例如增加签名校验机制等,来适配测试框架的高级功能特性。
幸运的是,使用Python
搭建 API 接口服务十分简单,特别是在结合使用Flask
框架的情况下。
例如,我们想实现一套可以对用户账号进行增删改查(CRUD
)功能的接口服务,用户账号的存储结构大致如下:
users_dict = {
'uid1': {
'name': 'name1',
'password': 'pwd1'
},
'uid2': {
'name': 'name2',
'password': 'pwd2'
}
}
那么,新增(Create)和更新(Update)功能的接口就可以通过如下方式实现。
import json
from flask import Flask
from flask import request, make_response
app = Flask(__name__)
users_dict = {}
@app.route('/api/users/<int:uid>', methods=['POST'])
def create_user(uid):
user = request.get_json()
if uid not in users_dict:
result = {
'success': True,
'msg': "user created successfully."
}
status_code = 201
users_dict[uid] = user
else:
result = {
'success': False,
'msg': "user already existed."
}
status_code = 500
response = make_response(json.dumps(result), status_code)
response.headers["Content-Type"] = "application/json"
return response
@app.route('/api/users/<int:uid>', methods=['PUT'])
def update_user(uid):
user = users_dict.get(uid, {})
if user:
user = request.get_json()
success = True
status_code = 200
else:
success = False
status_code = 404
result = {
'success': success,
'data': user
}
response = make_response(json.dumps(result), status_code)
response.headers["Content-Type"] = "application/json"
return response
限于篇幅,其它类型的接口实现就不在此赘述,完整的接口实现可以参考项目源码。
接口服务就绪后,按照Flask
官方文档,可以通过如下方式进行启动:
$ export FLASK_APP=test/api_server.py
$ flask run
* Serving Flask app "test.api_server"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
启动后,我们就可以通过请求接口来调用已经实现的接口功能了。例如,先创建一个用户,然后查看所有用户的信息,在Python
终端中的调用方式如下:
$ python
Python 3.6.0 (default, Mar 24 2017, 16:58:25)
>>> import requests
>>> requests.post('http://127.0.0.1:5000/api/users/1000', json={'name': 'user1', 'password': '123456'})
<Response [201]>
>>> resp = requests.get('http://127.0.0.1:5000/api/users')
>>> resp.content
b'{"success": true, "count": 1, "items": [{"name": "user1", "password": "123456"}]}'
>>>
通过接口请求结果可见,接口服务运行正常。
API 接口服务(Mock Server
)已经有了,但是如果每次运行单元测试时都要先在外部手工启动 API 接口服务的话,做法实在是不够优雅。
推荐的做法是,制作一个ApiServerUnittest
基类,在其中添加setUpClass
类方法,用于启动 API 接口服务(Mock Server
);添加tearDownClass
类方法,用于停止 API 接口服务。由于setUpClass
会在单元测试用例集初始化的时候执行一次,所以可以保证单元测试用例在运行的时候 API 服务处于可用状态;而tearDownClass
会在单元测试用例集执行完毕后运行一次,停止 API 接口服务,从而避免对下一次启动产生影响。
# test/base.py
import multiprocessing
import time
import unittest
from . import api_server
class ApiServerUnittest(unittest.TestCase):
"""
Test case class that sets up an HTTP server which can be used within the tests
"""
@classmethod
def setUpClass(cls):
cls.api_server_process = multiprocessing.Process(
target=api_server.app.run
)
cls.api_server_process.start()
time.sleep(0.1)
@classmethod
def tearDownClass(cls):
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
即可;其它的写法跟普通的单元测试完全一致。
例如,下例包含一个单元测试用例,测试 “创建一个用户,该用户之前不存在” 的场景。
# test/test_apiserver.py
import requests
from .base import ApiServerUnittest
class TestApiServer(ApiServerUnittest):
def setUp(self):
super(TestApiServer, self).setUp()
self.host = "http://127.0.0.1:5000"
self.api_client = requests.Session()
self.clear_users()
def tearDown(self):
super(TestApiServer, self).tearDown()
def test_create_user_not_existed(self):
self.clear_users()
url = "%s/api/users/%d" % (self.host, 1000)
data = {
"name": "user1",
"password": "123456"
}
resp = self.api_client.post(url, json=data)
self.assertEqual(201, resp.status_code)
self.assertEqual(True, resp.json()["success"])
当我们的项目具有单元测试之后,我们就可以为项目添加持续集成构建检查,从而在每次提交代码至GitHub
时都运行测试,确保我们每次提交的代码都是可正常部署及运行的。
要实现这个功能,推荐使用Travis CI
提供的服务,该服务对于 GitHub 公有仓库是免费的。要完成配置,操作也很简单,基本上只有三步:
大多数情况下,.travis.yml
配置文件可以很简单,例如ApiTestEngine
的配置就只有如下几行:
sudo: false
language: python
python:
- 2.7
- 3.3
- 3.4
- 3.5
- 3.6
install:
- pip install -r requirements.txt
script:
- python -m unittest discover
具体含义不用解释也可以很容易看懂,其中install
中包含我们项目的依赖库安装命令,script
中包含执行构建测试的命令。
配置完毕后,后续每次提交代码时,GitHub
就会调用Travis CI
实现构建检查;并且更赞的在于,构建检查可以同时在多个指定的Python
版本环境中进行。
下图是某次提交代码时的构建结果。
另外,我们还可以在GitHub
项目的README.md
中添加一个Status Image
,实时显示项目的构建状态,就像下图显示的样子。
配置方式也是很简单,只需要先在Travis CI
中获取到项目Status Image
的 URL 地址,然后添加到README.md
即可。
通过本文中的工作,我们就对项目搭建好了测试框架,并实现了持续集成构建检查机制。从下一篇开始,我们就将开始逐步实现接口自动化测试框架的核心功能特性了。