python+unittest接口测试框架demo

 目录

接口测试思路

python+unittest接口测试框架

 config.py  #配置文件

 logger.py  #日志模块

comm_fun.py # 公共函数

test_AddUserInfo.py  #测试模块,接口AddUserInfo

debug.py  #调试测试 执行文件

TestRunner.py  #执行文件

输出logs

输出报告


接口测试思路

小菜鸡接口测试一般分两个步骤:

第一步,先写文字版的用例;

第二步,实现成脚本用例

(中间当然还有用例评审、修正等环节)

 

接口测试用例一般从三个维度考虑:

(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

输出报告

 

没搞那么多花里胡哨的功能,老老实实一条条写用例,简简单单的测试接口框架。

还有很多可优化的点。共勉。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值