项目背景:
目前测试接口有些是依赖第三方接口,若第三方接口出现异常,会对测试进度有所影响。需要开发mock相关功能辅助测试。
技术选型:
1.前端:python+xadmin+django+mysql,通过界面录入生成wiremock下mapping、_files文件夹下的json文件;
2.后端:WireMock的standalone模式,通过shell脚本进行一键启停管理,以及实时刷新url、mapping映射。-----未完成
wiremock工作原理:
最新jar包下载地址:http://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-standalone/2.9.0/wiremock-standalone-2.9.0.jar
启动jar包:java -jar wiremock-standalone-2.9.0.jar –port 9999 —verbose
(–port设定端口为9999; –verbose开启日志。更多参数需要参考:
http://wiremock.org/docs/running-standalone/
启动后在同目录下生成两个空的文件夹:__files和mappings。__files是放上传/下载/录制文件的,mappings放response和request url的映射的。
在mappings文件夹下随便创建一个*.json文件,比如长下面这样:
"request": {
"method": "GET",
"url": "/api/testdetail"
},
"response": {
"status": 200,
"bodyFileName": "testdetail.json”,
"headers": {
"Content-Type": "application/json",
"Cache-Control": "max-age=86400"
}
}
}
bodyFileName还可以是html、xml等文档。
在浏览器或者使用curl命令,调用http://localhost:9999/api/testdetail,就能返回testdetail.json的内容了。testdetail.json就是需要我们在__files里面建立的响应文件。wiremock也支持直接在response结构体中返回响应内容,比如在mapping文件中,将response写成下面这样:
"response": {
"status": 200,
"body": “Hello world ",
"headers": {
"Content-Type": "application/json",
"Cache-Control": "max-age=86400"
}
当发送请求时候,将直接返回“Hello world”。
前端实现:
1.思路分析:
1)添加:
支持get和post两种,其中:get可带请求参数,也可不带;post请求必带请求参数;返回类型可为text,也可为json类型;添加后应根据选择请求方式和请求返回类型生成不同格式的json文件;
2)删除:
删除时应同时删除已生成的json文件;
2.代码部分:
views.py:
from __future__ import unicode_literals
from django.shortcuts import render,redirect,HttpResponse
from mockserver import models
from .mock_forms import *
from django.db.models import Q
from utils.pagination import *
from mockjson import *
import json
# Create your views here.
class JsonCustomEncoder(json.JSONEncoder):
def default(self, field):
if isinstance(field, ValidationError):
return {'code': field.code, 'messages': field.messages}
else:
return json.JSONEncoder.default(self, field)
def MockManage(request,mid):
'''mock接口管理'''
if request.method == "GET":
method_type = request.GET.get("method_type")
keywords = request.GET.get("keywords")
reset = request.GET.get("reset")
if reset:
return redirect('/mock/mockmanage-%s' % mid)
else:
project_info = models.MockProject.objects.filter(id=mid).values("mockname", 'id')
if not method_type:
method_type = 0
elif int(method_type) == 1:
selectstatus = "GET"
elif int(method_type) == 2:
selectstatus = "POST"
if not keywords:
keywords = ""
if int(method_type) == 0:
selectstatus = u"全部"
counts = models.MockDetail.objects.filter(Q(mock_name__contains=keywords),De_Pro_id = mid).count()
if counts:
mockinfo = models.MockDetail.objects.filter(Q(mock_name__contains=keywords),De_Pro_id = mid).values(
'id',
"mock_name",
"mock_summary",
"mock_method",
"mock_url",
"mock_status",
"mock_query"
).order_by("id")
current_page = request.GET.get('p', 1)
current_page = int(current_page)
page_obj = Page(current_page, counts)
try:
mockinfo = mockinfo[page_obj.start:page_obj.end]
now_url = request.get_full_path()
if "&p=" in now_url:
now_url = (now_url.split("&p="))[0]
elif "?p=" in now_url:
now_url = (now_url.split("?p="))[0]
page_str = page_obj.page_str(now_url)
except Exception:
mockinfo = 0
page_str = 0
return render(
request,
'mockcenter/mockmanage.html',{
'project_info':project_info,
'counts':counts,
"mockinfo":mockinfo,
'keywords': keywords,
'selectstatus': selectstatus,
"page_str": page_str,
}
)
else:
counts = 0
return render(
request,
'mockcenter/mockmanage.html', {
'project_info': project_info,
'counts': counts,
'keywords': keywords,
'selectstatus': selectstatus,
}
)
elif int(method_type) == 1 or int(method_type) == 2:
counts = models.MockDetail.objects.filter(Q(mock_name__contains=keywords), De_Pro_id=mid,mock_method=selectstatus).count()
if counts:
mockinfo = models.MockDetail.objects.filter(Q(mock_name__contains=keywords), mock_method=selectstatus,De_Pro_id=mid).values(
'id',
"mock_name",
"mock_summary",
"mock_method",
"mock_url",
"mock_status",
"mock_query"
).order_by("id")
current_page = request.GET.get('p', 1)
current_page = int(current_page)
page_obj = Page(current_page,counts)
try:
mockinfo = mockinfo[page_obj.start:page_obj.end]
now_url = request.get_full_path()
if "&p=" in now_url:
now_url = (now_url.split("&p="))[0]
elif "?p=" in now_url:
now_url = (now_url.split("?p="))[0]
page_str = page_obj.page_str(now_url)
except Exception:
mockinfo = 0
page_str = 0
return render(
request,
'mockcenter/mockmanage.html', {
'project_info': project_info,
'counts': counts,
"mockinfo": mockinfo,
'keywords': keywords,
'selectstatus': selectstatus,
"page_str": page_str,
}
)
else:
counts = 0
return render(
request,
'mockcenter/mockmanage.html', {
'project_info': project_info,
'counts': counts,
'keywords': keywords,
'selectstatus': selectstatus,
}
)
def AddMock(request,mid):
'''增加mock接口'''
if request.method == "GET":
project_info = models.MockProject.objects.filter(id=mid).values("mockname", 'id','mockdetail__mock_method')
return render(
request,
'mockcenter/mockadd.html',{
'mid':mid,
'project_info': project_info,
}
)
elif request.method == "POST":
mockname = request.POST.get("mockname")
mocksummary = request.POST.get("mocksummary")
mock_method = request.POST.get("mock_method")
mockurl = request.POST.get("mockurl")
mockquery = request.POST.get("mockquery")
mockstatus = request.POST.get("mockstatus")
response_json = request.POST.get("response_json")
response_text = request.POST.get("response_text")
mockheaders = request.POST.get("mockheaders")
mockseconds = request.POST.get("mockseconds")
mock_stype = request.POST.get("mock_stype")
ret={'status':False,'data':None,'error':None}
obj = Mock_FM(request.POST)
if obj.is_valid():
if str(mock_stype) == "TEXT":
mockresponse = response_text
print(mockresponse)
elif str(mock_stype) == "JSON":
mockresponse = response_json
mock_info = {
"mockname":mockname,
"url":mockurl,
"method":mock_method,
"mockquery":mockquery,
"status":mockstatus,
"mock_type":mock_stype,
"response":mockresponse,
"headers":mockheaders,
"mockseconds":mockseconds
}
Make_mockjson(mock_info) #往wiremock的mappings和__files文件中新增json文件
models.MockDetail.objects.create(
De_Pro_id=mid,
mock_name=mockname,
mock_method=mock_method,
mock_url=mockurl,
mock_summary=mocksummary,
mock_status=mockstatus,
mock_stype = mock_stype,
mock_reponse=mockresponse,
mock_query=mockquery,
reponse_headers=mockheaders,
mock_seconds=mockseconds
)
ret['status'] = True
else:
print(obj.errors.as_data())
ret['status'] = False
ret['error'] = obj.errors.as_data()
result = json.dumps(ret,cls=JsonCustomEncoder)
return HttpResponse(result)
def DelMock(request,mid):
'''删除mock接口'''
row_id = request.POST.get("row_id")
if row_id:
row_id = row_id.split(",")
if len(row_id) > 1:
for i in row_id[1:]:
models.MockDetail.objects.filter(id=i,De_Pro_id=mid).delete()
else:
models.MockDetail.objects.filter(id=row_id[0], De_Pro_id=mid).delete()
return redirect("/mock/mockmanage-%s"%mid)
def EditMock(request,mid,cid):
if request.method == "GET":
project_info = models.MockProject.objects.filter(id=mid).values("mockname",'id')
mockinfo = models.MockDetail.objects.filter(De_Pro_id=mid,id=cid).all()
return render(
request,
"mockcenter/mockedit.html",{
"mid":mid,
"project_info":project_info,
"mockinfo":mockinfo
})
elif request.method == "POST":
mockname = request.POST.get("mockname")
mocksummary = request.POST.get("mocksummary")
mock_method = request.POST.get("mock_method")
mockurl = request.POST.get("mockurl")
mockquery = request.POST.get("mockquery")
mockstatus = request.POST.get("mockstatus")
mockresponse = request.POST.get("mockresponse")
mockheaders = request.POST.get("mockheaders")
mockseconds = request.POST.get("mockseconds")
ret = {'status': False, 'data': None, 'error': None}
obj = Mock_FM(request.POST)
if obj.is_valid():
old_mockinfo = models.MockDetail.objects.filter(De_Pro_id=mid,mock_name=mockname).values("id")
try:
if int(old_mockinfo[0]["id"]) != int(cid):
edict_dir = {}
edict_dir['mockname']= [ValidationError(u"已存在相同名称的接口,请重新输入!")]
ret['status'] = False
ret['error'] = edict_dir
else:
models.MockDetail.objects.filter(De_Pro_id=mid, id=cid).update(
mock_name=mockname,
mock_method=mock_method,
mock_url=mockurl,
mock_summary=mocksummary,
mock_status=mockstatus,
mock_reponse=mockresponse,
mock_query=mockquery,
reponse_headers=mockheaders,
mock_seconds=mockseconds
)
ret['status'] = True
except Exception:
models.MockDetail.objects.filter(De_Pro_id=mid, id=cid).update(
mock_name=mockname,
mock_method=mock_method,
mock_url=mockurl,
mock_summary=mocksummary,
mock_status=mockstatus,
mock_reponse=mockresponse,
mock_query=mockquery,
reponse_headers=mockheaders,
mock_seconds=mockseconds
)
ret['status'] = True
else:
ret['status'] = False
print(obj.errors.as_data())
ret['error'] = obj.errors.as_data()
result = json.dumps(ret, cls=JsonCustomEncoder)
return HttpResponse(result)
mock_forms.py:
# -*- coding:utf-8 -*-
# __author__ == 'cc'
from django import forms
from django.forms import fields
from django.core.exceptions import ValidationError
from mockserver import models
import re,json
class Mock_FM(forms.Form):
mockname = fields.CharField(
max_length=64,
required=True,
error_messages={
'required':u'接口名称不能为空',
'max_length':u"接口名称最多为64个字符",
}
)
mocksummary = fields.CharField(
max_length=512,
required=True,
error_messages={
'required': u'接口描述不能为空',
'max_length': u"接口描述最多为512个字符",
}
)
mock_method = fields.CharField(
required=True
)
mock_stype = fields.CharField(
required=True
)
mockurl = fields.CharField(
max_length=255,
required=True,
error_messages={
'required': u'url不能为空',
'max_length': u"url最多为255个字符",
}
)
mockquery = forms.CharField(
max_length=512,
required=False,
error_messages={
'max_length': u"请求参数最多为512个字符",
}
)
mockstatus = forms.IntegerField(error_messages={
'required':u"状态码不能为空",
"invalid":u"必须输入正确的状态码",
})
response_text = fields.CharField(
max_length=6144,
required=False,
error_messages={
'max_length': u"返回结果最多为6144个字符",
}
)
response_json = fields.CharField(
max_length=6144,
required=False,
error_messages={
'max_length': u"返回结果最多为6144个字符",
}
)
mockheaders = fields.CharField(
max_length=2048,
required=False,
error_messages={
'max_length': u"返回头最多为2048个字符",
}
)
mockseconds = forms.CharField(
max_length=255,
required=False,
error_messages={
'max_length': u"延迟时间最多为255个字符",
}
)
def clean_mock_name(self):
mockname = self.cleaned_data['mock_name']
if mockname:
c = models.MockDetail.objects.filter(mock_name=mockname).count()
if c:
raise ValidationError(u"接口描述已存在,请重新输入!")
else:
return mockname
def clean_mock_url(self):
mock_url = self.cleaned_data['mock_url']
if mock_url:
c = models.MockDetail.objects.filter(mock_url=mock_url).count()
if c:
raise ValidationError(u"接口描述已存在,请重新输入!")
else:
return mock_url
def clean_mockquery(self):
mockquery = self.cleaned_data['mockquery']
mock_method = self.cleaned_data['mock_method']
if mock_method == "GET":
if mockquery:
mockquery = str(mockquery).replace("'", '"')
try:
mockquery = json.loads(mockquery)
if isinstance(mockquery, dict):
return mockquery
else:
raise ValidationError(u"请输入正确格式的请求参数!")
except ValueError:
raise ValidationError(u"请输入正确格式的请求参数!")
else:
mockquery = ""
return mockquery
elif mock_method == "POST":
if mockquery:
mockquery = str(mockquery).replace("'", '"')
try:
mockquery = json.loads(mockquery)
if isinstance(mockquery, dict):
return mockquery
else:
raise ValidationError(u"请输入正确格式的请求参数!")
except ValueError:
raise ValidationError(u"请输入正确格式的请求参数!")
else:
raise ValidationError(u"请输入正确格式的请求参数!")
def clean_mockresponse(self):
mock_stype = self.cleaned_data['mock_stype']
if str(mock_stype) == "TEXT":
mockresponse = self.cleaned_data['response_text']
if mockresponse:
mockresponse = str(mockresponse).replace("'", '"')
return mockresponse
else:
raise ValidationError(u"返回结果不能为空!")
elif str(mock_stype) == "JSON":
mockresponse = self.cleaned_data['response_json']
if mockresponse:
mockresponse = str(mockresponse).replace("'", '"')
try:
mockresponse = json.loads(mockresponse)
if isinstance(mockresponse, dict):
return mockresponse
else:
raise ValidationError(u"请输入正确格式的返回结果!")
except ValueError:
raise ValidationError(u"请输入正确格式的返回结果!")
else:
raise ValidationError(u"返回结果不能为空!")
def clean_mockheaders(self):
mockheaders = self.cleaned_data['mockheaders']
mock_stype = self.cleaned_data['mock_stype']
mockheaders = str(mockheaders).replace("'", '"')
if str(mock_stype) == "TEXT":
return mockheaders
elif str(mock_stype) == "JSON":
try:
mockheaders = json.loads(mockheaders)
if isinstance(mockheaders,dict):
return mockheaders
else:
raise ValidationError(u"请输入正确格式的返回头!")
except ValueError:
raise ValidationError(u"请输入正确格式的返回结果头!")
def clean_mockseconds(self):
mockseconds = self.cleaned_data['mockseconds']
if mockseconds:
return mockseconds
else:
mockseconds = ""
return mockseconds
生成json文件:
# -*- coding:utf-8 -*-
# __author__ == 'cc'
import json,os
def Make_mockjson(mockinfo):
'''获取mock信息,创建符合wiremock要求的json文件'''
json_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
mapping_name = "%s_%s%s.json" % (mockinfo['url'].split("/")[-1],mockinfo['method'],mockinfo['status']) #mapping夹下的json文件名:截取url最后路径_接口方式_状态码
file_name = "file_%s%s.json"%(mockinfo['url'].split("/")[-1],mockinfo['status'])#__file文件夹下的json文件名:file_截取url最后路径_状态码
if mockinfo['method'] == "GET":
if mockinfo['mockquery']:
mockquery = json.loads(mockinfo['mockquery'])
query_infos = ""
for k,v in enumerate(mockquery):
query_infos +="%s=%s&"%(v,mockquery[v]) #参数组合成k1=v1&k2=v2格式
if str(mockinfo['mock_type']) == "JSON": #接口方式为get、有请求参数且返回类型为json时,创建如下格式的json文件
json_data = {
"request":{
"method":"GET",
"urlPattern":r"%s\?%s"%(mockinfo['url'],query_infos[:-1]),
},
"response":{
"status":mockinfo['status'],
"bodyFileName":"%s.json"%file_name,
"headers":json.loads(mockinfo['headers'])
}
}
elif str(mockinfo['mock_type']) == "TEXT":#接口方式为get、有请求参数且返回类型为text时,创建如下格式的json文件
json_data = {
"request": {
"method": "GET",
"urlPattern": r"%s\?%s" % (mockinfo['url'], query_infos[:-1]),
},
"response": {
"status": mockinfo['status'],
"body": mockinfo['response'],
"headers": json.loads(mockinfo['headers'])
}
}
else:
if str(mockinfo['mock_type']) == "JSON": #接口方式为get,m
json_data = {
"request":{
"method":"GET",
"url":mockinfo['url']
},
"response":{
"status":mockinfo['status'],
"bodyFileName": "%s.json" % file_name,
"headers": json.loads(mockinfo['headers'])
}
}
elif str(mockinfo['mock_type']) == "TEXT":
json_data = {
"request": {
"method": "GET",
"url": mockinfo['url']
},
"response": {
"status": mockinfo['status'],
"body": mockinfo['response'],
}
}
elif mockinfo['method'] == "POST":
mockquery = json.loads(mockinfo['mockquery'])
json_query = json.dumps(mockquery)
json_query_new=json_query.replace('"',r'\"')
if str(mockinfo['mock_type']) == "JSON":
json_data = {
"request": {
"method": "POST",
"url": mockinfo['url'],
"bodyPatterns":[
{
"equalToJson":"%s"%json_query_new,
"jsonCompareMode":"LENIENT"
}
]
},
"response": {
"status": mockinfo['status'],
"bodyFileName": "%s" % file_name,
"headers": json.loads(mockinfo['headers'])
}
}
elif str(mockinfo['mock_type']) == "TEXT":
json_data = {
"request": {
"method": "POST",
"url": mockinfo['url'],
"bodyPatterns": [
{
"equalToJson": "%s" % json_query_new,
"jsonCompareMode": "LENIENT"
}
]
},
"response": {
"status": mockinfo['status'],
"body": mockinfo['response'],
}
}
mapping_data = json.dumps(json_data)
mapping_path = os.path.join(json_dir,'wiremock/mappings/%s'%mapping_name)
with open(mapping_path,'wb+') as f:
f.write(mapping_data)
if str(mockinfo['mock_type']) == "JSON":
file_path =os.path.join(json_dir,'wiremock\__files\%s'%file_name)
post_response = json.dumps(str(mockinfo['response']))
with open(file_path,'wb+') as f1:
f1.write(json.loads(post_response))
def Del_mockjson():
'''删除json文件'''
pass
html模板:
1)mockmanage.html
接口查询
接口名称:
接口类型:
{% if selectstatus == "GET" %}
全部
GET
POST
{% elif selectstatus == "POST" %}
全部
GET
POST
{% else %}
全部
GET
POST
{% endif %}
查询接口
- {{ counts }} 接口信息
{% if counts %}
接口名称
接口描述
接口类型
URL
状态码
操作
{% for m in mockinfo %}
{{ m.mock_name }}
{{ m.mock_summary }}
{{ m.mock_method }}
{{ m.mock_url }}
{{ m.mock_status }}
{% endfor %}
{% else %}
暂无Mock接口信息
{% endif %}
{{ page_str }}
2)mockadd.html
接口名称
*
接口描述
*
方法类型
*
GET
POST
URL
*
请求参数
返回状态码
*
返回类型
*
TEXT
JSON
返回结果
*
返回头
*
返回结果
*
响应延迟
保存
3)mockedit.html
{% for m in mockinfo %}
接口名称
*
接口描述
*
方法类型
*
{% if m.mock_method == "GET" %}
GET
POST
{% elif m.mock_method == "POST" %}
GET
POST
{% endif %}
URL
*
请求参数
{{ m.mock_query }}
返回状态码
*
返回结果
*
{{ m.mock_reponse }}
返回头
*
{{ m.reponse_headers }}
响应延迟
{% endfor %}
保存
4)mock.js
function ValChange() {
var val = $("#mock_rtype").val();
if(val == "TEXT"){
$("#return_json").addClass("hide");
$("#return_text").removeClass("hide");
}else{
$("#return_json").removeClass("hide");
$("#return_text").addClass("hide");
}
};
$.ajaxSetup({
beforeSend:function (xhr,settings){
xhr.setRequestHeader('X-CSRFtoken',$.cookie('csrftoken'));
}
});
$("#addmocknow").click(function () {
var mid=$("#mockproject_id").val();
$.ajax({
url: "/mock/mockmanage-%s/add"%mid,
type: "POST",
data:$('#mockadd_form').serialize(),
success: function (data) {
var obj = JSON.parse(data);
if (obj.status) {
location.href="./";//若点击保存按钮,保存后返回到上级页面
}else{
if (obj.error.mockname){
$('#mockname_error').text(obj.error.mockname[0].messages);
}else{
if(obj.error.mocksummary){
$("#summary_error").text(obj.error.mocksummary[0].messages)
}else {
if (obj.error.mockurl) {
$("#url_error").text(obj.error.mockurl[0].messages)
} else {
if (obj.error.mockquery){
$("#query_error").text(obj.error.mockquery[0].messages)
}else{
if (obj.error.mockstatus) {
$("#status_error").text(obj.error.mockstatus[0].messages)
}else {
if (obj.error.mockresponse) {
$("#resopnse_error").text(obj.error.mockresponse[0].messages)
} else {
if (obj.error.mockheaders) {
$("#headers_error").text(obj.error.mockheaders[0].messages)
}else{
$("#headers_error").text(obj.error)
}
}
}
}
}
}
}
}
}
})
});
$("#addanother").click(function () {
var mid=$("#modeltype").val();
$.ajax({
url: "/case/casemanage-%s/add"%mid,
type: "POST",
data:$('#caseadd_form').serialize(),
success: function (data) {
var obj = JSON.parse(data);
if (obj.status) {
location.reload();//若点击保存并增加另一个则刷新当前页面
}else{
$('#error_msg').text(obj.error);
}
}
})
});
$(".case_action_del").each(function () {
$(this).click(function () {
$(this).parents().find(".del_text").removeClass("hide");
var row_id = $(this).attr("row_id");
$("#row_id").val(row_id);
})
});
$("#Del_chos").click(function () {
$("#Del_dom").removeClass("hide");
})
$("#del_console").click(function () {
$("#Del_dom").addClass("hide");
});
$("#editmocknow").click(function () {
var mid=$("#mockproject_id").val();
var cid = $("#edit_id").val();
$.ajax({
url: "/mock/mockmanage-%s/edit-%s"%(mid,cid),
type: "POST",
data:$('#mockedit_form').serialize(),
success: function (data) {
var obj = JSON.parse(data);
if (obj.status) {
location.href="./";//若点击保存按钮,保存后返回到上级页面
}else{
if (obj.error.mockname){
$('#mockname_error').text(obj.error.mockname[0].messages);
}else{
if(obj.error.mocksummary){
$("#summary_error").text(obj.error.mocksummary[0].messages)
}else {
if (obj.error.mockurl) {
$("#url_error").text(obj.error.mockurl[0].messages)
} else {
if (obj.error.mockquery){
$("#query_error").text(obj.error.mockquery[0].messages)
}else{
if (obj.error.mockstatus) {
$("#status_error").text(obj.error.mockstatus[0].messages)
}else {
if (obj.error.mockresponse) {
$("#resopnse_error").text(obj.error.mockresponse[0].messages)
} else {
if (obj.error.mockheaders) {
$("#headers_error").text(obj.error.mockheaders[0].messages)
}else{
$("#headers_error").text(obj.error)
}
}
}
}
}
}
}
}
}
})
});
后端部分
未实现---待补
最终效果: