接口压力测试、性能测试工具
文章说明
使用jmeter有些地方我觉得有点小复杂,我写了一个小工具来进行接口的简单性能和压力测试
核心源码
1.0版本--采用浏览器发送ajax请求进行性能测试
使用到的核心技术主要包含以下几点:
1、采用队列进行请求的逐个执行
2、采用 requestIdleCallback 进行页面的部分渲染优化
接口测试页面
html
<script setup>
import {computed, onBeforeMount, reactive, ref} from "vue";
import RequestItem from "@/component/RequestItem.vue";
import {deleteRequest, getRequest, message, postRequest, putRequest} from "@/util";
import {openRecordLog} from "@/util/config";
import {dbOperation} from "@/util/dbOperation";
const data = reactive({
interfaceList: [],
selectInterface: {
method: "",
url: "",
},
method: null,
form: {
selectInterfaceId: null,
body: "",
threadNumber: 1,
eachRequestTimesForThread: 1,
},
rules: {
selectInterfaceId: [
{
required: true,
trigger: "change",
message: "请选择测试接口",
},
],
threadNumber: [
{
required: true,
trigger: "blur",
message: "请输入线程数量",
},
],
eachRequestTimesForThread: [
{
required: true,
trigger: "blur",
message: "请输入每个线程请求次数",
},
],
},
resultGroupList: [],
testing: false,
startTime: 0,
endTime: 0,
});
onBeforeMount(() => {
const interfaceList = JSON.parse(localStorage.getItem("interfaceList"));
if (interfaceList) {
for (let i = 0; i < interfaceList.length; i++) {
data.interfaceList.push(interfaceList[i]);
}
}
});
function updateSelectInterface() {
for (let i = 0; i < data.interfaceList.length; i++) {
if (data.form.selectInterfaceId === data.interfaceList[i].id) {
data.selectInterface = {
method: data.interfaceList[i].method,
url: data.interfaceList[i].url,
};
data.method = data.selectInterface.method;
break;
}
}
}
const formRef = ref();
function judgeInteger(number) {
const reg = /^\+?[1-9][0-9]*$/;
return reg.test(number);
}
const total = computed(() => {
if (!judgeInteger(data.form.threadNumber) || !judgeInteger(data.form.eachRequestTimesForThread)) {
message("线程数量、每个线程请求次数 需要为整数", "warning");
return 0;
}
return parseInt(data.form.threadNumber) * parseInt(data.form.eachRequestTimesForThread);
});
const success = computed(() => {
let count = 0;
for (let i = 0; i < data.resultGroupList.length; i++) {
count += data.resultGroupList[i].success;
}
return count;
});
const totalAverage = computed(() => {
let countOfTotal = 0;
let countOfSuccess = 0;
for (let i = 0; i < data.resultGroupList.length; i++) {
countOfTotal += data.resultGroupList[i].total;
countOfSuccess += data.resultGroupList[i].success;
}
return (countOfTotal / countOfSuccess).toFixed(2);
});
// 并发控制参数
const MAX_CONCURRENT_REQUESTS = 5; // 设置最大并发请求数
let runningRequests = 0; // 当前运行中的请求数
const requestQueue = []; // 请求队列
// 执行请求
function enqueueRequest(resultGroup, requestItem) {
requestQueue.push({resultGroup, requestItem});
processQueue();
}
// 处理队列
function processQueue() {
while (runningRequests < MAX_CONCURRENT_REQUESTS && requestQueue.length > 0) {
const {resultGroup, requestItem} = requestQueue.shift();
runningRequests++;
executeRequest(resultGroup, requestItem).finally(() => {
runningRequests--;
processQueue(); // 完成一个请求后,检查队列并启动下一个请求
});
}
}
async function executeRequest(resultGroup, requestItem) {
let response;
try {
requestItem.createTime = Date.now();
switch (requestItem.method) {
case "get":
response = await getRequest(requestItem.url);
break;
case "post":
response = await postRequest(requestItem.url, requestItem.body);
break;
case "put":
response = await putRequest(requestItem.url, requestItem.body);
break;
case "delete":
response = await deleteRequest(requestItem.url, requestItem.body);
break;
}
const now = Date.now();
requestItem.response = JSON.stringify(response.data);
requestItem.spend = now - requestItem.createTime;
requestItem.loading = false;
resultGroup.success++;
resultGroup.total += requestItem.spend;
data.endTime = Date.now();
await openRecordLog();
await dbOperation.add([
{
url: requestItem.url,
method: requestItem.method,
body: requestItem.body,
response: requestItem.response,
create_time: requestItem.createTime,
spend: requestItem.spend,
},
]);
} catch (e) {
console.error(e);
}
}
// 时间切片处理
function processRequestsInChunks(chunks) {
if (chunks.length === 0) {
data.testing = false;
return;
}
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 1 && chunks.length > 0) {
const [resultGroup, requestItems] = chunks[0];
let j;
for (j = 0; j < requestItems.length; j++) {
const requestItem = requestItems[j];
enqueueRequest(resultGroup, requestItem);
}
requestItems.splice(0, j);
if (requestItems.length === 0) {
chunks.shift();
}
}
processRequestsInChunks(chunks);
}, {timeout: 5000});
}
function test() {
if (data.testing) {
message("当前正在测试", "warning");
return;
}
formRef.value.validate((valid) => {
if (valid) {
data.testing = true;
data.startTime = Date.now();
if (!judgeInteger(data.form.threadNumber) || !judgeInteger(data.form.eachRequestTimesForThread)) {
message("线程数量、每个线程请求次数 需要为整数", "warning");
return;
}
data.resultGroupList = [];
const chunks = [];
for (let i = 0; i < data.form.threadNumber; i++) {
const resultGroup = reactive({
groupId: Date.now() + "__" + (i + 1),
groupLabel: "线程" + (i + 1),
requestList: [],
total: 0,
success: 0,
});
data.resultGroupList.push(resultGroup);
const requestItems = [];
for (let j = 0; j < data.form.eachRequestTimesForThread; j++) {
const requestItem = reactive({
requestId: j + 1,
url: data.selectInterface.url,
method: data.selectInterface.method,
body: data.form.body,
response: "",
createTime: 0,
spend: 0,
loading: true,
});
requestItems.push(requestItem);
resultGroup.requestList.push(requestItem);
}
chunks.push([resultGroup, requestItems]);
}
// 开始时间切片处理
processRequestsInChunks(chunks);
}
});
}
</script>
<template>
<el-form ref="formRef" :model="data.form" :rules="data.rules" label-position="right" label-width="auto">
<el-form-item label="测试接口:" prop="selectInterfaceId">
<el-select v-model="data.form.selectInterfaceId" placeholder="请选择测试接口" @change="updateSelectInterface">
<template v-for="item in data.interfaceList" :key="item.id">
<el-option :label="item.url + '__' + item.method" :value="item.id"></el-option>
</template>
</el-select>
</el-form-item>
<el-form-item v-if="data.method && data.method !== 'get'" label="请求报文:">
<el-input v-model="data.form.body" :rows="10" placeholder="请输入请求报文" type="textarea"/>
</el-form-item>
<el-form-item label="线程数量:" prop="threadNumber">
<el-input v-model="data.form.threadNumber" placeholder="请输入线程数量"/>
</el-form-item>
<el-form-item label="每个线程请求次数:" prop="eachRequestTimesForThread">
<el-input v-model="data.form.eachRequestTimesForThread" placeholder="请输入每个线程请求次数"/>
</el-form-item>
<el-form-item>
<div style="width: 100%; display: flex; justify-content: center">
<el-button type="danger" @click="test">测试</el-button>
</div>
</el-form-item>
</el-form>
<el-divider/>
<h3 style="margin-bottom: 20px">
<span>测试结果展示:</span>
<el-tag type="primary" style="margin-right: 10px">请求总数:{{ total }}</el-tag>
<el-tag type="success" style="margin-right: 10px">已完成:{{ success }}</el-tag>
<el-tag type="info" style="margin-right: 10px">请求开始:{{ data.startTime }}</el-tag>
<el-tag type="info" style="margin-right: 10px">请求结束:{{ data.endTime }}</el-tag>
<el-tag type="info" style="margin-right: 10px">请求总耗时:{{ data.endTime - data.startTime }}</el-tag>
<el-tag type="info" style="margin-right: 10px">平均耗时:{{ ((data.endTime - data.startTime) / success).toFixed(2) }}</el-tag>
<el-tag type="info">请求平均耗时:{{ totalAverage }}</el-tag>
</h3>
<el-tabs style="height: fit-content" type="border-card">
<template v-for="resultGroup in data.resultGroupList" :key="resultGroup.groupId">
<el-tab-pane :label="resultGroup.groupLabel" lazy>
<h3 style="margin-bottom: 20px">
<span>请求结果展示:</span>
<el-tag type="primary" style="margin-right: 10px">请求总数:{{ resultGroup.requestList.length }}</el-tag>
<el-tag type="success" style="margin-right: 10px">已完成:{{ resultGroup.success }}</el-tag>
<el-tag type="info" style="margin-right: 10px">请求总耗时:{{ resultGroup.total }}</el-tag>
<el-tag type="info">平均耗时:{{ (resultGroup.total / resultGroup.success).toFixed(2) }}</el-tag>
</h3>
<el-collapse>
<template v-for="item in resultGroup.requestList" :key="item.requestId">
<RequestItem :body="item.body" :create-time="item.createTime" :loading="item.loading" :method="item.method"
:request-id="item.requestId" :response="item.response" :spend="item.spend" :url="item.url"/>
</template>
</el-collapse>
</el-tab-pane>
</template>
</el-tabs>
</template>
<style lang="scss" scoped>
</style>
2.0版本--结合Java模拟压力测试功能
Java端开启线程进行http请求执行
java
package com.boot.util;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.boot.entity.Request;
import com.boot.entity.RequestItem;
import com.boot.entity.Result;
import com.boot.entity.ResultGroup;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.java_websocket.WebSocket;
import java.util.List;
import java.util.Map;
/**
* @author bbyh
* @date 2024/11/12 15:03
*/
@Data
@AllArgsConstructor
public class HandleHttpThread implements Runnable {
private final Request request;
private final WebSocket webSocket;
@Override
public void run() {
Map<String, Object> body;
if (request.getBody() != null && !"".equals(request.getBody())) {
body = JSONUtil.parseObj(request.getBody());
} else {
body = null;
}
List<ResultGroup> resultGroupList = request.getResultGroupList();
for (ResultGroup resultGroup : resultGroupList) {
String groupId = resultGroup.getGroupId();
List<RequestItem> requestList = resultGroup.getRequestList();
new Thread(() -> {
for (RequestItem requestItem : requestList) {
Integer requestId = requestItem.getRequestId();
long start = System.currentTimeMillis();
Result result = HttpClientUtil.normal(request.getUrl(), body, null, request.getMethod());
long end = System.currentTimeMillis();
JSONObject msg = new JSONObject();
msg.putOnce("groupId", groupId);
msg.putOnce("requestId", requestId);
msg.putOnce("spend", end - start);
msg.putOnce("response", JSONUtil.toJsonStr(result));
synchronized (webSocket) {
webSocket.send(JSONUtil.toJsonStr(msg));
}
}
}).start();
}
}
}
前台进行websocket获取http请求执行的返回消息
html
<script setup>
import {computed, nextTick, onBeforeMount, reactive, ref} from "vue";
import RequestItem from "@/component/RequestItem.vue";
import {message} from "@/util";
import {openRecordLog} from "@/util/config";
import {dbOperation} from "@/util/dbOperation";
import {MyWebSocket} from "@/util/myWebSocket";
const data = reactive({
interfaceList: [],
selectInterface: {
method: "",
url: "",
},
method: null,
form: {
selectInterfaceId: null,
body: "",
threadNumber: 1,
eachRequestTimesForThread: 1,
},
rules: {
selectInterfaceId: [
{
required: true,
trigger: "change",
message: "请选择测试接口",
},
],
threadNumber: [
{
required: true,
trigger: "blur",
message: "请输入线程数量",
},
],
eachRequestTimesForThread: [
{
required: true,
trigger: "blur",
message: "请输入每个线程请求次数",
},
],
},
resultGroupList: [],
testing: false,
startTime: 0,
endTime: 0,
});
onBeforeMount(() => {
const interfaceList = JSON.parse(localStorage.getItem("interfaceList"));
if (interfaceList) {
for (let i = 0; i < interfaceList.length; i++) {
data.interfaceList.push(interfaceList[i]);
}
}
});
function updateSelectInterface() {
for (let i = 0; i < data.interfaceList.length; i++) {
if (data.form.selectInterfaceId === data.interfaceList[i].id) {
data.selectInterface = {
method: data.interfaceList[i].method,
url: data.interfaceList[i].url,
};
data.method = data.selectInterface.method;
break;
}
}
}
const formRef = ref();
function judgeInteger(number) {
const reg = /^\+?[1-9][0-9]*$/;
return reg.test(number);
}
const total = computed(() => {
if (!judgeInteger(data.form.threadNumber) || !judgeInteger(data.form.eachRequestTimesForThread)) {
message("线程数量、每个线程请求次数 需要为整数", "warning");
return 0;
}
return parseInt(data.form.threadNumber) * parseInt(data.form.eachRequestTimesForThread);
});
const success = computed(() => {
let count = 0;
for (let i = 0; i < data.resultGroupList.length; i++) {
count += data.resultGroupList[i].success;
}
return count;
});
const totalAverage = computed(() => {
let countOfTotal = 0;
let countOfSuccess = 0;
for (let i = 0; i < data.resultGroupList.length; i++) {
countOfTotal += data.resultGroupList[i].total;
countOfSuccess += data.resultGroupList[i].success;
}
return (countOfTotal / countOfSuccess).toFixed(2);
});
let groupMap = {};
let requestItemMap = {};
function test() {
if (data.testing) {
message("当前正在测试", "warning");
return;
}
formRef.value.validate((valid) => {
if (valid) {
data.testing = true;
data.startTime = Date.now();
groupMap = {};
requestItemMap = {};
if (!judgeInteger(data.form.threadNumber) || !judgeInteger(data.form.eachRequestTimesForThread)) {
message("线程数量、每个线程请求次数 需要为整数", "warning");
return;
}
data.resultGroupList = [];
for (let i = 0; i < data.form.threadNumber; i++) {
const resultGroup = reactive({
groupId: Date.now() + "__" + (i + 1),
groupLabel: "线程" + (i + 1),
requestList: [],
total: 0,
success: 0,
});
data.resultGroupList.push(resultGroup);
groupMap[resultGroup.groupId] = resultGroup;
for (let j = 0; j < data.form.eachRequestTimesForThread; j++) {
const requestItem = reactive({
requestId: j + 1,
url: data.selectInterface.url,
method: data.selectInterface.method,
body: data.form.body,
response: "",
createTime: 0,
spend: 0,
loading: true,
});
resultGroup.requestList.push(requestItem);
requestItemMap[resultGroup.groupId + "__" + requestItem.requestId] = requestItem;
}
}
const socket = new MyWebSocket(8082, function (event) {
handleHttp(event);
}, function () {
socket.send({
request: {
url: data.selectInterface.url,
method: data.selectInterface.method,
body: data.form.body,
resultGroupList: getResultGroupList(),
}
});
});
}
});
}
async function handleHttp(event) {
const transferData = JSON.parse(event.data);
const groupId = transferData.groupId;
const spend = transferData.spend;
const response = transferData.response;
groupMap[groupId].total += spend;
groupMap[groupId].success++;
const requestId = groupId + "__" + transferData.requestId;
const createTime = Date.now() - spend;
requestItemMap[requestId].createTime = createTime;
requestItemMap[requestId].spend = spend;
requestItemMap[requestId].response = response;
requestItemMap[requestId].loading = false;
data.endTime = Date.now();
await openRecordLog();
await dbOperation.add([
{
url: data.selectInterface.url,
method: data.selectInterface.method,
body: data.form.body,
response: response,
create_time: createTime,
spend: spend,
},
]);
await nextTick(() => {
if (success.value === total.value) {
data.testing = false;
}
});
}
function getResultGroupList() {
const resultGroupList = [];
for (let i = 0; i < data.resultGroupList.length; i++) {
const resultGroup = {
groupId: data.resultGroupList[i].groupId,
requestList: [],
};
for (let j = 0; j < data.resultGroupList[i].requestList.length; j++) {
const requestItem = {
requestId: data.resultGroupList[i].requestList[j].requestId,
};
resultGroup.requestList.push(requestItem);
}
resultGroupList.push(resultGroup);
}
return resultGroupList;
}
</script>
<template>
<el-form ref="formRef" :model="data.form" :rules="data.rules" label-position="right" label-width="auto">
<el-form-item label="测试接口:" prop="selectInterfaceId">
<el-select v-model="data.form.selectInterfaceId" placeholder="请选择测试接口" @change="updateSelectInterface">
<template v-for="item in data.interfaceList" :key="item.id">
<el-option :label="item.url + '__' + item.method" :value="item.id"></el-option>
</template>
</el-select>
</el-form-item>
<el-form-item v-if="data.method && data.method !== 'get'" label="请求报文:">
<el-input v-model="data.form.body" :rows="10" placeholder="请输入请求报文" type="textarea"/>
</el-form-item>
<el-form-item label="线程数量:" prop="threadNumber">
<el-input v-model="data.form.threadNumber" placeholder="请输入线程数量"/>
</el-form-item>
<el-form-item label="每个线程请求次数:" prop="eachRequestTimesForThread">
<el-input v-model="data.form.eachRequestTimesForThread" placeholder="请输入每个线程请求次数"/>
</el-form-item>
<el-form-item>
<div style="width: 100%; display: flex; justify-content: center">
<el-button type="danger" @click="test">测试</el-button>
</div>
</el-form-item>
</el-form>
<el-divider/>
<h3 style="margin-bottom: 20px">
<span>测试结果展示:</span>
<el-tag type="primary" style="margin-right: 10px">请求总数:{{ total }}</el-tag>
<el-tag type="success" style="margin-right: 10px">已完成:{{ success }}</el-tag>
<el-tag type="info" style="margin-right: 10px">请求开始:{{ data.startTime }}</el-tag>
<el-tag type="info" style="margin-right: 10px">请求结束:{{ data.endTime }}</el-tag>
<el-tag type="info" style="margin-right: 10px">请求总耗时:{{ data.endTime - data.startTime }}</el-tag>
<el-tag type="info" style="margin-right: 10px">平均耗时:{{ ((data.endTime - data.startTime) / success).toFixed(2) }}</el-tag>
<el-tag type="info">请求平均耗时:{{ totalAverage }}</el-tag>
</h3>
<el-tabs style="height: fit-content" type="border-card">
<template v-for="resultGroup in data.resultGroupList" :key="resultGroup.groupId">
<el-tab-pane :label="resultGroup.groupLabel" lazy>
<h3 style="margin-bottom: 20px">
<span>请求结果展示:</span>
<el-tag type="primary" style="margin-right: 10px">请求总数:{{ resultGroup.requestList.length }}</el-tag>
<el-tag type="success" style="margin-right: 10px">已完成:{{ resultGroup.success }}</el-tag>
<el-tag type="info" style="margin-right: 10px">请求总耗时:{{ resultGroup.total }}</el-tag>
<el-tag type="info">平均耗时:{{ (resultGroup.total / resultGroup.success).toFixed(2) }}</el-tag>
</h3>
<el-collapse>
<template v-for="item in resultGroup.requestList" :key="item.requestId">
<RequestItem :body="item.body" :create-time="item.createTime" :loading="item.loading" :method="item.method"
:request-id="item.requestId" :response="item.response" :spend="item.spend" :url="item.url"/>
</template>
</el-collapse>
</el-tab-pane>
</template>
</el-tabs>
</template>
<style lang="scss" scoped>
</style>
运行截图
接口列表
接口测试
测试记录
测试记录详情