hello大家好,咱们今天介绍的是如何使用python+Django实现测试桩,该测试桩功能包含登录,验证登录,修改mock信息,合并mock信息,提交mock信息等
可能你会问该测试桩是实现的什么功能呢,该测试桩主要是在项目中对一些对接第三方平台,而第三方平台没有测试环境或者说数据测试达不到自己项目需求而衍生出来的一个平台。可能你会说市面上有mock平台啊或者也可以让开发写死返回这种,但是这两个方法有弊端:
1:市面上的mock平台不能在不满足mock数据时透传到真正的第三方
2:开发写死的话那如果我要测另一个场景然后再让开发写死?
3:市面上的mock平台在管理多个项目的mock时不好集中管理
测试桩的优点:
1:实时配置mock数据并返回
2:可以服务多个项目,数据分离
3:在mock数据不满足时,透传请求到真正的第三方平台获取数据并透传返回
4:有前后端页面可以给项目中所有人使用,在多个客户端同步mock数据时不出现冲突(根据mock数据结构写了个算法)
这里我画了两个流程图:
环境配置
- 安装python,这个方法不用说了吧,这个都不会的话出门左转吧
- 安装django,pip 安装即可
- 执行django新项目命令,名称随便起
django-admin startproject myproject;
- 进入到项目目录,新建子应用
python manage.py startapp myapp
- 注册子应用,在项目目录下找到setting文件新增
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"myapp",
]
- 在setting文件所在目录中找到urls文件,新增路由到子应用
from django.contrib import admin
from django.urls import path,re_path,include
urlpatterns = [
path('admin/', admin.site.urls),
path("",include('myapp.urls'))
]
开发mock接口
- 在项目根目录下新增项目json文件,我这里准备给三个项目mock
- 约定mock文件格式
{
"接口路径(域名后部分,如:adAgentBackend/api/ad_haiLiang)": {
"direct":"透传接口",
"pass":"是否透传True&False"
"description": "接口描述,如嗨量请求接口等",
"method": "接口请求方式(GET&POST)",
"mock(mock请求部分)": [
{
"description": "mock描述,比如:请求嗨量,返回抖音一元等",
"request": {
"key": "value"
},
"response(json格式返回)": {
"key": "value"
},
"response (字符串方式返回)": "value"
}
]
}
}
- 格式解读
method,direct,pass,mock这些为必须项
接口路径部分就是你请求的接口路径前边加上域名,就可以mock了。
direct为透传接口,就是真正的第三方,为了不影响没有mock的数据需要填写该参数
pass是否透传,如为True,请求该mock接口的所有数据都会透传到真正的第三方并透传返回,如为False,获取mock中查找是否有匹配的数据,如果有就返回mock数据,如果没有也会透传
method,这个… - 编写后端view路由到mock接口
在子应用文件目录中新增urls文件,添加该项
from django.urls import path, re_path
from . import views
urlpatterns = [
re_path("(?P<url>.*)",views.test),
]
- 在子应用文件目录中新增views文件,新增视图函数
@csrf_exempt
def test(request,url):
channel = request.headers.get("channel")
headers = {key.lower(): value for key, value in request.headers.items() if
key.lower() not in ['host', "postman-token", "content-type",
"content-length"] and value != ""}
if channel is not None:
data = read_json(url,channel.lower())
if data:
method = data.get("method")
if method:
method = method.upper()
else:
return JsonResponse({"code": "000000", "message": "该接口未配置请求方式"})
request_data = {}
request_json = {}
request_params = {}
expected = None
if method == str(request.method):
if method == "POST":
if request.POST.dict():
request_data = request.POST.dict()
elif request.body:
request_json = json.loads(request.body.decode())
else:
request_json = {}
expected = request_data if request_data else request_json
elif method == "GET":
request_params = request.GET.dict()
expected = request_params
else:
return JsonResponse({"code": "000000", "message": "该接口配置的请求方式不一致"})
direct_url = data.get("direct")
passthrough = data.get("pass")
if passthrough is not None and passthrough == "True" and direct_url is not None:
response = requests.request(method=method, url=direct_url, params=request_params, data=request_data,
json=request_json, headers=headers)
return HttpResponse(response)
else:
response = is_in(data.get("mock"),"request",expected,headers)
if response is not False:
return HttpResponse(response)
else:
if direct_url:
response = requests.request(method=method, url=direct_url, params=request_params,
data=request_data, json=request_json, headers=headers)
return HttpResponse(response)
else:
return JsonResponse({"code":"000000","message": "该接口未配置该请求返回信息"})
else:
return JsonResponse({"code":"000000","message": f"{channel}未配置该接口信息"})
else:
return JsonResponse({"code": "000000", "message": "channel为空"})
为什么需要@csrf_exempt这个装饰器呢,这个为后边的跨域问题作解决,有了该装饰器,django就不会验证这个函数的跨域问题
request参数后的url参数需与路由中的正则匹配名称一样
- test视图函数解读
1:首先我这里是给三个项目准备的,所以得和咱们的后端程序员约定,比如说Android项目需要请求mock,得在heders中添加channel项我才能到对应的文件中查找是否有mock数据
2:后边的话就是匹配请求路径,如有查看是否有匹配的mock数据,其他的话就跟前边介绍的json文件格式一样的判断
3:透传接口需要把headers中的一些做转换,不然很可能会报错
headers = {key.lower(): value for key, value in request.headers.items() if
key.lower() not in ['host', "postman-token", "content-type",
"content-length"] and value != ""}
4:需要用到查找mock是否存在的函数,该函数返回在mock中的索引,我把该函数放在了同目录下的utils文件中
def is_in(list,key,expect,headers):
for i in list:
if i.get(key) == expect:
mkheaders = i.get("headers")
if mkheaders:
if headers_(headers,i.get("headers")):
return i.get("response")
else:
pass
else:
return i.get("response")
else:
return False
5:由于有的请求还会有header,所以我这个测试桩也会验证header。如果你mock中有header,我在获取到请求参数跟mock中request一致时,会去判断两个header是否一致,如不一致也会透传请求接口,需要该函数
def headers_(requestHeaders,mockheaders):
for k,v in mockheaders.items():
k = k.lower()
if not requestHeaders.get(k) == v:
return False
return True
- 现在你就可以在项目json中新增mock数据来调试了
启动项目为
python manage.py runserver
前端部分展示json文件数据
有了上述部分代码后,你基本可以自己mock接口了,但是如果想把该项目给部门其他同事用,还得需要前端部分来编辑json文件,毕竟不可能在你电脑上来改吧
- 准备工作
1:需要jsoneditor组件
2:添加路由
path('index', views.Index.as_view(),name='index'),
- 编写路由函数
直接使用django自带的render返回html页面,你可以先写成函数格式的,我这里类格式是因为我编写的有登录功能,所以需要登录通过后才能进入到这编辑json文件
class Index(LoginRequiredMixin,View):
def get(self,request):
return render(request,'index.html')
- 编写html文件
1:在项目根目录下新增文件夹template
2:在setting文件中修改TEMPLATES为如下
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'template')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
- 3:编写html页面
3.1:在template文件下新增index.html文件
3.2:在项目根目录下新增static文件夹并保存以下文件
- 3.3:在setting中新增
# 静态文件的 URL 前缀
STATIC_URL = '/static/'
# 收集静态文件的目录,用于部署时
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# 除了每个应用下的 static 目录,额外的静态文件查找路径
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
- 3.4:在html head部分新增如下引用
<title>测试桩</title>
<!-- 引入 JSONEditor 的 CSS 文件 -->
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'jsoneditor/jsoneditor.min.css' %}">
<!-- 引入 JSONEditor 的 JavaScript 文件 -->
<script type="text/javascript" src="{% static 'jsoneditor/jsoneditor.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
- 3.5:编写jsoneditor部分
添加样式
#jsoneditor {
width: 100%;
{#height: 100%;#}
height: 600px;
{#<!-- min-height: 100px;-->#}
border: 1px solid #ccc;
margin-bottom: 10px;
}
添加标签,这部分在body中
<div id="jsoneditor"></div>
添加脚本,这部分在script中
const container = document.getElementById('jsoneditor');
const options = {
mode: 'tree', // 使用树状模式进行编辑
modes: ['tree', 'view', 'form', 'code', 'text'], // 支持多种模式
onEditable: function (node) {
return true; // 允许所有节点可编辑
},
onError: function (err) {
console.error('JSONEditor error:', err);
}
};
const editor = new JSONEditor(container, options);
- 3.6:编写项目选择框
我这里是给3个项目使用
<div class="dropdown-container">
<select id="dropdown">
<option value="android">android</option>
<option value="pcstore">pcstore</option>
<option value="browser">browser</option>
</select>
</div>
添加样式
.dropdown-container {
display: flex;
justify-content: center;
margin-bottom: 10px; /* 与下方元素的间距 */
}
#dropdown {
padding: 8px 12px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
}
- 3.7:给下拉选择框添加点击事件
const apigetUrl = 'http://10.109.4.173:8000/get_json';
const selectElement = document.getElementById('dropdown');
let former = null;
selectElement.addEventListener('change', function () {
const selectedValue = this.value;
if (selectedValue) {
fetchData();
}
});
async function fetchData() {
try {
former = null;
const selectedValue = selectElement.value;
const url = `${apigetUrl}?channel=${selectedValue}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// 将获取到的数据加载到编辑器中
former = data;
editor.set(data);
} catch (error) {
console.error('获取数据时出错:', error);
}
}
- 3.8:编写后端路由函数
urls文件中新增路由
path('get_json', views.Get_j.as_view()),
views文件中新增视图函数
class Get_j(LoginRequiredMixin,View):
def get(self,request):
request_data = request.GET.dict()
channel = request_data.get("channel")
if channel is not None:
channel = channel.lower()
JSON_ = os.path.join(BASE_DIR,f"{channel}_config.json")
try:
with open(JSON_, 'r', encoding='utf-8') as file:
data = json.load(file)
return JsonResponse(data)
except FileNotFoundError:
print(f"未找到文件: {JSON_}")
except json.JSONDecodeError:
print(f"文件 {JSON_} 不是有效的 JSON 格式。")
- 3.9:好现在就可以在前端部分展示你的json文件了
前端部分更新json文件数据
- 1:由于可能会有多个客户端操作,所以得考虑冲突并如何解决冲突。我这里的解决方法为,客户端请求数据的时候,本地会存一份副本,本地修改后提交给服务端,会把修改前,修改后的一起发给服务端。服务端会根据客户端修改前修改后的数据来判断,要删除哪些数据新增哪些数据修改哪些数据。
- 2:由于上述的fetchdata代码中已经包含了存储副本,所以这里就直接写提交数据了,首先需要保存按钮。在index.html文件中新增以下即可
button {
padding: 8px 16px;
border: none;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
}
.save-button {
background-color: #2196F3;
color: white;
}
<div class="button-container">
<button class="save-button" id="submitButton">保存</button>
</div>
const apiupUrl = 'http://10.109.4.173:8000/up_json';
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
async function submitData() {
const modifiedData = editor.get();
const selectedValue = selectElement.value;
const csrftoken = getCookie('csrftoken');
const total_json = {"after":modifiedData,
"former":former};
{#console(total_json);#}
try {
const response = await fetch(apiupUrl, {
method: 'POST', // 根据后端接口要求修改请求方法
headers: {
'Content-Type': 'application/json',
'channel':selectedValue,
'X-CSRFToken':csrftoken
},
body: JSON.stringify(total_json)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('数据提交成功:', result);
fetchData();
} catch (error) {
console.error('提交数据时出错:', error);
}
}
// 页面加载完成后获取数据
window.addEventListener('load', fetchData);
// 为提交按钮添加点击事件监听器
const submitButton = document.getElementById('submitButton');
submitButton.addEventListener('click', submitData);
- 3:编写后端views视图函数
urls中新增
path('up_json', views.Up_j.as_view()),
- 4:views视图中新增
这里在做下说明哈,LoginRequiredMixin这个是验证登录的,如果你没写登录你就把views视图路由写成函数并添加装饰器@csrf_exempt,像test那个视图一样写
class Up_j(LoginRequiredMixin,View):
def post(self,request):
headers = request.headers
channel = headers.get("channel")
if channel is not None:
channel = channel.lower()
JSON_ = os.path.join(BASE_DIR, f"{channel}_config.json")
request_data = request.POST.dict() if request.POST else json.loads(request.body.decode())
former = request_data.get("former")
after = request_data.get("after")
try:
with open(JSON_, 'r', encoding='utf-8') as file:
data = json.load(file)
try:
responseDict_ = merge_json(former, after, data)
with open(JSON_, 'w', encoding='utf-8') as file:
json.dump(responseDict_, file, indent=4, ensure_ascii=False)
file.flush()
return JsonResponse({"code":"000000","message":"保存成功"})
except:
return JsonResponse({"code": "000000", "message": "保存成功"})
except FileNotFoundError:
print(f"未找到文件: {JSON_}")
except json.JSONDecodeError:
print(f"文件 {JSON_} 不是有效的 JSON 格式。")
else:
return JsonResponse({"code": "000000"})
- 代码解读:
1:获取前端传的former 和 after数据,获取本地json数据
2:调用merge_json函数查看新增项,删除项,修改项
def merge_json(former_dict, after_dict, data_dict):
try:
result_dict = data_dict.copy()
former_keys = list(former_dict.keys())
after_keys = list(after_dict.keys())
result_keys = list(result_dict.keys())
added_items = {k: v for k, v in after_dict.items() if k not in former_keys and k not in result_keys}
deleted_items = {k: v for k, v in former_dict.items() if k not in after_keys}
modified_items = {k: v for k, v in after_dict.items() if k in former_keys and v != former_dict.get(k)
or k in result_keys and v != result_dict.get(k)}
for key, value in added_items.items():
result_dict[key] = value
for key in deleted_items:
if key in result_keys:
del result_dict[key]
for key, value in modified_items.items():
result_dict[key] = value
former_mock = former_dict.get(key, {}).get("mock", [])
after_mock = after_dict.get(key, {}).get("mock", [])
data_mock = data_dict.get(key, {}).get("mock", [])
mock = merge_mock(data_mock, after_mock, former_mock)
result_dict[key]["mock"] = mock
return result_dict
except Exception as e:
print(f"合并JSON数据时出错: {e}")
return data_dict
- 3:调用merge_mock函数查看每个接口中mock中的修改项,新增项以及删除项
def merge_mock(data_list, after_list, former_list):
try:
former_dict = {str(item.get("request"))+str(item.get("headers")): item for item in former_list}
after_dict = {str(item.get("request"))+str(item.get("headers")): item for item in after_list}
data_dict = {str(item.get("request"))+str(item.get("headers")): item for item in data_list}
former_keys = list(former_dict.keys())
after_keys = list(after_dict.keys())
result_keys = list(data_dict.keys())
deleted_items = []
modified_items = []
if former_list:
added_items = [after_dict[key] for key in after_keys if key not in former_keys]
deleted_items = [former_dict[key] for key in former_keys if key not in after_keys]
modified_items = [after_dict[key] for key in after_keys if key in former_keys and after_dict.get(key) != former_dict.get(key)]
else:
added_items = [after_dict[key] for key in after_keys if key not in result_keys]
for item in added_items:
if item not in data_list:
data_list.append(item)
for item in deleted_items:
if item in data_list:
data_list.remove(item)
for item in modified_items:
request_key = str(item.get("request"))+str(item.get("headers"))
if request_key in result_keys:
data_list.remove(data_dict[request_key])
data_list.append(item)
else:
data_list.append(item)
return data_list
except Exception as e:
print(f"合并mock数据时出错: {e}")
return data_list
- 这两个mock的函数我就不讲了哈,太伤脑细胞了,想了我好久,现在前端获取数据,和保存数据都写好了,就可以愉快的交给项目组的人用了,但是咱们还是得精益求精把登录也写出来
前端登录功能
- 1:在template中新增login.html代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录</title>
<!-- 引入 Bootstrap CSS -->
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'css/bootstrap.min.css' %}">
</head>
<body>
<div class="container d-flex justify-content-center align-items-center vh-100">
<div class="card shadow" style="width: 25rem;">
<div class="card-header bg-primary text-white text-center">
<h3>用户登录</h3>
</div>
<div class="card-body">
{% if messages %}
<div class="alert alert-danger">
{% for message in messages %}
{{ message }}
{% endfor %}
</div>
{% endif %}
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">登录</button>
</div>
</form>
</div>
{# <div class="card-footer text-center">#}
{# <a href="#" class="text-decoration-none">忘记密码?</a>#}
{# </div>#}
</div>
</div>
<!-- 引入 Bootstrap JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
- 2:在子应用中新增models文件,新增以下代码
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class User(AbstractUser):
'''自定义用户模型类'''
# 其他自定义字段...
class Meta:
#自定义表名
db_table = 'tb_user'
verbose_name = '用户'
verbose_name_plural = verbose_name
def __str__(self):
return self.username
- 3:执行python命令行
python manage.py makemigrations
python manage.py migrate
- 4:setting中新增
AUTHENTICATION_BACKENDS = ['myapp.utils.UsernameMobileBackend']
- 5:在子应用utils文件中新增
def get_user_by_account(account):
try:
user = User.objects.get(username=account)
if user:
return user
else:
return None
except User.DoesNotExist:
return None
class UsernameMobileBackend(ModelBackend):
'''自定义用户认证后端'''
def authenticate(self, request, username=None, password=None, **kwargs):
if request is not None:
# 使用账号查询用户
user = get_user_by_account(username)
# 如果可以查询到用户,还需要效验密码是否正确
if user and user.check_password(password):
# 返回user
return user
else:
return None
else:
return None
- 6:在路由中新增
path('login', views.Login.as_view(),name='login'),
- 7:编写路由函数
class Login(View):
def get(self,request):
return render(request,'login.html')
'''用户注册'''
def post(self,request):
'''提供用户注册页面'''
username = request.POST.get('username')
password = request.POST.get('password')
user = authenticate(request=request, username=username, password=password)
if user is None:
messages.error(request, '用户名或密码错误,请重试。')
return render(request, 'login.html')
login(request, user)
request.session.set_expiry(None)
response = redirect("index")
response.set_cookie('username', username, max_age=86400)
return response
-
8:经过以上编写,启动项目访问login路径你可以得到
-
注册函数自己写
-
访问index页面你可以得到,合并按钮和保存一致,可以不管