接口测试 [分享] 自己写的接口测试平台-----IAPTest

雷子 · 2017年11月24日 · 最后由 雷子 回复于 2022年01月17日 · 3619 次阅读
本帖已被设为精华帖!

背景: 1.平时测试接口,总是现写代码,对测试用例的管理,以及测试报告的管理持久化做的不够,
2.工作中移动端开发和后端开发总是不能并行进行,需要一个 mock 的依赖来让他们并行开发。
3.同时让自己锻炼去开发测试平台,掌握 flask 开发程序,提高自己的业务水平。

整体思路: 1.利用 flask+bootstrap 来进行 web 界面开发,对接口,接口测试用例,定时任务,测试报告的持续集成。
2.IAPTest 支持接口用例管理,接口多用例测试,支持定时测试任务,测试报告持久化
3.目前 mock 服务支持单一 path,定时任务可以开启暂停多用例执行,定时任务执行后自动发送测试报告,多用例的单次执行,单接口的调试功能。对测试环境的管理
下面来看下最后的效果图,以及附上 github 开源地址。

测试环境管理界面:

定时任务界面:

mock 界面

测试报告界面

用例管理界面

接口管理界面

核心代码分享区:

定时任务对应视图开发

class StartTaskView(MethodView):#开始定时任务
    @login_required
    def get(self,id):
        task=Task.query.filter_by(id=id).first()
        if len(task.interface.all())<=1:
            flash('定时任务执行过程的测试用例为多用例,请你谅解')
            return  redirect(url_for('timingtask'))
        try:
            scheduler.add_job(func=addtask, id=str(id), args=str(id),trigger=eval(task.taskstart),replace_existing=True)
            task.yunxing_status='启动'
            db.session.commit()
            flash(u'定时任务启动成功!')
            return  redirect(url_for('timingtask'))
        except Exception as e:
            flash(u'定时任务启动失败!请检查任务的各项内容各项内容是否正常')
            return redirect(url_for('timingtask'))
class ZantingtaskView(MethodView):#暂停定时任务
    @login_required
    def get(self,id):
        task = Task.query.filter_by(id=id).first()
        try:
            scheduler.pause_job(str(id))
            task.yunxing_status = '暂停'
            db.session.commit()
            flash(u'定时任务暂停成功!')
            return redirect(url_for('timingtask'))
        except:
            task.yunxing_status = '创建'
            db.session.commit()
            flash(u'定时任务暂停失败!已经为您初始化')
            return redirect(url_for('timingtask'))
class HuifutaskView(MethodView):#回复定时任务
    @login_required
    def get(self,id):
        task = Task.query.filter_by(id=id).first()
        try:
            scheduler.resume_job(str(id))
            task.yunxing_status='启动'
            db.session.commit()
            flash(u'定时任务恢复成功!')
            return redirect(url_for('timingtask'))
        except:
            task.yunxing_status = '创建'
            db.session.commit()
            flash(u'定时任务恢复失败!已经为您初始化')
            return redirect(url_for('timingtask'))
class YichuTaskView(MethodView):#移除定时任务
    @login_required
    def get(self,id):
        task = Task.query.filter_by(id=id).first()
        try:
            scheduler.delete_job(str(id))
            task.yunxing_status='关闭'
            db.session.commit()
            flash(u'定时任务移除成功!')
            return redirect(url_for('timingtask'))
        except:
            task.yunxing_status = '创建'
            db.session.commit()
            flash(u'定时任务移除失败!已经为您初始化')
            return redirect(url_for('timingtask'))

定时任务所执行的 func 代码

