接口压力测试、性能测试工具

接口压力测试、性能测试工具

文章说明

使用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>

运行截图

接口列表

接口测试

测试记录

测试记录详情

源码下载

接口性能测试工具

相关推荐
滚雪球~几秒前
@vue/cli启动异常:ENOENT: no such file or directory, scandir
前端·javascript·vue.js
GDAL11 分钟前
vue3入门教程:ref函数
前端·vue.js·elementui
GISer_Jing20 分钟前
Vue3状态管理——Pinia
前端·javascript·vue.js
web150854159351 小时前
vue 集成 webrtc-streamer 播放视频流 - 解决阿里云内外网访问视频流问题
vue.js·阿里云·webrtc
炭烤玛卡巴卡4 小时前
学习postman工具使用
学习·测试工具·postman
一个处女座的程序猿O(∩_∩)O4 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
迷糊的『迷』4 小时前
vue-axios+springboot实现文件流下载
vue.js·spring boot
web135085886354 小时前
uniapp小程序使用webview 嵌套 vue 项目
vue.js·小程序·uni-app
陈大爷(有低保)4 小时前
uniapp小案例---趣味打字坤
前端·javascript·vue.js
cronaldo914 小时前
研发效能DevOps: Vite 使用 Element Plus
vue.js·vue·devops