技术基线:Django 5.x LTS(2026)、FastAPI 0.115+、Vue 3.4+、Vite 5、Ant Design Vue 4.x、Kubernetes 1.30+、Allure 2.30+。
测试平台(Test Platform)是一套将"用例管理、任务调度、执行引擎、报告展示、数据治理、环境管理"统一收口的中枢系统。它不是某个测试框架的 Web 壳,而是一组面向测试生命周期的服务集合:上游对接需求与代码仓库,下游对接 CI/CD 与可观测性体系,把零散的脚本、工具、人工流程沉淀为可复用、可度量、可审计的工程能力。
业内常见两条路径:
判断清单(满足 3 项以上再考虑自建):
切忌"为了平台而平台"。重复造轮子是测试平台领域最大的坑。
| 模块 | 核心能力 |
|---|---|
| 用例管理 | CRUD、标签、版本、用例评审、与需求双向追溯 |
| 任务调度 | 手动触发、Cron 定时、CI Webhook 触发、并发与依赖编排 |
| 执行引擎 | JMeter/k6/Selenium/pytest/Playwright 适配器,统一执行协议 |
| 测试报告 | Allure/HTML/PDF、实时日志推送、历史趋势对比 |
| 数据管理 | 数据池、数据工厂(Factory Boy/Faker)、数据隔离与回收 |
| 环境管理 | 多环境配置、环境隔离、Mock 服务、环境快照 |
| 权限与审计 | RBAC、操作审计、SSO 集成、数据脱敏 |
现代测试平台普遍采用"前后端分离 + 执行层独立 + 调度层下沉"的三段式架构:
不要一开始就拆成 20 个微服务。推荐按 能力域 拆分三服务起步:platform-api(FastAPI,业务 API)、platform-admin(Django,后台与权限)、platform-scheduler(Celery worker,调度执行)。当某服务出现独立扩缩容诉求时再进一步拆分,避免过早分布式化带来的运维负担。
test-runner,资源配额隔离防止压测打挂平台自身;alpine + python3.12 + jdk17),按工具分层缓存;| 框架 | 承担职责 | 选型理由 |
|---|---|---|
| FastAPI 0.115+ | 对外 REST、WebSocket、依赖注入、Pydantic 校验 | 异步高性能、OpenAPI 自动生成、与 async ORM 契合 |
| Django 5.x | Admin、ORM 模型、RBAC、SSO、定时任务 | Admin 开箱即用、ORM 成熟、生态稳定 |
两者通过共享 PostgreSQL 与 Redis 协作,FastAPI 不直接写库业务对象,复杂查询走 Django 暴露的内部 HTTP/RPC 接口。
# app/api/cases.py —— 测试用例路由
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.deps import get_db, require_roles
from app.schemas import CaseCreate, CaseOut
router = APIRouter(prefix="/api/v1/cases", tags=["用例管理"])
@router.post("", response_model=CaseOut, summary="创建测试用例")
async def create_case(
payload: CaseCreate,
db: AsyncSession = Depends(get_db),
_user = Depends(require_roles("qa", "admin")), # 角色鉴权依赖注入
):
"""新建用例并写入版本快照,tags 自动去重。"""
case = await CaseService(db).create(payload, creator=_user.id)
if not case:
raise HTTPException(409, "同项目下用例编号已存在")
return case依赖链 get_db → require_roles 让鉴权与事务边界清晰可测,Pydantic v2 的 CaseCreate 自动生成 OpenAPI 文档,前端可零成本对齐。
# platform_admin/cases/models.py —— Django 模型,与 FastAPI 共享同一库
from django.db import models
from django.contrib.auth.models import User
class TestCase(models.Model):
"""测试用例:与 FastAPI 侧 CaseOut 字段一一对应。"""
title = models.CharField("用例标题", max_length=200, db_index=True)
project = models.ForeignKey("Project", on_delete=models.CASCADE, related_name="cases")
tags = models.JSONField("标签", default=list) # 5.x JSONField 原生支持
version = models.PositiveIntegerField("版本号", default=1)
creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [models.Index(fields=["project", "version"])]
verbose_name = "测试用例"
# admin.py —— 一行注册即可获得完整 CRUD + 过滤 + 搜索 + 导出
from django.contrib import admin
@admin.register(TestCase)
class TestCaseAdmin(admin.ModelAdmin):
list_display = ("id", "title", "project", "version", "creator", "created_at")
list_filter = ("project", "tags", "created_at")
search_fields = ("title",)Django 5.x 的 JSONField 默认走 jsonb,标签查询性能远优于旧版字符串拼接;Admin 配合 django-import-export 可直接对用例做批量 Excel 导入导出。
<!-- src/views/cases/CaseList.vue —— 用例管理列表页 -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import type { TableColumnsType } from 'ant-design-vue'
import { api } from '@/api' // Axios 实例,自动携带 JWT
interface CaseItem { id: number; title: string; tags: string[]; version: number }
const columns: TableColumnsType<CaseItem> = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{ title: '用例标题', dataIndex: 'title', ellipsis: true },
{ title: '标签', dataIndex: 'tags', key: 'tags' },
{ title: '版本', dataIndex: 'version', width: 80 },
]
const data = ref<CaseItem[]>([])
const loading = ref(false)
async function fetchData() { // 拉取用例列表
loading.value = true
try {
const res = await api.get('/api/v1/cases')
data.value = res.data.items
} catch (e) {
message.error('用例加载失败,请检查网络或权限')
} finally {
loading.value = false
}
}
onMounted(fetchData)
</script>
<template>
<a-table :columns="columns" :data-source="data" :loading="loading" row-key="id">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'tags'">
<a-tag v-for="t in record.tags" :key="t" color="blue">{{ t }}</a-tag>
</template>
</template>
</a-table>
</template>要点:组合式 API 把数据获取与 UI 解耦,Antd 4 的 bodyCell 插槽替代旧版 customRender,配合 Vite 5 的按需引入,首屏包体积可控制在 220KB(gzip)以内。
不同引擎的输入输出差异巨大,平台需定义统一执行协议(Execution Spec)屏蔽差异:
| 字段 | 含义 |
|---|---|
engine | jmeter / k6 / selenium / pytest |
entry | 脚本路径或 JMX 文件 |
env | 目标环境标识,注入到容器 ENV |
params | 参数化变量(数据驱动) |
report | 报告格式:allure / junit-xml / html |
resource | CPU/内存/副本数 |
# app/scheduler/k8s_job.py —— 通过 K8s API 调度测试任务为 Job
from kubernetes import client
from kubernetes.client import V1Job, V1JobSpec, V1ObjectMeta, V1PodTemplateSpec, V1Container
def build_test_job(task_id: str, spec: dict) -> V1Job:
"""构造 K8s Job:执行完即销毁,资源配额来自 spec.resource。"""
container = V1Container(
name="runner",
image=f"registry.internal/test-engines/{spec['engine']}:2026.08", # 镜像按引擎区分
command=["sh", "-c", spec["entry"]], # 入口命令
env=[{"name": k, "value": v} for k, v in spec["env"].items()], # 环境变量注入
resources={"requests": {"cpu": spec["resource"]["cpu"],
"memory": spec["resource"]["memory"]}},
)
return V1Job(
api_version="batch/v1", kind="Job",
metadata=V1ObjectMeta(name=f"test-{task_id}", labels={"task-id": task_id}),
spec=V1JobSpec(
backoff_limit=0, # 失败不重试,由平台决策
ttl_seconds_after_finished=3600, # 1 小时后自动清理 Pod
template=V1PodTemplateSpec(restart_policy="Never", containers=[container]),
),
)K8s Job 模式带来三个关键收益:资源隔离(压测不会挤占平台 Pod)、弹性扩缩(HPA 按队列深度横向扩 runner)、审计可追溯(每个任务对应独立 Pod,日志归档到 Loki)。
# app/api/ws.py —— FastAPI WebSocket 实时推送
from fastapi import APIRouter, WebSocket
from redis.asyncio import Redis
router = APIRouter()
@router.websocket("/ws/tasks/{task_id}")
async def task_ws(ws: WebSocket, task_id: str):
"""订阅 Redis Pub/Sub,把 runner 的事件流推给前端。"""
await ws.accept()
redis = Redis.from_url("redis://redis:6379/1")
pubsub = redis.pubsub()
await pubsub.subscribe(f"task:{task_id}:events")
try:
async for msg in pubsub.listen():
if msg["type"] == "message":
await ws.send_text(msg["data"].decode()) # 透传日志/进度
finally:
await pubsub.unsubscribe(f"task:{task_id}:events")
await ws.close()Runner Pod 内通过 redis-cli publish task:{id}:events '<event json>' 上报事件,WebSocket 端只做透传,避免业务逻辑阻塞推送链路。
Allure 历史报告需持久化。推荐方案:runner 把 allure-results 写入挂载的 PVC,独立 allure-server(基于 allure-framework/allure-server)扫描目录并生成趋势报告,前端通过 iframe 嵌入(注意:该开源项目已于 2025 年归档停止维护,选用前需评估替代方案)。注意:history 目录必须跨任务保留,否则趋势图失效。
代表产品:MeterPlus、Mabl、Testim。核心思路:用录制 + 可视化编排替代脚本编写,让业务测试与产品经理也能产出可执行用例。自建平台可借鉴"低代码编排层"——把 pytest/JMeter 用例抽象为节点,前端拖拽生成 DAG,后端编译为执行 spec。关键是保留代码逃生通道:低代码生成的 spec 可一键导出为 pytest 脚本,避免低代码平台成为新的供应商锁定。
2024-2026 最显著的变量是 LLM 介入测试平台。三个成熟落地场景:
注意:AI 生成结果必须有人工 review 闸门,未经确认的用例不能进入生产回归集。
MeterPlus、Apifox、Datadog Synthetics 等持续吞噬中小团队的自建预算。SaaS 化的反面是数据合规与定制化受限,2025 年后出现"混合模式":核心测试资产在内网自建平台,边缘场景(如真机云、海外拨测节点)走 SaaS,通过统一 API 网关聚合。
测试平台不再独立存在,而是作为 IDP 的一个插件/能力接入 Backstage、Port、Cortex 等。典型集成点:
test-plugin 面板直接展示服务健康度、回归通过率;版本说明:本文技术基线为 2026-08,Django 5.x LTS、FastAPI 0.115+、Vue 3.4+、Vite 5、Ant Design Vue 4、Kubernetes 1.30+、Allure 2.30+。