def addtask(id):#定时任务执行的时候所用的函数
    in_id=int(id)
    task=Task.query.filter_by(id=in_id).first()
    starttime = datetime.datetime.now()
    star = time.time()
    day = time.strftime("%Y%m%d%H%M", time.localtime(time.time()))
    basedir = os.path.abspath(os.path.dirname(__file__))
    file_dir = os.path.join(basedir, 'upload')
    file = os.path.join(file_dir, (day + '.log'))
    if os.path.exists(file) is False:
        os.system('touch %s' % file)
    filepath = os.path.join(file_dir, (day + '.html'))
    if os.path.exists(filepath) is False:
        os.system(r'touch %s' % filepath)
    projecct_list = []
    model_list = []
    Interface_name_list = []
    Interface_url_list = []
    Interface_meth_list = []
    Interface_pase_list = []
    Interface_assert_list = []
    Interface_headers_list = []
    id_list = []
    for task_yongli in task.interface.all():
        id_list.append(task_yongli.id)
        projecct_list.append(task_yongli.projects)
        model_list.append(task_yongli.models)
        Interface_url_list.append(task_yongli.Interface_url)
        Interface_name_list.append(task_yongli.Interface_name)
        Interface_meth_list.append(task_yongli.Interface_meth)
        Interface_pase_list.append(task_yongli.Interface_pase)
        Interface_assert_list.append(task_yongli.Interface_assert)
        Interface_headers_list.append(task_yongli.Interface_headers)
    apitest = ApiTestCase(Interface_url_list, Interface_meth_list, Interface_pase_list, Interface_assert_list, file,
                          Interface_headers_list)
    result_toal, result_pass, result_fail, relusts, bask_list = apitest.testapi()
    endtime = datetime.datetime.now()
    end = time.time()
    createHtml(titles=u'接口测试报告', filepath=filepath, starttime=starttime, endtime=endtime, passge=result_pass,
               fail=result_fail, id=id_list, name=projecct_list, headers=Interface_headers_list,
               coneent=Interface_url_list, url=Interface_meth_list, meth=Interface_pase_list,
               yuqi=Interface_assert_list, json=bask_list, relusts=relusts)
    hour = end - star
    user_id = User.query.filter_by(role_id=2).first().id
    new_reust = TestResult(Test_user_id=user_id, test_num=result_toal, pass_num=result_pass, fail_num=result_fail,
                           test_time=starttime, hour_time=hour, test_rep=(day + '.html'), test_log=(day + '.log'))
    email = EmailReport.query.filter_by(role_id=2, default_set=True).first()
    send_emails(sender=email.send_email, receivers=task.taskrepor_to, password=email.send_email_password,
                smtp=email.stmp_email, port=email.port, fujian1=file, fujian2=filepath, subject=u'%s自动用例执行测试报告' % day,
                url='%stest_rep'%(request.url_root))
    db.session.add(new_reust)
    db.session.commit()

mock 服务的一个请求方式的代码

class MakemockserverView(MethodView):#做一个mock服务
    def get(self,path):#get请求方法
        huoqupath=Mockserver.query.filter_by(path=path,status=True).first()
        heders=request.headers
        method=request.method
        if not huoqupath:
            abort(404)
        if method.lower() !=huoqupath.methods:
            return  jsonify({'code':'-1','message':'请求方式错误!','data':''})
        if huoqupath.is_headers==True:
            if comp_dict(heders,huoqupath.headers) ==True:
                if huoqupath.ischeck==True:
                    paerm = request.values.to_dict()
                    if dict_par(paerm,huoqupath.params)==True:
                        if huoqupath.rebacktype == 'json':
                            try:
                                json_fan = json.dumps(huoqupath.fanhui)
                                return jsonify({'code': '1', 'message': 'successs', 'data': json_fan})
                            except:
                                return jsonify({'code': '-2', 'message': '你写入的返回不能正常json!请检查', 'data': ''})
                        elif huoqupath.rebacktype == 'xml':
                            response = make_response(huoqupath.fanhui)
                            response.content_type = 'application/xml'
                            return response
                        else:
                            return jsonify({'code': '-2', 'message': '你写入的类型目前系统不支持', 'data': ''})
                    else:
                        return jsonify({'code': '-4', 'message': '你输入的参数不正确', 'data': ''})
                else:
                    if huoqupath.rebacktype=='json':
                        try:
                            json_fan=json.dumps(huoqupath.fanhui)
                            return  jsonify({'code': '1', 'message': 'successs', 'data':json_fan})
                        except:
                            return jsonify({'code': '-2', 'message': '你写入的返回不能正常json!请检查', 'data': ''})
                    elif huoqupath.rebacktype =='xml':
                        response=make_response(huoqupath.fanhui)
                        response.content_type='application/xml'
                        return response
                    else:
                        return jsonify({'code': '-2', 'message': '你写入的类型目前系统不支持', 'data': ''})
            else:
                return jsonify({'code': '-3', 'message': '安全校验失败!', 'data': ''})
        if huoqupath.ischeck == True:
            paerm = request.values.to_dict()
            if dict_par(paerm, huoqupath.params) == True:
                if huoqupath.rebacktype == 'json':
                    try:
                        json_fan = json.dumps(huoqupath.fanhui)
                        return jsonify({'code': '1', 'message': 'successs', 'data': json_fan})
                    except:
                        return jsonify({'code': '-2', 'message': '你写入的返回不能正常json!请检查', 'data': ''})
                elif huoqupath.rebacktype == 'xml':
                    response = make_response(huoqupath.fanhui)
                    response.content_type = 'application/xml'
                    return response
                else:
                    return jsonify({'code': '-2', 'message': '你写入的类型目前系统不支持', 'data': ''})
            else:
                return jsonify({'code': '-4', 'message': '你输入的参数不正确', 'data': ''})
        else:
            if huoqupath.rebacktype == 'json':
                try:
                    json_fan = json.dumps(huoqupath.fanhui)
                    return jsonify({'code': '1', 'message': 'successs', 'data': json_fan})
                except:
                    return jsonify({'code': '-2', 'message': '你写入的返回不能正常json!请检查', 'data': ''})
            elif huoqupath.rebacktype == 'xml':
                response = make_response(huoqupath.fanhui)
                response.content_type = 'application/xml'
                return response
            else:
                return jsonify({'code': '-2', 'message': '你写入的类型目前系统不支持', 'data': ''}) #

