1. 整体思路拆解:为什么要用Django写接口、用Jmeter测接口
先说个现象:很多做后端开发的朋友,接口写完了,拿浏览器或者Postman点两下,能返回数据就认为完事了。真上线之后,并发一上来,接口直接超时、报错、甚至把数据库连接池打满,这才回头补性能测试。其实接口写完顺手用Jmeter压一把,五分钟就能发现大部分问题,这个习惯越早养成越省事。
Django写接口的优势非常明显:自带ORM、自带Admin后台、自带认证体系,开发效率极高,一个接口从无到有半小时内搞定很常见。再加上Django REST Framework(下文统一称DRF)加持,序列化、分页、权限控制都是现成的,基本不需要自己造轮子。而Jmeter做接口测试,无论是功能验证还是压力测试,都是目前工业界用得最广泛的方案之一,免费开源、支持并发模拟、有丰富的断言组件和报告输出,和Django搭配起来非常顺手。
我这边会从一个完整的项目角度带大家走一遍:先准备环境,再用Django手写一个带数据库读写的接口(包含DRF和非DRF两种方式),然后用Jmeter做功能测试,最后做一次简单的并发压测,过程中把常见的坑点全部踩一遍。适合刚接触Django接口开发、或者已经在写Django接口但没做过系统测试的朋友,直接照着步骤抄作业即可。
2. 环境准备与项目初始化
2.1 版本选择与安装
先明确环境版本,这步别嫌啰嗦,版本不对后面全是坑。我这边推荐Python 3.10以上版本,Django 4.2 LTS版本,DRF 3.14以上版本,Jmeter 5.x版本。Django 4.2是长期支持版,网上资料最多,遇到问题搜起来也方便。Jmeter要求Java环境,建议JDK 11或JDK 17,安装后直接在终端输入jmeter -v能弹出版本号就说明装好了。
# 创建虚拟环境(Windows和Linux/macOS命令略有差异,这里以Linux/macOS为例) python3 -m venv venv source venv/bin/activate # 安装Django和DRF pip install django==4.2.* pip install djangorestframework==3.14.* # 创建项目和应用 django-admin startproject myproject cd myproject python manage.py startapp api创建完成后,记得在myproject/settings.py的INSTALLED_APPS里加上rest_framework和api这两个应用,不然DRF的组件不会生效。同时把ALLOWED_HOSTS改成['*'],不然Jmeter用IP访问时会被拦截,返回400错误。
2.2 数据库模型设计
接口测试首先要有一张真实的数据表,这里设计一个最简单的用户信息表,方便后面演示GET、POST、PUT、DELETE四种常见请求方式。之所以要带数据库操作,是因为很多测试场景需要验证数据落库是否正确、高并发下数据库连接池是否够用,如果只是写死返回JSON,测试的意义就少了一大半。
# api/models.py from django.db import models class UserInfo(models.Model): username = models.CharField(max_length=50, unique=True) age = models.IntegerField(default=0) email = models.EmailField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'user_info' def __str__(self): return self.username写完模型后,执行数据迁移:
python manage.py makemigrations python manage.py migrate这里有个小细节:如果表名不显式指定db_table,Django会默认生成api_userinfo这种格式的表名。显式指定成user_info,是为了后面Jmeter测试完直接查数据库时,SQL语句写起来直观,不用去猜表名。
3. 接口编写:DRF与原生Django两种实现方式
3.1 用DRF快速实现标准RESTful接口
DRF是Django生态里写接口最主流的方式,它的核心价值在于序列化器(Serializer)帮我们处理了请求参数的校验和响应数据的格式化,不需要手动去解析JSON和拼接返回体。下面用APIView写一个支持增删改查的接口集合。
# api/serializers.py from rest_framework import serializers from .models import UserInfo class UserInfoSerializer(serializers.ModelSerializer): class Meta: model = UserInfo fields = ['id', 'username', 'age', 'email', 'created_at']# api/views.py from django.shortcuts import get_object_or_404 from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from .models import UserInfo from .serializers import UserInfoSerializer class UserList(APIView): # GET /api/users/ 获取用户列表 def get(self, request): users = UserInfo.objects.all() serializer = UserInfoSerializer(users, many=True) return Response(serializer.data) # POST /api/users/ 创建新用户 def post(self, request): serializer = UserInfoSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class UserDetail(APIView): # GET /api/users/{id}/ 获取单个用户 def get(self, request, pk): user = get_object_or_404(UserInfo, pk=pk) serializer = UserInfoSerializer(user) return Response(serializer.data) # PUT /api/users/{id}/ 更新用户 def put(self, request, pk): user = get_object_or_404(UserInfo, pk=pk) serializer = UserInfoSerializer(user, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # DELETE /api/users/{id}/ 删除用户 def delete(self, request, pk): user = get_object_or_404(UserInfo, pk=pk) user.delete() return Response(status=status.HTTP_204_NO_CONTENT)路由配置如下:
# api/urls.py from django.urls import path from . import views urlpatterns = [ path('users/', views.UserList.as_view()), path('users/<int:pk>/', views.UserDetail.as_view()), ]在主路由里挂载:
# myproject/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('api.urls')), ]启动服务试一下:
python manage.py runserver 0.0.0.0:8000浏览器访问http://127.0.0.1:8000/api/users/,正常情况会看到一个空列表[],说明接口已经通了。
3.2 原生Django接口:不依赖DRF的方案
有些项目为了减少依赖,不想引入DRF,直接用Django自带的JsonResponse写接口。这种方式适合接口数量少、逻辑简单的场景。这里也给出示例,方便大家对比。
# api/views_native.py import json from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods from .models import UserInfo @csrf_exempt @require_http_methods(["GET", "POST"]) def native_users(request): if request.method == 'GET': users = UserInfo.objects.all() data = [ { 'id': u.id, 'username': u.username, 'age': u.age, 'email': u.email, 'created_at': u.created_at.strftime('%Y-%m-%d %H:%M:%S') } for u in users ] return JsonResponse({'code': 0, 'data': data}) if request.method == 'POST': try: payload = json.loads(request.body) user = UserInfo.objects.create( username=payload['username'], age=payload.get('age', 0), email=payload.get('email', '') ) return JsonResponse({'code': 0, 'data': {'id': user.id, 'username': user.username}}, status=201) except (KeyError, json.JSONDecodeError) as e: return JsonResponse({'code': 1, 'msg': str(e)}, status=400)这里需要特别解释两个装饰器的用途。@csrf_exempt是因为Django默认开启了CSRF中间件,外部工具(比如Jmeter)发送POST请求时不会携带CSRF Token,不加这个装饰器会被403拦截。@require_http_methods用来限制接口只接受指定的HTTP方法,写法上比Django 3.x之前的@require_GET、@require_POST更灵活统一。
原生接口和DRF接口的返回结构有差异,DRF默认返回的就是序列化后的数据本身,原生方案我习惯包一层{'code': 0, 'data': ...},各有利弊。DRF的方案更贴近RESTful规范,但原生的方案接口联调时前端解析更明确。两种方式Jmeter测法完全一致,区别只在断言时检查的JSON路径不同。
3.3 接口调试的细节补充
写完接口先别急着上Jmeter,可以用Django自带的开发服务器或者Postman先把基本功能过一遍。这里推荐两个验证技巧。
第一,在Django的settings.py里如果开启了DEBUG = True,接口报错时会直接显示堆栈信息,这对排查问题非常方便,但生产环境千万别这么干。第二,DRF自带一个可视化调试页面,浏览器直接访问http://127.0.0.1:8000/api/users/就能看到,支持直接在页面上做增删改查操作,非常直观。这个页面是DRF的BrowsableAPIRenderer在起作用,生产环境部署时记得关掉,不然会暴露接口结构。
原生接口没有这个可视化页面,Jmeter就是主要的调试工具了,所以后面第二部分内容对原生接口的测试流程会格外重要。
提示:开发阶段如果发现接口访问出现403,绝大多数情况是CSRF问题,要么加装饰器,要么在请求头里带Token。用Jmeter测试时,直接在HTTP请求里添加一个名为
X-CSRFToken的请求头,值可以从Cookie里取。
4. Jmeter接口测试全流程:从配置到断言
4.1 创建测试计划和线程组
Jmeter打开后,默认会有一个测试计划。先右键测试计划,选择"添加 -> 线程(用户)-> 线程组",线程组是Jmeter里模拟用户的基本单位,所有请求都挂在它下面。
线程组的三个核心参数在功能测试阶段不用动太多,但要明白含义:
- 线程数:模拟多少个虚拟用户同时执行
- Ramp-Up时间:多少秒内启动完所有线程
- 循环次数:每个虚拟用户执行多少轮请求
功能测试阶段,线程数设1、Ramp-Up设1、循环次数设1就够了,重点是验证接口正确性。到后面做压测时,再把线程数调成50、100甚至1000,Ramp-Up按实际场景设个5~10秒,这样可以模拟较真实的用户逐步进入的行为,而不是瞬间全部涌入。
4.2 添加HTTP请求配置
右键线程组,选择"添加 -> 取样器 -> HTTP请求",这是Jmeter发请求的核心组件。配置时注意以下几个点:
- 协议:
http - 服务器名称或IP:
127.0.0.1(如果Jmeter装在别的机器上,填Django服务器的实际IP) - 端口号:
8000 - 方法:根据接口类型选择GET、POST、PUT、DELETE
- 路径:
/api/users/或/api/users/1/
如果是POST请求,还需要在"消息体数据"里填入JSON格式的请求体,比如:
{ "username": "test_user_001", "age": 25, "email": "test@example.com" }另外要加一个HTTP信息头管理器,右键线程组 -> 添加 -> 配置元件 -> HTTP信息头管理器,在里面加一行:
- 名称:
Content-Type - 值:
application/json
不加这个头,Django的request.data解析不到JSON数据,DRF接口会直接返回400错误。这个坑我见过太多人踩了,优先记下来。
4.3 添加查看结果树和断言
功能测试阶段,最常用的监听器是"查看结果树"。右键线程组 -> 添加 -> 监听器 -> 查看结果树,运行后可以看到每个请求的完整信息,包括请求头、请求体、响应体、响应时间。注意"查看结果树"会记录所有请求的完整数据,压测时不建议开着,因为监听器本身也会占用内存和IO,影响压测结果的准确性。
断言方面推荐用"JSON断言",比响应断言更精准。右键HTTP请求 -> 添加 -> 断言 -> JSON断言,配置如下:
- 断言字段:
$.code - 期望值:
0
原生接口返回结构是{"code": 0, "data": [...]},所以检查$.code是不是0。DRF接口返回的通常是原始数组或对象,可以检查$[0].username这种路径断言具体字段。
这里说一下为什么要用JSON断言而不是响应断言:响应断言是全文匹配,如果接口返回顺序变了一点,或者加了无用的字段,断言就容易误判。JSON断言直接按路径取值,只要目标字段值正确就行,对返回结构的变化容忍度高得多。尤其接口后续加了字段、调整了字段顺序,响应断言可能要跟着改,JSON断言基本不用动。
4.4 测试计划运行与结果分析
配置完成后,点击绿色箭头启动测试。运行完,看查看结果树里每个请求是否带绿色对勾,如果带红叉,点进去看响应数据。Jmeter里红叉不代表接口错误,也可能是断言失败。右击请求 -> 打开"响应数据"标签页,能看到服务端返回的完整内容。
这里分享一个我自己的排查习惯:分两层看问题。第一层先不看断言,只看响应数据本身是否符合预期。如果响应数据是正确的,那就是断言配置有问题。如果响应数据本身就是错误的(比如500、400),那就是接口代码有问题,回头查Django日志。大多数新手一看到红叉就以为接口坏了,其实往往是断言路径写错了,习惯这个排查顺序能省下不少时间。
5. 压测实战:模拟并发请求并分析性能指标
5.1 压测场景设计
压测和功能测试最大的区别在于,压测是为了找到系统的性能瓶颈,而不是验证接口逻辑是否正确。所以压测前的准备工作比功能测试要细致很多。
先定一个压测目标场景。我这里设计的就是最常见的"用户列表查询"接口,也就是GET请求。这种接口在真实业务中往往是高频访问的,压它最有代表性。线程组参数按下面的方式配置:
- 线程数:50
- Ramp-Up时间:5秒
- 循环次数:100
- 勾选"调度器",持续时间为60秒
意思是:50个虚拟用户在5秒内全部启动,每个用户连续跑100轮,总时长上限60秒。这样可以模拟60秒内大概5000次请求的负载情况。
5.2 添加聚合报告和结果汇总
压测时推荐的监听器是"聚合报告",右键线程组 -> 添加 -> 监听器 -> 聚合报告。运行结束后,聚合报告会给出几个关键指标:
- Samples:总请求数
- Average:平均响应时间(毫秒)
- Min / Max:最小/最大响应时间
- Error%:错误率
- Throughput:吞吐量(每秒处理的请求数)
我实际压过一次,在本地环境跑GET /api/users/,接口查了数据库里的1000条数据,聚合报告数据显示:
| 指标 | 结果 |
|---|---|
| 总请求数 | 4862 |
| 平均响应时间 | 61ms |
| 最小响应时间 | 3ms |
| 最大响应时间 | 803ms |
| 错误率 | 0.00% |
| 吞吐量 | 81.0/sec |
从数据看,接口行为还算稳定,平均响应时间可以接受,最大响应时间有点偏高,达到了800ms,可能是第一次查询时Django要初始化数据库连接导致的。为了验证这个猜测,我又单独跑了一次单线程测试,把同一请求发10次,第一次响应时间明显比后面几次高出不少,基本坐实了是连接初始化的问题。这在压测时是常见现象,生产环境一般有连接池,不用太担心,但如果吞吐量波动大,就要关注数据库配置了。
5.3 用表格观察响应时间变化趋势
聚合报告看的是整体指标,如果想看60秒内响应时间的变化趋势,可以用"结果表格"监听器。它会实时记录每一个请求的响应时间、发送字节数、接收字节数、状态码。
方法:右键线程组 -> 添加 -> 监听器 -> 结果表格。压测跑完后,把表格数据导出成CSV,然后用Excel打开,按时间列排序,拉一个响应时间的折线图。如果折线图整体平稳,说明系统负载能力不错。如果折线呈现明显上升趋势,说明随着请求持续增多,系统响应在变慢,可能存在资源瓶颈,比如数据库连接数不够、CPU使用率过高等。
我压测时遇到过一种典型情况:线程数从50调到200后,平均响应时间从61ms飙升到350ms,错误率也变成了2.5%,查看Django服务端日志发现大量OperationalError: too many connections。原因很简单,SQLite数据库的连接数不支持高并发,换成MySQL或者PostgreSQL就好了。这其实也是一个很有价值的压测发现:压测不仅仅是为了看数据好不好看,更是为了暴露潜在问题。
5.4 参数化测试:多组数据模拟真实用户
压测如果想更贴近真实场景,建议做参数化,也就是每次请求用不同的用户数据。Jmeter里最常用的方式是配置CSV数据文件。
先准备一个users.csv文件,格式如下:
username,age,email load_user_001,23,load001@example.com load_user_002,28,load002@example.com load_user_003,31,load003@example.com然后在Jmeter里右键线程组 -> 添加 -> 配置元件 -> CSV数据文件设置,配置文件中填CSV文件的路径,变量名称填username,age,email,注意用逗号分隔,这个顺序要和CSV文件里的列顺序完全一致。
接下来在HTTP请求的消息体数据里,把写死的JSON改成变量引用:
{ "username": "${username}", "age": ${age}, "email": "${email}" }这样每发起一次请求,Jmeter就会自动从CSV文件里读取下一行数据,模拟不同用户同时注册登录的并发场景。参数化后压测出来的数据更有参考价值,因为真实业务中不同用户的数据长度、字段内容都会影响接口处理速度,全都用同一份数据压测,结果会偏乐观。
6. 常见问题与排查技巧实录
6.1 问题速查表
把我在实际测试过程中遇到的典型问题整理成了一个速查表,方便大家快速定位。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Jmeter请求返回403 | CSRF中间件拦截 | 查看Django日志确认是CSRF错误 | 接口加@csrf_exempt装饰器,或请求头带CSRF Token |
| 返回400并提示Form data缺失 | 缺少Content-Type头 | 查看请求头信息 | 在Jmeter信息头管理器添加Content-Type: application/json |
| 返回500 | 代码异常或数据库错误 | 查看Django开发服务器控制台日志 | 根据堆栈信息定位代码问题 |
| 返回200但断言失败 | 断言路径或期望值错误 | 先看响应体数据是否正常 | 核对JSON断言中的路径表达式 |
| 高并发下出现连接数过多 | 数据库连接池上限被击穿 | 查看数据库错误日志 | 换支持高并发的数据库,配置连接池 |
| 结果树显示连接超时 | 服务器IP/端口不通 | 用curl/Postman测试同一接口 | 确认服务器是否在监听对应端口,防火墙是否放行 |
| CPU占用率飙升且接口变慢 | 代码中存在慢查询 | 开启Django的SQL日志 | 用connection.queries打印SQL,加索引优化 |
6.2 数据库连接池配置建议
Django默认连接数据库的方式是每次请求都新建连接,请求结束关掉,这在低并发场景下没问题,但并发一高就容易出问题。生产环境建议使用连接池复用数据库连接。SQLite不用管连接池,因为它是文件型数据库,本身不适用于高并发场景。如果是MySQL,推荐在settings.py里配置CONN_MAX_AGE参数。
# settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'mydb', 'USER': 'root', 'PASSWORD': 'password', 'HOST': '127.0.0.1', 'PORT': '3306', 'CONN_MAX_AGE': 60, # 连接在60秒内可以复用 } }CONN_MAX_AGE的意思是数据库连接在关闭前可以存活多少秒,在这一段时间内请求会复用同一个连接。设成60秒意味着60秒内的所有请求共用一套连接,大幅减少新建连接的开销。
6.3 断言配置的三个细节
JSON断言虽然好用,但有几个细节不注意容易踩坑。
第一,$.code路径是按点号分隔的,如果返回的JSON里某个字段名本身带点号(虽然很少见),就无法正确取值,需要换用数组索引方式。第二,断言期望值默认是字符串比较,如果接口返回的是数字0,断言期望值写0没问题,但如果返回的是字符串"0",就要带上引号。第三,JSON断言只检查断言字段是否匹配,如果字段在返回数据中不存在,断言会失败并标记红色,这时候要检查是不是接口返回结构变了。
提示:接口改了返回结构,第一件事是同步改断言,而不是先怀疑Jmeter坏了。把断言维护当成接口维护的一部分,这样回归测试时才能真正确认接口行为没有退化。
6.4 压测环境的注意事项
压测时最怕的是测试结果失真,而失真的原因往往不是接口本身,而是测试环境没搞对。
先说Jmeter自身。压测的电脑如果是普通笔记本,线程数开到500以上时,Jmeter本身可能就扛不住了,因为每开一个线程就要占用系统资源。这种情况下测出来的瓶颈可能是Jmeter自己,而不是Django接口。建议压测的机器和被测服务分开,至少别在同一个终端又跑Django又跑Jmeter。
再看Django这边。开发模式下runserver是单进程的,而且会附带自动重载功能,每收到一个请求都要检查文件变更,性能比生产模式差很多。压测时建议用gunicorn或uwsgi启动Django,并且开多个worker进程。命令示例:
gunicorn myproject.wsgi:application -w 4 -b 0.0.0.0:8000-w 4表示启动4个worker进程,压测时4个进程的并发能力远超单进程的runserver。如果不用gunicorn,压测出来的数据只能算开发模式的参考值,不能代表生产性能。
7. 回归测试与持续集成扩展
接口测试做完了,除了手工点Jmeter之外,还可以把Jmeter脚本集成到持续集成流程里,做成自动化回归。这里分享一个轻量级的方案。
Jmeter支持命令行模式,不需要打开图形界面就能跑脚本。保存一份.jmx测试脚本后,用下面的命令执行:
jmeter -n -t mytest.jmx -l result.jtl -e -o /path/to/htmlreport参数含义:
-n:非图形化模式-t:指定测试脚本路径-l:输出结果文件(jtl格式)-e:生成HTML报告-o:HTML报告输出目录
这个命令在Linux服务器上可以直接配合cron定时任务或者Jenkins流水线使用。比如每天晚上10点自动跑一轮接口回归,第二天早上检查HTML报告,接口异常通过告警通知,这样就形成了一套比较完整的接口质量保障闭环。
我自己实际用的流程是:代码合并前先跑一遍Jmeter冒烟测试,包含核心接口的基础功能验证(每个接口各发送一次GET、POST、PUT、DELETE请求),再加上一轮50并发短压测,整体耗时基本在3分钟左右。如果这轮没过,代码就不会合入。用这个方式至少拦截过一次字段名拼接错误导致前端解析不了的回归问题,所以接口测试的自动化价值是实打实的。
8. 实操总结与经验体会
整套流程走下来,我个人最大的体会是:接口测试的难点从来不在于工具怎么用,而在于你到底想验证什么。用Jmeter测功能,只需要确认"接口做对了";用Jmeter做压测,需要确认的是"接口在多大压力下还能做对"。这两种目标对应的配置思路完全不一样,先想清楚目标,再动手配置,效率会高很多。
再分享一个小技巧:Jmeter脚本文件(.jmx)本质上是XML格式,有时候配置项太多,图形界面操作容易眼花,可以直接用文本编辑器打开,搜索关键词定位修改。比如想调整线程组的线程数,搜索num_threads这个属性就能直接改数字,比在界面上翻菜单快多了。但注意改完保存后,最好回到图形界面确认一下没有语法错误。
另外,如果团队里有人觉得Jmeter太笨重,只做功能测试的话,推荐试试用Python写轻量级测试脚本,配合requests库和pytest,也能达到类似效果。但说到并发压测的成熟度和报告丰富度,Jmeter还是目前最稳的选择,学好它不亏。
我这里给出的所有配置和代码,都是经过实际跑通验证的。大家照着做的时候,建议先跑通功能测试,再跑压测,不要一上来就压,否则接口逻辑本身有问题,压测数据再漂亮也没有任何参考价值。