[关闭]
@coolfish 2017-06-08T02:17:18.000000Z 字数 10042 阅读 10716

简单实现接口自动化测试(基于python)


一、简介

本文从一个简单的登录接口测试入手,一步步调整优化接口调用姿势,然后简单讨论了一下接口测试框架的要点,最后介绍了一下我们目前正在使用的接口测试框架pithy。期望读者可以通过本文对接口自动化测试有一个大致的了解。

二、引言

为什么要做接口自动化测试?

在当前互联网产品迭代频繁的背景下,回归测试的时间越来越少,很难在每个迭代都对所有功能做完整回归。但接口自动化测试因其实现简单、维护成本低,容易提高覆盖率等特点,越来越受重视。

为什么要自己写框架呢?

使用requets + unittest很容易实现接口自动化测试,而且requests的api已经非常人性化,非常简单,但通过封装以后(特别是针对公司内特定接口),再加上对一些常用工具的封装,可以进一步提高业务脚本编写效率。


三、环境准备

确保本机已安装python2.7以上版本,然后安装如下库

  1. pip install flask
  2. pip install requests

后面我们会使用flask写一个用来测试的接口,使用requests去测试


四、测试接口准备

下面使用flask实现两个http接口,一个登录,另外一个查询详情,但需要登录后才可以,新建一个demo.py文件(注意,不要使用windows记事本),把下面代码copy进去,然后保存、关闭

接口代码

  1. #!/usr/bin/python
  2. # coding=utf-8
  3. from flask import Flask, request, session, jsonify
  4. USERNAME = 'admin'
  5. PASSWORD = '123456'
  6. app = Flask(__name__)
  7. app.secret_key = 'pithy'
  8. @app.route('/login', methods=['GET', 'POST'])
  9. def login():
  10. error = None
  11. if request.method == 'POST':
  12. if request.form['username'] != USERNAME:
  13. error = 'Invalid username'
  14. elif request.form['password'] != PASSWORD:
  15. error = 'Invalid password'
  16. else:
  17. session['logged_in'] = True
  18. return jsonify({'code': 200, 'msg': 'success'})
  19. return jsonify({'code': 401, 'msg': error}), 401
  20. @app.route('/info', methods=['get'])
  21. def info():
  22. if not session.get('logged_in'):
  23. return jsonify({'code': 401, 'msg': 'please login !!'})
  24. return jsonify({'code': 200, 'msg': 'success', 'data': 'info'})
  25. if __name__ == '__main__':
  26. app.run(debug=True)

最后执行如下命令

  1. python demo.py

响应如下

  1. * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
  2. * Restarting with stat

大家可以看到服务已经起起来了

接口信息

登录接口

详情接口

五、编写接口测试

测试思路

脚本实现

  1. #!/usr/bin/python
  2. # coding=utf-8
  3. import requests
  4. import unittest
  5. class TestLogin(unittest.TestCase):
  6. @classmethod
  7. def setUpClass(cls):
  8. cls.login_url = 'http://127.0.0.1:5000/login'
  9. cls.info_url = 'http://127.0.0.1:5000/info'
  10. cls.username = 'admin'
  11. cls.password = '123456'
  12. def test_login(self):
  13. """
  14. 测试登录
  15. """
  16. data = {
  17. 'username': self.username,
  18. 'password': self.password
  19. }
  20. response = requests.post(self.login_url, data=data).json()
  21. assert response['code'] == 200
  22. assert response['msg'] == 'success'
  23. def test_info(self):
  24. """
  25. 测试info接口
  26. """
  27. data = {
  28. 'username': self.username,
  29. 'password': self.password
  30. }
  31. response_cookies = requests.post(self.login_url, data=data).cookies
  32. session = response_cookies.get('session')
  33. assert session
  34. info_cookies = {
  35. 'session': session
  36. }
  37. response = requests.get(self.info_url, cookies=info_cookies).json()
  38. assert response['code'] == 200
  39. assert response['msg'] == 'success'
  40. assert response['data'] == 'info'

六、优化

封装接口调用

写完这个测试登录脚本,你或许会发现,在整个项目的测试过程,登录可能不止用到一次,如果每次都这么写,会不会太冗余了? 对,确实太冗余了,下面做一下简单的封装,把登录接口的调用封装到一个方法里,把调用参数暴漏出来,示例脚本如下:

  1. #!/usr/bin/python
  2. # coding=utf-8
  3. import requests
  4. import unittest
  5. try:
  6. from urlparse import urljoin
  7. except ImportError:
  8. from urllib.parse import urljoin
  9. class DemoApi(object):
  10. def __init__(self, base_url):
  11. self.base_url = base_url
  12. def login(self, username, password):
  13. """
  14. 登录接口
  15. :param username: 用户名
  16. :param password: 密码
  17. """
  18. url = urljoin(self.base_url, 'login')
  19. data = {
  20. 'username': username,
  21. 'password': password
  22. }
  23. return requests.post(url, data=data).json()
  24. def get_cookies(self, username, password):
  25. """
  26. 获取登录cookies
  27. """
  28. url = urljoin(self.base_url, 'login')
  29. data = {
  30. 'username': username,
  31. 'password': password
  32. }
  33. return requests.post(url, data=data).cookies
  34. def info(self, cookies):
  35. """
  36. 详情接口
  37. """
  38. url = urljoin(self.base_url, 'info')
  39. return requests.get(url, cookies=cookies).json()
  40. class TestLogin(unittest.TestCase):
  41. @classmethod
  42. def setUpClass(cls):
  43. cls.base_url = 'http://127.0.0.1:5000'
  44. cls.username = 'admin'
  45. cls.password = '123456'
  46. cls.app = DemoApi(cls.base_url)
  47. def test_login(self):
  48. """
  49. 测试登录
  50. """
  51. response = self.app.login(self.username, self.password)
  52. assert response['code'] == 200
  53. assert response['msg'] == 'success'
  54. def test_info(self):
  55. """
  56. 测试获取详情信息
  57. """
  58. cookies = self.app.get_cookies(self.username, self.password)
  59. response = self.app.info(cookies)
  60. assert response['code'] == 200
  61. assert response['msg'] == 'success'
  62. assert response['data'] == 'info'

OK,在这一个版本中,我们不但在把登录接口的调用封装成了一个实例方法,实现了复用,而且还把host(self.base_url)提取了出来,但问题又来了,登录之后,登录接口的http响应会把session以 cookie的形式set到客户端,之后的接口都会使用此session去请求,还有,就是在接口调用过程中,希望可以把日志打印出来,以便调试或者出错时查看。
好吧,我们再来改一版。

保持cookies&增加log信息

使用requests库里的同一个Session对象(它也会在同一个Session 实例发出的所有请求之间保持 cookie),即可解决上面的问题,示例代码如下:

  1. #!/usr/bin/python
  2. # coding=utf-8
  3. import unittest
  4. from pprint import pprint
  5. from requests.sessions import Session
  6. try:
  7. from urlparse import urljoin
  8. except ImportError:
  9. from urllib.parse import urljoin
  10. class DemoApi(object):
  11. def __init__(self, base_url):
  12. self.base_url = base_url
  13. # 创建session实例
  14. self.session = Session()
  15. def login(self, username, password):
  16. """
  17. 登录接口
  18. :param username: 用户名
  19. :param password: 密码
  20. """
  21. url = urljoin(self.base_url, 'login')
  22. data = {
  23. 'username': username,
  24. 'password': password
  25. }
  26. response = self.session.post(url, data=data).json()
  27. print('\n*****************************************')
  28. print(u'\n1、请求url: \n%s' % url)
  29. print(u'\n2、请求头信息:')
  30. pprint(self.session.headers)
  31. print(u'\n3、请求参数:')
  32. pprint(data)
  33. print(u'\n4、响应:')
  34. pprint(response)
  35. return response
  36. def info(self):
  37. """
  38. 详情接口
  39. """
  40. url = urljoin(self.base_url, 'info')
  41. response = self.session.get(url).json()
  42. print('\n*****************************************')
  43. print(u'\n1、请求url: \n%s' % url)
  44. print(u'\n2、请求头信息:')
  45. pprint(self.session.headers)
  46. print(u'\n3、请求cookies:')
  47. pprint(dict(self.session.cookies))
  48. print(u'\n4、响应:')
  49. pprint(response)
  50. return response
  51. class TestLogin(unittest.TestCase):
  52. @classmethod
  53. def setUpClass(cls):
  54. cls.base_url = 'http://127.0.0.1:5000'
  55. cls.username = 'admin'
  56. cls.password = '123456'
  57. cls.app = DemoApi(cls.base_url)
  58. def test_login(self):
  59. """
  60. 测试登录
  61. """
  62. response = self.app.login(self.username, self.password)
  63. assert response['code'] == 200
  64. assert response['msg'] == 'success'
  65. def test_info(self):
  66. """
  67. 测试获取详情信息
  68. """
  69. self.app.login(self.username, self.password)
  70. response = self.app.info()
  71. assert response['code'] == 200
  72. assert response['msg'] == 'success'
  73. assert response['data'] == 'info'

大功告成,我们把多个相关接口调用封装到一个类中,使用同一个requests Session实例来保持cookies,并且在调用过程中打印出了日志,我们所有目标都实现了,但再看下脚本,又会感觉不太舒服,在每个方法里,都要写一遍print 1、2、3... 要拼url、还要很多细节等等,但其实我们真正需要做的只是拼出关键的参数(url参数、body参数或者传入headers信息),可不可以只需定义必须的信息,然后把其它共性的东西都封装起来呢,统一放到一个地方去管理?

封装重复操作

来,我们再整理一下我们的需求:

我们先看一下实现后,脚本可能是什么样:

  1. class DemoApi(object):
  2. def __init__(self, base_url):
  3. self.base_url = base_url
  4. @request(url='login', method='post')
  5. def login(self, username, password):
  6. """
  7. 登录接口
  8. """
  9. data = {
  10. 'username': username,
  11. 'password': password
  12. }
  13. return {'data': data}
  14. @request(url='info', method='get')
  15. def info(self):
  16. """
  17. 详情接口
  18. """
  19. pass

调用登录接口的日志

  1. ******************************************************
  2. 1、接口描述
  3. 登录接口
  4. 2、请求url
  5. http://127.0.0.1:5000/login
  6. 3、请求方法
  7. post
  8. 4、请求headers
  9. {
  10. "Accept": "*/*",
  11. "Accept-Encoding": "gzip, deflate",
  12. "Connection": "keep-alive",
  13. "User-Agent": "python-requests/2.7.0 CPython/2.7.10 Darwin/16.4.0"
  14. }
  15. 5body参数
  16. {
  17. "password": "123456",
  18. "username": "admin"
  19. }
  20. 6、响应结果
  21. {
  22. "code": 200,
  23. "msg": "success"
  24. }

在这里,我们使用python的装饰器功能,把公共特性封装到装饰器中去实现。现在感觉好多了,没什么多余的东西了,我们可以专注于关键参数的构造,剩下的就是如何去实现这个装饰器了,我们先理一下思路:

  • 获取装饰器参数
  • 获取函数/方法参数
  • 把装饰器和函数定义的参数合并
  • 拼接url
  • 处理requests session,有则使用,无则新生成一个
  • 组装所有参数,发送http请求并打印日志

因篇幅限制,源码不再列出,有兴趣的同学可以查看已经实现的源代码

源代码查看地址:
https://github.com/yuyu1987/pithy-test/blob/master/pithy/api.py


七、扩展

http接口请求的姿势我们定义好了,我们还可以做些什么呢?

  • 非HTTP协议接口
  • 测试用例编写
  • 配置文件管理
  • 测试数据管理
  • 工具类编写
  • 测试报告生成
  • 持续集成
  • 等等等等

需要做的还是挺多的,要做什么不要做什么,或者先做哪个,我觉得可以根据以下几点去判断:

下面就几项主要的点进行一下说明,限于篇幅,不再展开了

测试报告

这个应该是大家最关心的了,毕竟这是测试工作的产出;
目前python的主流单元测试框均有report插件,因此不建议自己再编写,除非有特殊需求的。

持续集成

持续集成推荐使用Jenkins,运行环境、定时任务、触发运行、邮件发送等一系列功能均可以在Jenkins上实现。

测试用例编写

推荐遵守如下规则:

测试工具类

这个可以根据项目情况去做,力求简化一些类库的使用,数据库访问、日期时间、序列化与反序列化等数据处理,或者封装一些常用操作,如随机生成订单号等等,以提高脚本编写效率。

测试数据管理

常见的方式有写在代码里、写在配置文件里(xml、yaml、json、.py、excel等)、写在数据库里等,该处没有什么好推荐的,建议根据个人喜好,怎么方便怎么来就可以。


八、pithy测试框架介绍

pithy意为简洁有力的,意在简化自动化接口测试,提高测试效率

目前实现的功能如下:

  • 一键生成测试项目
  • http client封装
  • thrift接口封装
  • 简化配置文件使用
  • 优化JSON、日期等工具使用

编写测试用例推荐使用pytest,pytest提供了很多测试工具以及插件,可以满足大部分测试需求。

安装

  1. pip install pithy-test
  2. pip install pytest

使用

一键生成测试项目

  1. >>> pithy-cli init
  2. 请选择项目类型,输入api或者app: api
  3. 请输入项目名称,如pithy-api-test: pithy-api-test
  4. 开始创建pithy-api-test项目
  5. 开始渲染...
  6. 生成 api/.gitignore [√]
  7. 生成 api/apis/__init__.py [√]
  8. 生成 api/apis/pithy_api.py [√]
  9. 生成 api/cfg.yaml [√]
  10. 生成 api/db/__init__.py [√]
  11. 生成 api/db/pithy_db.py [√]
  12. 生成 api/README.MD [√]
  13. 生成 api/requirements.txt [√]
  14. 生成 api/test_suites/__init__.py [√]
  15. 生成 api/test_suites/test_login.py [√]
  16. 生成 api/utils/__init__.py [√]
  17. 生成成功,请使用编辑器打开该项目

生成项目树

  1. >>> tree pithy-api-test
  2. pithy-api-test
  3. ├── README.MD
  4. ├── apis
  5.    ├── __init__.py
  6.    └── pithy_api.py
  7. ├── cfg.yaml
  8. ├── db
  9.    ├── __init__.py
  10.    └── pithy_db.py
  11. ├── requirements.txt
  12. ├── test_suites
  13.    ├── __init__.py
  14.    └── test_login.py
  15. └── utils
  16. └── __init__.py
  17. 4 directories, 10 files

调用HTTP登录接口示例

  1. from pithy import request
  2. @request(url='http://httpbin.org/post', method='post')
  3. def post(self, key1='value1'):
  4. """
  5. post method
  6. """
  7. data = {
  8. 'key1': key1
  9. }
  10. return dict(data=data)
  11. # 使用
  12. response = post('test').to_json() # 解析json字符,输出为字典
  13. response = post('test').json # 解析json字符,输出为字典
  14. response = post('test').to_content() # 输出为字符串
  15. response = post('test').content # 输出为字符串
  16. response = post('test').get_cookie() # 输出cookie对象
  17. response = post('test').cookie # 输出cookie对象
  18. # 结果取值, 假设此处response = {'a': 1, 'b': { 'c': [1, 2, 3, 4]}}
  19. response = post('13111111111', '123abc').json
  20. print response.b.c # 通过点号取值,结果为[1, 2, 3, 4]
  21. print response('$.a') # 通过object path取值,结果为1
  22. for i in response('$..c[@>3]'): # 通过object path取值,结果为选中c字典里大于3的元素
  23. print i

优化JSON、字典使用

  1. # 1、操作JSON的KEY
  2. from pithy import JSONProcessor
  3. dict_data = {'a': 1, 'b': {'a': [1, 2, 3, 4]}}
  4. json_data = json.dumps(dict_data)
  5. result = JSONProcessor(json_data)
  6. print result.a # 结果:1
  7. print result.b.a # 结果:[1, 2, 3, 4]
  8. # 2、操作字典的KEY
  9. dict_data = {'a': 1, 'b': {'a': [1, 2, 3, 4]}}
  10. result = JSONProcessor(dict_data)
  11. print result.a # 1
  12. print result.b.a # [1, 2, 3, 4]
  13. # 3、object path取值
  14. raw_dict = {
  15. 'key1':{
  16. 'key2':{
  17. 'key3': [1, 2, 3, 4, 5, 6, 7, 8]
  18. }
  19. }
  20. }
  21. jp = JSONProcessor(raw_dict)
  22. for i in jp('$..key3[@>3]'):
  23. print i
  24. # 4、其它用法
  25. dict_1 = {'a': 'a'}
  26. json_1 = '{"b": "b"}'
  27. jp = JSONProcessor(dict_1, json_1, c='c')
  28. print(jp)

更多使用方法

点击查看


九、总结

在本文中,我们以提高脚本开发效率为前提,一步一步打造了一个简易的测试框架,但因水平所限,并未涉及测试数据初始化清理、测试中如何MOCK等话题,前路依然任重而道远,希望给大家一个启发,不足之处还望多多指点,非常感谢。


作者简介

孙彦辉,饿了么软件测试工程师,主要负责大物流蜂鸟商家版的测试工作。


参考:
[1] requests:http://www.python-requests.org/en/master/
[2] thriftpy:http://thriftpy.readthedocs.io/en/latest/
[3] objectpath:http://objectpath.org/
[4] pytest:https://docs.pytest.org

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注