开源地址:https://github.com/liwanlei/FXTest

使用说明:

1.依赖包为 requirements.txt 文件下的,可能部分不全,需要什么可以自己使用中增加。
2.目前由于考虑后续迁移内部使用的话,所以移除了注册功能,改为管理员后台添加方式,默认登录:liwanlei 密码:liwanlei
3.部分功能调试还存在一定的问题,欢迎各位多提宝贵意见,

部分功能欠缺:

  1. 定时任务的持久化,现在处理容易受到运行过程中的宕机等情况重新启动服务器的定时任务全部需要开启
  2. mock 接口只能支持单一的 path
  3. 测试环境没有改为动态配置,动态支持。目前测试环境管理以及上线
  4. 部分地方可能还会有不严谨性,但是工具可以使用。
  5. 目前仅支持 http 请求中的 json 格式的,

大家可以多提意见,后续会优化,最近一直熬夜加班可能有些消息不能及时回复,还望谅解。

项目开发中,有些功能的开发参照了论坛部分作者的文章给我学习上面指引了方向,感恩 testerhome,

测试路上,我一直在前进,感恩各位社区大拿的分享的好技术,好文章

附言 1  ·  2017年12月01日

增加体验地址。临时地址
http://60.205.187.178:89/ 账号:liwanlei 密码:liwanlei

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 40 条回复 时间 点赞

😮 加油~

白纸 回复

继续努力。

想请问下,界面是用什么做的呢

尹全旺 回复

Bootstrap

if huoqupath.is_headers==True:
            if comp_dict(heders,huoqupath.headers) ==True:
                if huoqupath.ischeck==True:
                    paerm = request.values.to_dict()
                    if dict_par(paerm,huoqupath.params)==True:
                        if huoqupath.rebacktype == 'json':

代码需要优化,判断条件太多且重复,需要抽离方法,先提一个,判断 Trueis 不用 ==

建议把任务管理独立模块化或者做成 service。

0x88 回复

目前没有这个打算,后期可以优化

fdeferf 回复

有些地方看是重复,但是有的地方的检验是必须的,优化后续再进行。

可以做环境分离 建议参考 hdc 插件的设计

雷子 #10 · 2017年11月28日 Author
大奇 回复

Hdc 插件是什么,没了解呢,请指点

雷子 回复

Chrome 一个 api 插件 类似 psotman

雷子 #12 · 2017年11月28日 Author
大奇 回复

后续可以研究研究优化进去。

思寒_seveniruby 将本帖设为了精华贴 11月28日 21:16

厉害!前来捧场
正好最新在学习 flask,这次有代码可以看了,谢谢!
里面的 Task 类,是自己实现的吗?

好巧,我用 reactjs+flask+unitest,也写了个。😛 😛

匿名 #16 · 2017年12月04日

问一下,支持多接口间传参吗?支持数据库查询值作为参数吗?支持对 json 类型返回值的解析吗?

下库下了一个小时😂

雷子 #18 · 2017年12月04日 Author

用例就是存数据库。json 支持的,你说的多接口传参什么意思。

雷子 #19 · 2017年12月04日 Author
FelixKang 回复

没救了,网速

匿名 #20 · 2017年12月05日
雷子 回复

场景 1:
我需要先请求创建订单的接口,才能生成相应的订单 id;然后下一个编辑订单的接口里,我需要用这个订单 id 作为入参;额外需要的能力是,接口返回的是 json 格式的内容,而这个 id 是 json 内的很多个键值对的其中之一,所以需要解析 json 才能拿到这个 id 值;
场景 2:
现在有个接口,他的入参之一是个时间值,规则是不能超过当前日期 60 天,不能低于当前时间 30 天,例如现在是 12 月 5 日,那么这个参数只能是 2 月 5 日之前,1 月 5 日之后的时间值(大约的数值,没有判定 31 天什么的),那么在你的平台我要如何生成这个时间值?

雷子 #21 · 2017年12月05日 Author

我这里的接口都是保证接口测试的独立性。

匿名 #22 · 2017年12月05日
雷子 回复

你的意思是你的接口只能独立测试(也就是单接口测试),而不能组成业务流程?那直接用 postman 不就好了?postman 有分组协作功能的,完全可以当做一个平台使用

雷子 #23 · 2017年12月05日 Author

业务流那是另外一种,我这种服务的是单独的接口测试,想改业务流的那种也可以。目前没有做到那一步。工具选择根据自己的业务和自己公司的情况来定,工具不是万能的,只能是大家一起努力让工具更加万能

其实把前一个接口的返回值保存起来(文件、常量或者数据库),下个接口再去取,并读取解析到指定字段就可以了。我的平台已经实现了,有空再整理下分享出来。

81—1 回复

👍 ,如果真的支持业务接口串行了,那你这个可以做爬虫了😀
我们最近也要做一个自动化测试平台,包含 ui 自动化和接口自动化,另外包含部分运维功能。
给点儿建议:
1、可以再增加一些返回是 html 的解析。在判断期望值和接口返回值的时候做一下兼容。
2、接口测试方法要改一下,最好重新封装,做一些兼容。尽量把错误抛出来,不要错误就返回空{},可以改成{‘error’:str(e)}
在报告里面给出来。
3、换数据库
另外有些表单的处理,并没有触发相关的 sql,可以改进一下。一个人写这样很不错了,我们三个人做一个平台,还没搞定。😂

这个就是我前段时间想做的,但是实践有点难啊,虽然已经写好了接口的 PO 框架,但不知如何和 flask 结合起来,我要好好看下你写的,学习一下

—— 来自 TesterHome 官方 安卓客户端

你们开发用的也是 Python 么,我对 Python. mock 不是很了解,我们公司开发语言是 Java,能用 Python 模拟 java 的 mock 么,目前 mock 都是开发自己写的,比如放款功能,如果可以用 Python 写的话那就是福音了,哈哈哈哈哈哈

—— 来自 TesterHome 官方 安卓客户端

雷子 #28 · 2017年12月12日 Author
小L 回复

可以的,完全可以实现。

仅楼主可见

想请问下楼主,自动化测试的投入产出比咋样,或者通过什么措施将自动化测试价值体现到最大。现在比较困惑这一点,求交流。😀

Kenneth 回复

我之前也有这个困惑,这个投入产出跟你做的自动化项目框架、项目需求是否稳定等各种原因堆积在一起的。如果需求经常变更,甚至 ui 经常变更,这种做自动化项目投入比较大,。当然不管怎么样,一个好的框架,能提高产出比。

雷子 #32 · 2017年12月14日 Author
joker 回复

注意,我这里说到的是接口,接口变动的相对于 ui 低,我也在力推接口。

对于 mock 可以更好的改进一下,可以通过 jsonpath 匹配 post 中的 json ,还有一个就是可以控制 mock response status 和 header ,还有回调方法!还有一种需要解决的问题就是如何处理 pathvaribal 的请求方式!

太厉害了。。希望有一天自己也能像你一样维护自己搭建的测试框架

没看懂最后的接口管理界面和用例管理界面的区别是什么,为什么要设计两个,看内容感觉好像一回事儿

雷子 #36 · 2017年12月20日 Author
许雯 回复

接口管理和接口用例这两个功能都不一样,依据接口管理的接口来写接口测试用例。一个管理接口,一个管理用例

雷子 #37 · 2017年12月26日 Author

我感觉两个场景都是可以实现的,目前没有加入进去这个,第一个场景 技术处理还简单,第二个 能实现,但是做出来也内部用行, 一般我做的 基于的都是通用的,类似你说的这种可以实现,但是不通用的,不能做到所有的接口都支持,只能是尽可能的去满足大部分的接口测试,迁移到公司用的开放平台都会二次开发,因为这些做的都是针对通用的技术比较多。

雷子 #38 · 2017年12月26日 Author
joker 回复

谢谢建议,数据库我才用的是最简单方便的,表单处理上面会如果产生数据的交互才会触发,这个数据库迁移还是很快的,我在迁移到公司来说,这个 sqlite 满足业务需求,增加运维平台这里我感觉应该运维是一个单独的平台更好,UI 自动化的测试平台其实好做了很多,接口能做出来,做 UI 就不难,关键是需求,还有三个人并行开发的配合能力、。

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 12月13日 14:44
simple [精彩盘点] TesterHome 社区 2018 年 度精华帖 中提及了此贴 01月07日 12:08

说实话这种代码根本就过不了 code review!

Jay_ 回复

不断精进🔥

雷子 #43 · 2022年01月17日 Author
Jay_ 回复

很久以前的文章了,都更新迭代了很多版本了

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册