目录
test_AddUserInfo.py #测试模块,接口AddUserInfo
接口测试思路
小菜鸡接口测试一般分两个步骤:
第一步,先写文字版的用例;
第二步,实现成脚本用例
(中间当然还有用例评审、修正等环节)
接口测试用例一般从三个维度考虑:
(1)最重要的放前面,接口最基本、最主要正常功能用例
(2)接口参数检查。根据接口定义,检测参数的
- 缺省值。
- 参数类型(合法性)。
- 参数长度限制。
- 业务约束(合理性)。譬如有的参数传参是关联的别的表的字段,如果值不存在,即不合理。
(3)其他的场景。这个“其他”就比较广泛了。
- 一般就是从业务功能流程去考虑用例(划重点!!非常重要);
- 还可以从接口性能方面去考虑(性能方面可能涉及性能测试,小菜鸡涉及的比较少);
- 有权限控制的,特别要考虑权限的问题(这个很重要,也容易忽略的点);
- 特别还要考虑兼容性,接口改动部分,是否兼容旧功能。
- 考虑安全性
- 接口设计的合理性(这一点需要尽早确认,有能力者需求评审、用例评审阶段就需要多了解开发的设计思路。)
接口脚本用例实现思路
测试脚本文件小菜鸡也给它分成4个部分:
(1)前置条件。
例如,查询接口,前置条件需要先创建数据,再调用查询。
一般放在setUp或者setUpClass或者写在request请求之前。
(2)接口调用。
接口主要的执行体,就是request部分(requests.post(url=url, params=params)
(3)结果断言。划重点。
一般写接口,需要和开发确认好,接口调用会写入到那些数据表。断言一般需要关注以下:
- 查询数据库判断是否插入或改动数据,
- 判断插入或者改动内容是否与传入参数一致,
- 判断接口返回的code是否正确,
- 如果接口有返回信息也需要判断返回的信息是否与数据库的一致。
(4)数据清理。
用例测试完,需要清理数据。unittest 是随机执行的,避免影响其他用例。
一般写在tearDown或者tearDownClass里面。
小菜鸡喜欢写在setUpClass里面去清理数据,有时候用例跑失败,数据不会被清理掉,方便回溯问题。
个人设置喜好,怎么方便怎么来。
python+unittest接口测试框架
config.py #配置文件
logger.py #日志模块
TestRunner.py #执行文件
debug.py #调试测试 执行文件
comm_fun.py # 公共函数
testCases #测试用例集
-UserApiTest #用户模块api集,系统功能模块多的时候可以分模块存放用例
-test_AddUserInfo.py #测试模块,接口AddUserInfo
testReport #测试报告存放
logs #存放输出 日志
config.py #配置文件
#config.py
class ConstError(Exception): pass
class _const(object):
def __setattr__(self, k, v):
if k in self.__dict__:
raise ConstError
else:
self.__dict__[k] = v
COMMCFG = _const()
COMMCFG.port = '8000'
COMMCFG.url = 'http://127.0.0.1:'+COMMCFG.port
COMMCFG.db_host = ''
COMMCFG.db_user = ''
COMMCFG.db_passwd = ''
COMMCFG.db_database = ''
logger.py #日志模块
#logger.py
import logging
import time
import os
logs_dir = "./logs"
class Logger(object):
def __init__(self,logger):
self.logger = logging.getLogger(logger)
self.logger.setLevel(logging.DEBUG)
rq = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
log_name = os.path.join(logs_dir, logger + rq + '.log')
fh = logging.FileHandler(log_name)
fh.setLevel(logging.INFO)
# ch = logging.StreamHandler()
# ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
# ch.setFormatter(formatter)
self.logger.addHandler(fh)
# self.logger.addHandler(ch)
def getLog(self):
return self.logger
comm_fun.py # 公共函数
#comm_fun.py
import mysql.connector
import config
from logger import Logger
import time
logger = Logger(logger='comm_fun').getLog()
cfg = config.COMMCFG
def query_once(sql, method):
conn = mysql.connector.connect(
host=cfg.db_host,
user=cfg.db_user,
password=cfg.db_passwd,
database=cfg.db_database,
charset='utf8',
use_pure=True
)
logger.info('connected to mysql: host=%s, user=%s, database=%s' % (cfg.db_host, cfg.db_user, cfg.db_database))
cursor = conn.cursor()
logger.info(sql)
cursor.execute(sql)
if method == 'insert':
conn.commit()
records = None
if method != 'insert':
records = cursor.fetchall()
conn.close()
return records
def select(tablename, *rows, **conditions):
# tablename是查询表,rows是查询列,conditions是查询条件
conn = mysql.connector.connect(host=cfg.db_host, user=cfg.db_user, password=cfg.db_passwd, database=cfg.db_database,
charset='utf8', use_pure=True)
logger.info('connected to mysql:host=%s user=%s database=%s' % (cfg.db_host, cfg.db_user, cfg.db_database))
cursor = conn.cursor()
sql = 'select * from %s' % tablename
if rows:
sql = 'select %s from %s' % (','.join(rows), tablename)
if conditions:
sql = sql + ' where '
for k in conditions:
sql = sql + '%s=\'%s\' and ' % (k, conditions[k]) if isinstance(conditions[k],
(str,)) else sql + '%s=%s and ' % (
k, conditions[k])
sql = sql[:-5]
logger.info(sql)
cursor.execute(sql)
records = cursor.fetchall()
logger.info(records)
conn.close()
return records
def clean_all_table(table_tuple):
# table_tuple是要清除的表列表
for table in table_tuple:
delete_table(table)
def genSignature(params):
'''处理签名'''
return params
test_AddUserInfo.py #测试模块,接口AddUserInfo
#test_AddUserInfo.py
import requests
import unittest
import comm_fun
import config
import random
import time
import json
import os
from logger import Logger
logger = Logger(logger='TestAddUserInfo').getLog()
cfg = config.COMMCFG
tables = ('demo_app_userinfo',)
# #接口AddUserInfo POST
#参数 user 用户名
# user_type 用户类型 表demo_app_usertype的id字段
# email 邮箱
# pwd = 密码
#user 、 user_type 通过url传参, email、pwd放在body里(接口是为了搞接口框架,才这样设计的,)
class TestAddUserInfo(unittest.TestCase):
@classmethod
def setUpClass(cls):
# logger.info('setUpClass...clean tables:' + ','.join([i for i in tables]))
# tools.clean_all_table(tables)
pass
def setUp(self):
# 在日志里面打印执行的是哪个case
logger.info("setUp...start")
logger.info("testCase %s -> %s" % (self.__dict__["_testMethodName"], self.__dict__["_testMethodDoc"]))
# 每个测试用例的前置条件,
# 查询表 demo_app_usertype,获取user_type
self.user_type = comm_fun.select('demo_app_usertype', '*', **{'caption': "zhouerduo"})[0][0]
def tearDown(self):
logger.info("teardowm...end")
# logger.info('teardowm...clean tables:' + ','.join([i for i in tables]))
# tools.clean_all_table(tables)
@classmethod
def tearDownClass(cls):
# 测试完,清理数据表,还原成测试前的状态,用例是随机执行的,清理的目的是为了不影响其他用例测试,.慎重!按需清理
logger.info('teardowmClass...clean tables:' + ','.join([i for i in tables]))
# tools.clean_all_table(tables)
# pass
def test_add_userinfo(self):
'''参数正确'''
user = "zhouerduo"
user_type = self.user_type
email ="123@qq.com"
pwd = "123456"
params = [('user', user), ('user_type', user_type),]
body = {
"email":email,
"pwd":pwd
}
comm_fun.genSignature(params)
url = cfg.url+'/api'+'/AddUserInfo'
logger.info('api/AddUserInfo: url[%s] prams[%s] ' % (url, params))
res = requests.post(url=url, params=params,data=body)
logger.info('api/AddUserInfo:response %s'%(res.text))
rsp_body = json.loads(res.text)
# rsp_body [{"message": "success", "resCode": 0}]
self.assertEqual(rsp_body['resCode'], 0)
records = comm_fun.select('demo_app_userinfo', 'user', 'user_type_id', 'email', 'pwd', **{'user':user, 'user_type_id':user_type})
# records [('zhouerduo', 2,'123@qq.com', '123456')]
self.assertEqual(records[0][0],user)
self.assertEqual(records[0][1],user_type)
self.assertEqual(records[0][2],email)
self.assertEqual(records[0][3],pwd)
def test_2(self):
'''user_type is not exits'''
user = "zhouerduo"
user_type = "23445" #user_type 不存在 demo_app_usertype中
email = "123@qq.com"
pwd = "123456"
params = [('user', user), ('user_type', user_type), ]
body = {
"email": email,
"pwd": pwd
}
comm_fun.genSignature(params)
url = cfg.url + '/api' + '/AddUserInfo'
logger.info('api/AddUserInfo: url[%s] prams[%s] ' % (url, params))
res = requests.post(url=url, params=params, data=body)
logger.info('api/AddUserInfo:response %s' % (res.text))
rsp_body = json.loads(res.text)
# rsp_body [{"message": "..", "resCode": -1}]
self.assertNotEqual(rsp_body['resCode'], 0)
records = comm_fun.select('demo_app_userinfo', 'user', 'user_type_id', 'email', 'pwd',
**{'user': user, 'user_type_id': user_type})
# records []
self.assertEqual(records, [])
def test_3(self):
'''no user '''
user = "zhouerduo"
user_type = self.user_type
email = "123@qq.com"
pwd = "123456"
records_before = comm_fun.select('demo_app_userinfo', '*')
params = [
# ('user', user), 不传user
('user_type', user_type),
]
body = {
"email": email,
"pwd": pwd
}
comm_fun.genSignature(params)
url = cfg.url + '/api' + '/AddUserInfo'
logger.info('api/AddUserInfo: url[%s] prams[%s] ' % (url, params))
res = requests.post(url=url, params=params, data=body)
logger.info('api/AddUserInfo:response %s' % (res.text))
rsp_body = json.loads(res.text)
# rsp_body [{"message": "..", "resCode": -1}]
self.assertNotEqual(rsp_body['resCode'], 0)
records_after = comm_fun.select('demo_app_userinfo', '*')
self.assertEqual(len(records_after), len(records_before))
debug.py #调试测试 执行文件
#debug.py
import unittest
from HTMLTestRunner import HTMLTestRunner
from testCases.UserApiTest.test_AddUserInfo import TestAddUserInfo
# from testCases.UserApiTest.test_GetUserInfo import TestGetUserInfo
if __name__ == '__main__':
############################################跑指定某条case#################################################
suite = unittest.TestSuite()
suite.addTest(TestAddUserInfo('test_add_userinfo'))
############################################跑指定某个模块#################################################
# suite.addTests(unittest.TestLoader().loadTestsFromName('testCases.UserApiTest.test_AddUserInfo.TestAddUserInfo'))
# # # ## run #################
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
TestRunner.py #执行文件
import unittest
from HTMLTestRunner import HTMLTestRunner
import time
if __name__ == '__main__':
suite_userinfo = unittest.TestLoader().discover('testCases/UserApiTest')
suite = unittest.TestSuite((suite_userinfo, ))
test_time = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
report_file = './testReport/' + test_time + 'XXXApiTestReport.html'
with open(report_file, 'xb') as fp:
runner = HTMLTestRunner(fp, title='XXXApiTestReport', description='apitest',
verbosity=2)
runner.run(suite)
输出logs
2020-12-28 13:40:59,406 - TestAddUserInfo - INFO - setUp...start
2020-12-28 13:40:59,406 - TestAddUserInfo - INFO - testCase test_2 -> user_type is not exits
2020-12-28 13:40:59,679 - comm_fun - INFO - connected to mysql:host=XXX.XXX.XXX.XXX user=root database=DataBaseName
2020-12-28 13:40:59,693 - comm_fun - INFO - select * from demo_app_usertype where caption='zhouerduo'
2020-12-28 13:40:59,701 - comm_fun - INFO - [(2, 'zhouerduo'), (3, 'zhouerduo')]
2020-12-28 13:40:59,702 - TestAddUserInfo - INFO - api/AddUserInfo: url[http://127.0.0.1:8000/api/AddUserInfo] prams[[('user', 'zhouerduo'), ('user_type', '23445')]]
2020-12-28 13:40:59,808 - TestAddUserInfo - INFO - api/AddUserInfo:response {"message": "(1452, 'Cannot add or update a child row: a foreign key constraint fails (`DataBaseName`.`demo_app_userinfo`, CONSTRAINT `demo_app_userinfo_user_type_id_e7f6d111_fk_demo_app_usertype_id` FOREIGN KEY (`user_type_id`) REFERENCES `demo_app_usertype` (`id`))')", "resCode": -1}
2020-12-28 13:40:59,877 - comm_fun - INFO - connected to mysql:host=XXX.XXX.XXX.XXX user=root database=DataBaseName
2020-12-28 13:40:59,892 - comm_fun - INFO - select user,user_type_id,email,pwd from demo_app_userinfo where user='zhouerduo' and user_type_id='23445'
2020-12-28 13:40:59,901 - comm_fun - INFO - []
2020-12-28 13:40:59,902 - TestAddUserInfo - INFO - teardowm...end
输出报告
没搞那么多花里胡哨的功能,老老实实一条条写用例,简简单单的测试接口框架。
还有很多可优化的点。共勉。