Django+Vue自动化测试平台实战:前后端分离与Celery异步任务设计
简介一份基于Django与Vue前后端分离的自动化测试平台项目源码面向Python开发者、测试工程师及全栈项目学习者可用于理解自动化测试流程、前后端通信、接口调用与平台模块设计。压缩包共2000个文件其中1694个py文件实现后端逻辑与测试用例123个html、92个js和15个css构成前端页面64个txt与6个md文档提供部署说明、配置指引和项目介绍整体仅21.2MB轻量易下载目录结构清晰便于按模块查阅。项目已经过运行验证配套部署文档齐全数据库等配置可按提示替换适合小白直接上手也能帮助有基础的读者快速掌握前后端分离项目的工程结构、接口测试平台开发思路与部署要点。目前已有119人学习下载适合用于毕业设计、自动化测试平台二次开发或作为Django与Vue技术栈进阶的实战参考。1. 一套DjangoVue自动化测试平台先想清楚这两个问题原本以为测试平台就是把Excel里的用例搬到网页上跑通后才发现真正难的是任务怎么异步跑、报告怎么结构化。这个基于DjangoVue前后端分离的自动化测试平台后端用Django提供API前端用Vue做单页应用能解决用例管理、任务调度、结果展示三块主要问题。适合测开工程师做二次开发也适合Python学习者练习前后端分离项目实战你先按部署文档跑通环境再看代码怎么串起来的。项目源码里的静态资源、Django配置和部署说明都齐全替换数据库配置就能用。不过开始前有两个问题要先想清楚一是用例执行不能靠同步请求硬扛要引入消息队列二是前后端分离后的token认证要放在请求拦截器里统一处理。下面先从架构选型说起。2. 前后端分离架构选型与Django API环境搭建2.1 为什么把Django模板替掉改用Vue SPA很多从Django模板迁过来的项目第一反应是在views.py里render一个html再靠jQuery发ajax。这个方案在小场景里能跑但页面越加越多static/js里的脚本会变成一团乱麻。测试平台更需要像数据可视化报表、步骤编排、实时任务状态这类强交互界面Vue的单文件组件能把模板、脚本、样式收拢在一个文件里维护成本明显低。另一个原因是多端复用后端只输出JSON测试任务可以通过接口被CI系统调用也可以被内部工具调用而不用在Django里再写一套模板。这就是典型的Django前后端分离架构。前端独立打包成静态文件放在nginx或其他Web服务器上后端专注处理/api下的请求。因此后端会出现跨域配置、JWT认证、序列化器校验这些原本不用关心的问题。我会在2.2节把环境搭起来并对关键配置逐项说明。2.2 Python 3.7环境与Django项目初始化这个项目要求python3.7及以上版本我先在虚拟环境里装依赖。以服务器为例常见做法是python3.7 -m venv venv source venv/bin/activate pip install django djangorestframework django-cors-headers mysqlclient pip install celery redis pymysql django-admin startproject test_platform cd test_platform python manage.py startapp apipython3.7 -m venv用于创建干净虚拟环境避免和系统python装冲突django-admin startproject生成项目根目录startapp生成api应用后续模型和视图都写在这个目录下。如果项目里用了MySQL需要安装mysqlclient或者改用pymysql并写兼容代码。部署文档里一般会写明具体依赖以源码为准。开发前端时Vue项目我一般放在与test_platform同级的frontend目录npm create vuelatest frontend cd frontend npm install npm run dev提示如果源码是基于Vue2与element-ui的不要直接使用Vue3模版vue-router和element-plus的写法差别较大建议先查看package.json确认版本再决定是否重装依赖。这里也涉及Vue安装及环境配置的问题。Vue3默认使用Vite构建Vue2则用webpack两者对node版本要求不同。源码里若已有package-lock.json直接npm install可以避免依赖版本漂移。2.3 settings.py里的API配置与JWT认证前后端分离后Django默认的session认证在跨域场景下会踩很多坑。项目基本都会选择JWT。这里给出一个典型的settings配置节选# test_platform/settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, corsheaders, django_filters, api, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, django.middleware.common.CommonMiddleware, # Django自带中间件省略 ] CORS_ALLOWED_ORIGINS [ http://localhost:5173, https://test.example.com, ] REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], DEFAULT_FILTER_BACKENDS: [ django_filters.rest_framework.DjangoFilterBackend, ], DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination, PAGE_SIZE: 10, }注意CORS_ALLOWED_ORIGINS必须与前端实际访问域名一致开发时是http://localhost:5173上线后要换成域名否则浏览器控制台会报CORS error。JWT认证是一种无状态token方案服务端不保存登录态API层每次从Authorization头解析Bearer token适合Vue单页应用。DEFAULT_FILTER_BACKENDS必须显式配置这样才能在列表接口用?request_typeGET做筛选。接下来是数据库与Redis配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: test_platform, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } } CELERY_BROKER_URL redis://127.0.0.1:6379/0 CELERY_RESULT_BACKEND redis://127.0.0.1:6379/1CELERY_BROKER_URL是任务队列提交的任务先进入Redisworker再拉取执行。执行结果写回CELERY_RESULT_BACKEND。为什么不用Django自带的同步任务因为一个测试任务可能要跑几十个用例每个用例要做HTTP请求、断言、超时控制同步执行会让前端一直等待极易超时。引入Celery后接口只负责创建任务记录并返回任务ID真正的执行放到后台。配置项作用常见错误CORS_ALLOWED_ORIGINS控制允许的前端域名未配置导致浏览器跨域DATABASES持久化用例、任务、报告MySQL编码不是utf8导致乱码CELERY_BROKER_URL任务消息队列地址Redis未启动导致发任务报错PAGE_SIZE控制列表返回条数前端一页拿不到全部数据到这里后端骨架已经能启动。下一章进入核心业务实现。3. 测试用例模型、接口视图与Celery任务的后端实现3.1 用例、任务、报告三层模型怎么拆自动化测试平台的数据模型我习惯拆成三层TestCase保存单条接口用例TestTask保存一次执行任务TestTask和TestCase是多对多关系执行之后生成的结果再以JSON快照形式写到TestTask.report字段。这样做的好处是任务执行时用例可能被修改但历史报告要保持原样所以报告里不要直接关联TestCase的实时引用而是把结果序列化成独立JSON。下面是一个典型的models.py实际项目还会增加环境配置、断言列表等字段这里保留主结构from django.db import models from django.contrib.auth.models import User class TestCase(models.Model): METHOD_CHOICES [(GET, GET), (POST, POST), (PUT, PUT), (DELETE, DELETE)] name models.CharField(max_length200, verbose_name用例名称) request_type models.CharField(max_length10, choicesMETHOD_CHOICES) url models.CharField(max_length500, verbose_name请求地址) headers models.TextField(default{}, verbose_name请求头JSON) body models.TextField(default{}, verbose_name请求体JSON) expected_code models.IntegerField(default200, verbose_name期望状态码) timeout models.IntegerField(default10, help_text单位秒) created_by models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue) def __str__(self): return self.name class TestTask(models.Model): STATUS_CHOICES [ (pending, 排队中), (running, 执行中), (done, 已完成), (failed, 失败), ] name models.CharField(max_length200, verbose_name任务名称) cases models.ManyToManyField(TestCase, blankTrue) status models.CharField(max_length10, choicesSTATUS_CHOICES, defaultpending) report models.TextField(blankTrue, default) created_by models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue) def __str__(self): return self.nameheaders和body直接用TextField存JSON串而不是JSONField是为了兼容MySQL5.7同时减少迁移风险。expected_code做状态码断言timeout防止请求卡死。created_by用SET_NULL保证用户删除后历史数据不丢失。实际序列化时要把这些字段转成Python字典交给serializers处理。3.2 用ModelViewSet快速生成查询APIDjango REST Framework的ModelViewSet把查询、删除、新建都封装好了。测试平台常见的API包括case列表、task列表、task执行。先用serializers定义字段from rest_framework import serializers from .models import TestCase, TestTask class TestCaseSerializer(serializers.ModelSerializer): created_by_name serializers.CharField( sourcecreated_by.username, read_onlyTrue) class Meta: model TestCase fields [id, name, request_type, url, headers, body, expected_code, timeout, created_by, created_by_name] extra_kwargs { created_by: {read_only: True} }created_by_name是把User对象的username平铺到接口返回前端表格直接显示不用再发一次用户查询。created_by设置read_only确保用户不能伪造创建人只能由后端在perform_create里写入。接下来是视图集from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response from .models import TestCase, TestTask from .serializers import TestCaseSerializer, TestTaskSerializer from .tasks import execute_task class TestCaseViewSet(viewsets.ModelViewSet): queryset TestCase.objects.all() serializer_class TestCaseSerializer filterset_fields [request_type, expected_code] def perform_create(self, serializer): serializer.save(created_byself.request.user) class TestTaskViewSet(viewsets.ModelViewSet): queryset TestTask.objects.all() serializer_class TestTaskSerializer def perform_create(self, serializer): task serializer.save(created_byself.request.user) execute_task.delay(task.id)filterset_fields对应接口/api/v1/cases/?request_typePOSTDRF会自动过滤。execute_task.delay(task.id)把任务Id丢进Celery队列接口立刻返回前端轮询task状态即可。需要注意delay传的是task.id不要在worker里再去查询对象避免跨进程序列化错误。再在urls.py里注册路由from rest_framework.routers import DefaultRouter from .views import TestCaseViewSet, TestTaskViewSet router DefaultRouter() router.register(rcases, TestCaseViewSet, basenamecase) router.register(rtasks, TestTaskViewSet, basenametask) urlpatterns router.urls这样ModelViewSet自动生成list、create、retrieve、update、partial_update、destroy六个接口。比如GET /api/v1/cases/5/查询单条用例PATCH /api/v1/tasks/3/局部更新任务状态。DRF的Browsable API可以直接在浏览器里调试这对接口调试很实用。方法URL说明GET/api/v1/cases/用例列表支持筛选POST/api/v1/cases/创建用例GET/api/v1/tasks/任务列表POST/api/v1/tasks/创建任务并触发执行GET/api/v1/tasks/{id}/查询任务状态和报告3.3 执行逻辑与结果解析要避免的坑常见的测试任务执行单元我用celery shared_task写遍历用例做请求并断言import json import requests from celery import shared_task from .models import TestCase, TestTask shared_task def execute_task(task_id): task TestTask.objects.select_related(created_by).get(idtask_id) task.status running task.save(update_fields[status]) results [] for case in task.cases.all(): record {case_id: case.id, case_name: case.name} try: headers json.loads(case.headers or {}) body json.loads(case.body or {}) resp requests.request( case.request_type, case.url, headersheaders, jsonbody, timeoutcase.timeout ) record[status_code] resp.status_code record[success] resp.status_code case.expected_code record[response_body] resp.text[:500] except Exception as exc: record[status_code] None record[success] False record[error] str(exc) results.append(record) task.report json.dumps(results, ensure_asciiFalse) task.status done task.save(update_fields[report, status])细节在于json.loads(case.headers or {})做了空值兜底如果数据库里存了空字符串不会抛异常。resp.text[:500]截断了响应体避免大响应体撑爆Redis但截断会丢信息我建议只在预览列表里展示完整报文还要落文件或对象存储。另一个坑是task.status done无论用例是否全通过都算done只有代码抛未捕获异常才算failed。业务上这是“执行完成”和“执行失败”两个维度不要把单条用例失败当成整个任务失败否则报告没法生成。如果用户删了一条用例已经建好的任务关联关系会留下脏数据。我一般会加一个post_delete信号在用例删除后清掉多对多关联from django.db.models.signals import post_delete from django.dispatch import receiver from .models import TestCase, TestTask receiver(post_delete, senderTestCase) def remove_case_from_tasks(sender, instance, **kwargs): TestTask.cases.through.objects.filter(testcase_idinstance.id).delete()Django的post_delete信号在Model.delete()之后触发这里直接操作through表删除关联记录避免遍历所有任务。测试报告已经存成JSON快照所以清理关联不会影响历史报告。4. Vue前端对接axios请求封装、token处理与页面路由4.1 SPA路由与页面布局前端部分不能只处理接口核心是路由设计与token拦截。Vue Router的history模式去掉了#看起来更像真实站点但需要一个关键配置nginx必须把未匹配路径try_files指向index.html否则刷新页面会404。路由上把需要登录的页面统一加meta.requiresAuth。// frontend/src/router/index.js import { createRouter, createWebHistory } from vue-router import TaskList from ../views/TaskList.vue import Login from ../views/Login.vue const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: Login }, { path: /tasks, component: TaskList, meta: { requiresAuth: true } } ] }) router.beforeEach((to) { const token localStorage.getItem(access_token) if (to.meta.requiresAuth !token) { return { path: /login, query: { redirect: to.fullPath } } } return true }) export default routerto.fullPath作为query参数传递用户登录成功后会跳回原始页面而不是固定首页这个细节在测试平台里很实用因为测开经常直接收藏某个用例详情页。query参数也是Vue路由参数里最常用的一种传参方式刷新后不会像params那样丢值。如果有人问你Vue面试题里的路由参数问题优先答query和params在刷新场景下的差异。4.2 axios拦截器实现token自动携带与401处理Token处理是前后端分离最容易翻车的地方。每个请求都手写Authorization头会累死人而且容易漏。封装在utils/request.js里// frontend/src/utils/request.js import axios from axios const service axios.create({ baseURL: /api/v1, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(access_token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error Promise.reject(error)) service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(access_token) localStorage.removeItem(refresh_token) window.location.href /login?redirect encodeURIComponent(window.location.pathname) } return Promise.reject(error) } ) export default servicebaseURL: /api/v1看起来奇怪生产环境前端与API同域部署这样的相对路径能直接交给nginx的/api/转发。如果前端和后端不同域再把baseURL改成环境变量。401处理逻辑里一定要同时清掉access_token与refresh_token否则用户点登录页后又被旧refresh_token回刷成新token出现“明明登出又自动登录”的问题。这里示例是简单实现如果源码里用了simplejwt的refresh机制还需要在401时尝试刷新token后再重放一次请求。配合axios封装给接口单独建一个模块// frontend/src/api/task.js import request from ../utils/request export function getTasks(params) { return request.get(/tasks/, { params }) } export function createTask(data) { return request.post(/tasks/, data) } export function getTaskReport(id) { return request.get(/tasks/${id}/report/) }这段代码的作用是把所有URL集中在api目录URL拼写错误只影响一个文件。前后端分离项目实战中常见的错误用法是在每个组件里直接写axios.get(http://localhost:8000/api/v1/tasks/)这样开发时能通部署时换个域名就要全局搜索替换。用api模块统一管理后只需要改request.js里的baseURL。4.3 用例列表页的数据加载与轮询在任务列表页创建任务后需要知道执行状态。最简单的方案是在模板里用setInterval轮询import { ref, onMounted, onUnmounted } from vue import { getTasks, getTaskReport } from ../api/task const tasks ref([]) let timer null const loadTasks async () { const data await getTasks({ page: 1 }) tasks.value data.results || [] const runningTask tasks.value.find(task task.status running) if (runningTask) { startPolling() } else { stopPolling() } } const startPolling () { if (timer) return timer setInterval(loadTasks, 3000) } const stopPolling () { if (timer) { clearInterval(timer) timer null } } onMounted(loadTasks) onUnmounted(stopPolling)轮询间隔3秒比较折中。太短会在任务多时打爆后端太长则UI响应慢。如果后端支持WebSocket或SSE可以改成推送但这份源码里能稳定复现的仍是轮询先跑通再优化。任务执行中的行要禁用删除按钮否则后台worker还在跑前端已经把任务删了worker完成后写一个不存在的记录会抛异常。字段位置作用access_tokenlocalStorage每个请求的AuthorizationbaseURLutils/request.js统一API前缀meta.requiresAuth路由meta控制页面访问权限statustask记录驱动轮询开关5. NginxuWSGI部署后的验证与常见排错5.1 生产构建与nginx路由生产环境部署前后端分离项目我见过最多的失败不是代码问题而是nginx路由和静态文件配置。前端build之后dist目录直接交给nginxDjango只保留/api路径。nginx核心配置server { listen 80; server_name test.example.com; client_max_body_size 20m; location / { root /opt/test-platform/frontend/dist; try_files $uri $uri/ /index.html; } location /api/ { include uwsgi_params; uwsgi_pass 127.0.0.1:8000; } location /static/ { alias /opt/test-platform/static/; expires 30d; } }try_files $uri $uri/ /index.html保证前端路由刷新不404这是history模式必须的。uwsgi_pass把/api请求交给Django应用服务器。client_max_body_size要放大否则导入大用例文件会返回413。5.2 uWSGI启动与健康检查接着启动uwsgiuwsgi --http :8000 --module test_platform.wsgi \ --static-map /static/opt/test-platform/static \ --processes 2 --threads 4 --master true--http适用于临时调试生产更建议用--socket配合nginx的uwsgi_pass减少一次HTTP解析开销。--processes 2 --threads 4表示两个worker进程、每进程4线程。线程模型在Django和Celery配合时要注意不要在多个线程里共享同一个Redis连接。5.3 用脚本验证部署与排错表验证部署是否正常写一个简单的健康检查脚本import requests base_url http://127.0.0.1:8000/api/v1 login_data {username: admin, password: admin123} response requests.post(base_url /token/, jsonlogin_data) if response.status_code ! 200: print(token接口失败) exit(1) access response.json()[access] headers {Authorization: fBearer {access}} resp requests.get(base_url /cases/, headersheaders) print(cases接口状态码:, resp.status_code) assert resp.status_code 200第一行请求token接口验证认证链路第二行带token查用例列表验证数据库和权限配置。这里如果项目用的不是simplejwt默认路径需要对照源码里的urls.py调整。症状排查方向页面能打开但接口404nginx里/api/location是否配置正确前端刷新后404try_files配置缺失接口返回CORS error后端CORS_ALLOWED_ORIGINS未包含当前域名任务一直pendingredis是否启动celery worker是否运行登录后仍跳loginlocalStorage里的token过期或前端拦截器未刷新token最后一个技巧查看任务一直pending时不要只盯celery日志先看Redis队列长度。执行redis-cli llen celery如果队列有积压说明worker没有消费如果队列为空但任务状态还是pending说明任务投递时id和库里的主键不一致检查execute_task.delay(task.id)里传的是不是int而不是ORM对象。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →