feat(接口测试): 接口通过率开发

This commit is contained in:
Jianguo-Genius 2024-11-12 14:32:17 +08:00 committed by 建国
parent 35af224764
commit 1f46ee6b9a
17 changed files with 578 additions and 85 deletions

View File

@ -2,6 +2,7 @@ package io.metersphere.api.controller.definition;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.api.constants.ApiScenarioStepType;
import io.metersphere.api.domain.ApiDefinition;
import io.metersphere.api.dto.ReferenceDTO;
import io.metersphere.api.dto.ReferenceRequest;
@ -10,8 +11,12 @@ import io.metersphere.api.dto.request.ApiEditPosRequest;
import io.metersphere.api.dto.request.ApiTransferRequest;
import io.metersphere.api.dto.request.ImportRequest;
import io.metersphere.api.dto.schema.JsonSchemaItem;
import io.metersphere.api.mapper.ExtApiDefinitionMapper;
import io.metersphere.api.mapper.ExtApiScenarioStepMapper;
import io.metersphere.api.mapper.ExtApiTestCaseMapper;
import io.metersphere.api.service.ApiFileResourceService;
import io.metersphere.api.service.definition.*;
import io.metersphere.api.service.scenario.ApiScenarioService;
import io.metersphere.project.service.FileModuleService;
import io.metersphere.sdk.constants.DefaultRepositoryDir;
import io.metersphere.sdk.constants.PermissionConstants;
@ -34,6 +39,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotBlank;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
@ -41,6 +47,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.List;
@ -51,6 +58,14 @@ import java.util.List;
@RequestMapping(value = "/api/definition")
@Tag(name = "接口测试-接口管理-接口定义")
public class ApiDefinitionController {
@Resource
private ExtApiDefinitionMapper extApiDefinitionMapper;
@Resource
private ExtApiTestCaseMapper extApiTestCaseMapper;
@Resource
private ExtApiScenarioStepMapper extApiScenarioStepMapper;
@Resource
private ApiDefinitionService apiDefinitionService;
@Resource
@ -61,12 +76,55 @@ public class ApiDefinitionController {
private ApiDefinitionImportService apiDefinitionImportService;
@Resource
private ApiDefinitionExportService apiDefinitionExportService;
@Resource
private ApiScenarioService apiScenarioService;
/*
接口覆盖率
业务注释误删
* 一个接口如果被跨项目的场景给关联了算不算覆盖 不算
* 自定义请求 不管它有多少个/"有多少子域 跟接口定义匹配的时候就用末端匹配法。
· 例如https://www.tapd.cn/tapd_fe/my/work?dialog_preview_id=abcdefg
·/work能匹配的上
·/my/work能匹配的上
·/my 不可以
·/my/{something}可以匹配的上
·/my/{something}/{other-thing}不可以
* 剩下的基本上就跟V2一样了. 有用例 or 被场景引用/复制 or 被自定义给命中了 就算覆盖 且自定义请求可以命中多个接口定义比如上一点
*/
@GetMapping("/rage/{projectId}")
@Operation(summary = "接口测试-接口管理-接口列表(deleted 状态为 1 时为回收站数据)")
@RequiresPermissions(PermissionConstants.PROJECT_API_DEFINITION_READ)
@CheckOwner(resourceId = "#projectId", resourceType = "project")
public ApiCoverageDTO rage(@PathVariable String projectId) {
List<String> apiAllIds = new ArrayList<>();
List<ApiDefinition> httpApiList = new ArrayList<>();
extApiDefinitionMapper.selectBaseInfoByProjectId(projectId).forEach(apiDefinition -> {
if (StringUtils.equalsIgnoreCase(apiDefinition.getProtocol(), "http")) {
httpApiList.add(apiDefinition);
}
apiAllIds.add(apiDefinition.getId());
});
List<String> apiDefinitionIdFromCase = extApiTestCaseMapper.selectApiId(projectId);
List<String> apiInScenarioStep = extApiScenarioStepMapper.selectResourceId(projectId, ApiScenarioStepType.API.name());
List<String> apiCaseIdInStep = extApiScenarioStepMapper.selectResourceId(projectId, ApiScenarioStepType.API_CASE.name());
if (CollectionUtils.isNotEmpty(apiCaseIdInStep)) {
List<String> apiCaseIdInScenarioStep = extApiTestCaseMapper.selectApiIdByCaseId(apiCaseIdInStep);
apiInScenarioStep.addAll(apiCaseIdInScenarioStep);
}
List<String> apiInStepList = apiScenarioService.selectApiIdInCustomRequest(projectId, httpApiList);
apiInStepList.addAll(apiInScenarioStep);
return new ApiCoverageDTO(apiAllIds, apiDefinitionIdFromCase, apiInStepList);
}
@PostMapping(value = "/add")
@Operation(summary = "接口测试-接口管理-添加接口定义")
@RequiresPermissions(PermissionConstants.PROJECT_API_DEFINITION_ADD)
@Log(type = OperationLogType.ADD, expression = "#msClass.addLog(#request)", msClass = ApiDefinitionLogService.class)
@CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project")
@CheckOwner(resourceId = "#request.getProjectId()s", resourceType = "project")
@SendNotice(taskType = NoticeConstants.TaskType.API_DEFINITION_TASK, event = NoticeConstants.Event.CREATE, target = "#targetClass.getApiDTO(#request)", targetClass = ApiDefinitionNoticeService.class)
public ApiDefinition add(@Validated @RequestBody ApiDefinitionAddRequest request) {
return apiDefinitionService.create(request, SessionUtils.getUserId());

View File

@ -3,10 +3,6 @@ package io.metersphere.api.controller.scenario;
import io.metersphere.api.constants.ApiResource;
import io.metersphere.api.dto.response.ApiScenarioBatchOperationResponse;
import io.metersphere.api.dto.scenario.*;
import io.metersphere.api.dto.scenario.ApiScenarioBatchCopyMoveRequest;
import io.metersphere.api.dto.scenario.ApiScenarioBatchEditRequest;
import io.metersphere.api.dto.scenario.ApiScenarioBatchRequest;
import io.metersphere.api.dto.scenario.ApiScenarioBatchRunRequest;
import io.metersphere.api.service.ApiValidateService;
import io.metersphere.api.service.scenario.ApiScenarioBatchRunService;
import io.metersphere.api.service.scenario.ApiScenarioNoticeService;

View File

@ -0,0 +1,82 @@
package io.metersphere.api.dto.definition;
import io.metersphere.sdk.util.CalculateUtils;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* @author lan
*/
@Data
@NoArgsConstructor
public class ApiCoverageDTO {
@Schema(description = "接口定义总量")
private int allApiCount;
@Schema(description = "接口定义未覆盖")
private int unCoverWithApiDefinition;
@Schema(description = "接口定义已覆盖")
private int coverWithApiDefinition;
@Schema(description = "接口覆盖率 接口URL用例或场景步骤数/接口总数*100%")
private String apiCoverage;
@Schema(description = "接口用例未覆盖")
private int unCoverWithApiCase;
@Schema(description = "接口用例已覆盖")
private int coverWithApiCase;
@Schema(description = "用例覆盖率 有用例的接口/接口总数*100%")
private String apiCaseCoverage;
@Schema(description = "接口场景未覆盖")
private int unCoverWithApiScenario;
@Schema(description = "接口场景已覆盖")
private int coverWithApiScenario;
@Schema(description = "场景覆盖率 被场景步骤包含的接口(URL)数/接口总数*100%")
private String scenarioCoverage;
public ApiCoverageDTO(List<String> allApiId, List<String> haveCaseIdList, List<String> apiIdOrUrlInStepList) {
// 去重过滤只留下apiId中存在的数据避免已删除的apiId导致统计错误
allApiId = allApiId.stream().distinct().toList();
haveCaseIdList = this.elementInList(allApiId, haveCaseIdList.stream().distinct().toList());
apiIdOrUrlInStepList = this.elementInList(allApiId, apiIdOrUrlInStepList.stream().distinct().toList());
this.allApiCount = allApiId.size();
// 用例覆盖率 有用例的接口/接口总数*100%
this.coverWithApiCase = haveCaseIdList.size();
this.unCoverWithApiCase = this.allApiCount - this.coverWithApiCase;
this.apiCaseCoverage = CalculateUtils.reportPercentage(coverWithApiCase, allApiCount);
// 场景覆盖率 被场景步骤包含的接口(URL)/接口总数*100%
this.coverWithApiScenario = apiIdOrUrlInStepList.size();
this.unCoverWithApiScenario = this.allApiCount - this.coverWithApiScenario;
this.scenarioCoverage = CalculateUtils.reportPercentage(coverWithApiScenario, allApiCount);
// 接口覆盖率
apiIdOrUrlInStepList.addAll(haveCaseIdList);
List<String> allCoverList = apiIdOrUrlInStepList.stream().distinct().toList();
this.coverWithApiDefinition = allCoverList.size();
this.unCoverWithApiDefinition = this.allApiCount - this.coverWithApiDefinition;
this.apiCoverage = CalculateUtils.reportPercentage(coverWithApiDefinition, allApiCount);
}
private List<String> elementInList(List<String> allList, List<String> compareList) {
List<String> returnList = new ArrayList<>();
compareList.forEach(item -> {
if (allList.contains(item)) {
returnList.add(item);
}
});
return returnList;
}
}

View File

@ -19,7 +19,7 @@ public class ApiScenarioBatchEditRequest extends ApiScenarioBatchRequest impleme
@Schema(description = "标签")
private LinkedHashSet<String> tags;
@Schema(description = "批量编辑的类型 用例等级: Priority,状态 :Status,标签: Tags,用例环境: Environment, 定时任务Schedule")
@Schema(description = "批量编辑的类型 用例等级: Priority,状态 :Status,标签: Tags,用例环境: Environment")
@NotBlank
private String type;
@Schema(description = "默认覆盖原标签")
@ -40,8 +40,6 @@ public class ApiScenarioBatchEditRequest extends ApiScenarioBatchRequest impleme
@Schema(description = "用例等级")
@Size(max = 50, message = "{api_test_case.priority.length_range}")
private String priority;
@Schema(description = "定时任务是否开启")
private boolean scheduleOpen;
public List<String> getTags() {
if (tags == null) {
return new ArrayList<>(0);

View File

@ -2,7 +2,6 @@ package io.metersphere.api.dto.scenario;
import io.metersphere.sdk.dto.api.task.ApiRunModeConfigDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -18,7 +17,6 @@ public class ApiScenarioBatchScheduleConfigRequest extends ApiScenarioBatchReque
private boolean enable;
@Schema(description = "Cron表达式")
@NotBlank
private String cron;
@Schema(description = "定时任务配置")

View File

@ -129,4 +129,5 @@ public interface ExtApiDefinitionMapper {
List<ApiDefinition> getCreateApiList(@Param("projectId") String projectId, @Param("startTime") Long startTime, @Param("endTime") Long endTime);
List<ApiDefinition> selectBaseInfoByProjectId(String projectId);
}

View File

@ -838,4 +838,10 @@
AND api_definition.create_time BETWEEN #{startTime} AND #{endTime}
</if>
</select>
<select id="selectBaseInfoByProjectId" resultType="io.metersphere.api.domain.ApiDefinition">
SELECT id, path, method, protocol
FROM api_definition
WHERE project_id = #{0}
AND deleted IS FALSE
</select>
</mapper>

View File

@ -1,6 +1,7 @@
package io.metersphere.api.mapper;
import io.metersphere.api.domain.ApiScenarioCsvStep;
import io.metersphere.api.domain.ApiScenarioStep;
import io.metersphere.api.dto.scenario.ApiScenarioStepDTO;
import org.apache.ibatis.annotations.Param;
@ -25,4 +26,8 @@ public interface ExtApiScenarioStepMapper {
* @return
*/
List<String> getHasBlobRequestStepIds(@Param("scenarioId") String scenarioId);
List<String> selectResourceId(@Param("projectId") String projectId, @Param("stepType") String stepType);
List<ApiScenarioStep> selectCustomRequestConfigByProjectId(String projectId);
}

View File

@ -28,4 +28,21 @@
and step_type in ('API', 'API_CASE', 'CUSTOM_REQUEST')
and ref_type in ('COPY', 'DIRECT')
</select>
<select id="selectResourceId" resultType="java.lang.String">
select DISTINCT step.resource_id
from api_scenario_step step
INNER JOIN api_scenario scenario
ON step.scenario_id = scenario.id
where step.step_type = #{stepType}
AND scenario.project_id = #{projectId}
AND scenario.deleted IS FALSE
</select>
<select id="selectCustomRequestConfigByProjectId" resultType="io.metersphere.api.domain.ApiScenarioStep">
select step.id, step.config
from api_scenario_step step
INNER JOIN api_scenario scenario
ON step.scenario_id = scenario.id
where scenario.project_id = #{0}
AND scenario.deleted IS FALSE
</select>
</mapper>

View File

@ -139,4 +139,8 @@ public interface ExtApiTestCaseMapper {
List<ApiTestCase> getSimpleApiCaseList(@Param("projectId") String projectId, @Param("startTime") Long startTime, @Param("endTime") Long endTime);
List<String> selectApiId(String projectId);
List<String> selectApiIdByCaseId(@Param("ids") List<String> apiCaseIdInStep);
}

View File

@ -1031,4 +1031,18 @@
</if>
</select>
<select id="selectApiId" resultType="java.lang.String">
SELECT distinct api.id
FROM api_test_case apiCase
INNER JOIN api_definition api ON apiCase.api_definition_id = api.id
WHERE apiCase.deleted is FALSE
AND api.deleted is FALSE
AND api.project_id = #{0}
</select>
<select id="selectApiIdByCaseId" resultType="java.lang.String">
SELECT api_definition_id FROM api_test_case WHERE deleted is false AND id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</select>
</mapper>

View File

@ -25,10 +25,12 @@ import io.metersphere.api.service.ApiFileResourceService;
import io.metersphere.api.service.definition.ApiDefinitionService;
import io.metersphere.api.service.definition.ApiTestCaseService;
import io.metersphere.api.utils.ApiDataUtils;
import io.metersphere.api.utils.ApiDefinitionUtils;
import io.metersphere.api.utils.ApiScenarioBatchOperationUtils;
import io.metersphere.api.utils.ApiScenarioUtils;
import io.metersphere.functional.domain.FunctionalCaseTestExample;
import io.metersphere.functional.mapper.FunctionalCaseTestMapper;
import io.metersphere.plugin.api.spi.AbstractMsProtocolTestElement;
import io.metersphere.plugin.api.spi.AbstractMsTestElement;
import io.metersphere.project.domain.*;
import io.metersphere.project.dto.MoveNodeSortDTO;
@ -278,7 +280,6 @@ public class ApiScenarioService extends MoveNodeService {
case STATUS -> batchUpdateStatus(example, updateScenario, request.getStatus(), mapper);
case TAGS -> batchUpdateTags(example, updateScenario, request, ids, mapper);
case ENVIRONMENT -> batchUpdateEnvironment(example, updateScenario, request, mapper);
case SCHEDULE -> batchUpdateSchedule(example, request, mapper, userId);
default -> throw new MSException(Translator.get("batch_edit_type_error"));
}
sqlSession.flushStatements();
@ -288,15 +289,6 @@ public class ApiScenarioService extends MoveNodeService {
apiScenarioNoticeService.batchSendNotice(ids, userId, projectId, NoticeConstants.Event.UPDATE);
}
private void batchUpdateSchedule(ApiScenarioExample example, ApiScenarioBatchEditRequest request, ApiScenarioMapper mapper, String userId) {
List<ApiScenario> apiScenarioList = mapper.selectByExample(example);
//批量编辑定时任务
for (ApiScenario apiScenario : apiScenarioList) {
scheduleService.updateIfExist(apiScenario.getId(), request.isScheduleOpen(), ApiScenarioScheduleJob.getJobKey(apiScenario.getId()),
ApiScenarioScheduleJob.getTriggerKey(apiScenario.getId()), ApiScenarioScheduleJob.class, userId);
}
}
private void batchUpdateEnvironment(ApiScenarioExample example, ApiScenario updateScenario,
ApiScenarioBatchEditRequest request, ApiScenarioMapper mapper) {
if (BooleanUtils.isFalse(request.isGrouped())) {
@ -2566,30 +2558,42 @@ public class ApiScenarioService extends MoveNodeService {
List<ApiScenario> apiScenarios = apiScenarioMapper.selectByExample(example);
if (CollectionUtils.isNotEmpty(apiScenarios)) {
apiScenarios.forEach(apiScenario -> {
ScheduleConfig scheduleConfig = ScheduleConfig.builder()
.resourceId(apiScenario.getId())
.key(apiScenario.getId())
.projectId(apiScenario.getProjectId())
.name(apiScenario.getName())
.enable(request.isEnable())
.cron(request.getCron())
.resourceType(ScheduleResourceType.API_SCENARIO.name())
.config(JSON.toJSONString(request.getConfig()))
.build();
if (StringUtils.isBlank(request.getCron()) && request.getConfig() == null) {
this.batchUpdateSchedule(apiScenarios, request.isEnable(), operator);
} else {
apiScenarios.forEach(apiScenario -> {
ScheduleConfig scheduleConfig = ScheduleConfig.builder()
.resourceId(apiScenario.getId())
.key(apiScenario.getId())
.projectId(apiScenario.getProjectId())
.name(apiScenario.getName())
.enable(request.isEnable())
.cron(request.getCron())
.resourceType(ScheduleResourceType.API_SCENARIO.name())
.config(JSON.toJSONString(request.getConfig()))
.build();
scheduleService.scheduleConfig(
scheduleConfig,
ApiScenarioScheduleJob.getJobKey(apiScenario.getId()),
ApiScenarioScheduleJob.getTriggerKey(apiScenario.getId()),
ApiScenarioScheduleJob.class,
operator);
});
apiScenarioLogService.batchScheduleConfigLog(request.getProjectId(), apiScenarios, operator);
scheduleService.scheduleConfig(
scheduleConfig,
ApiScenarioScheduleJob.getJobKey(apiScenario.getId()),
ApiScenarioScheduleJob.getTriggerKey(apiScenario.getId()),
ApiScenarioScheduleJob.class,
operator);
});
apiScenarioLogService.batchScheduleConfigLog(request.getProjectId(), apiScenarios, operator);
}
}
}
}
private void batchUpdateSchedule(List<ApiScenario> apiScenarioList, boolean isScheudleOpen, String userId) {
//批量编辑定时任务
for (ApiScenario apiScenario : apiScenarioList) {
scheduleService.updateIfExist(apiScenario.getId(), isScheudleOpen, ApiScenarioScheduleJob.getJobKey(apiScenario.getId()),
ApiScenarioScheduleJob.getTriggerKey(apiScenario.getId()), ApiScenarioScheduleJob.class, userId);
}
}
// 场景统计相关
public List<ApiScenarioDTO> calculateRate(List<String> ids) {
List<ApiScenarioDTO> result = new ArrayList<>();
@ -2619,4 +2623,47 @@ public class ApiScenarioService extends MoveNodeService {
}
return result;
}
public List<String> selectApiIdInCustomRequest(String projectId, List<ApiDefinition> apiDefinitions) {
List<String> returnList = new ArrayList<>();
List<ApiScenarioStep> stepConfigList = extApiScenarioStepMapper.selectCustomRequestConfigByProjectId(projectId);
List<String> requestIdList = new ArrayList<>();
stepConfigList.forEach(step -> requestIdList.add(step.getId()));
if (requestIdList.isEmpty()) {
return returnList;
}
ApiScenarioStepBlobExample scenarioStepBlobExample = new ApiScenarioStepBlobExample();
scenarioStepBlobExample.createCriteria().andIdIn(requestIdList);
List<ApiScenarioStepBlob> httpRequestStopBlobList = apiScenarioStepBlobMapper.selectByExampleWithBLOBs(scenarioStepBlobExample);
Map<String, List<String>> methodPathMap = new HashMap<>();
httpRequestStopBlobList.forEach(blob -> {
if (blob.getContent() != null) {
try {
AbstractMsProtocolTestElement protocolTestElement = ApiDataUtils.parseObject(new String(blob.getContent()), AbstractMsProtocolTestElement.class);
if (protocolTestElement instanceof MsHTTPElement msHTTPElement) {
String method = msHTTPElement.getMethod();
if (methodPathMap.containsKey(method)) {
methodPathMap.get(method).add(msHTTPElement.getPath());
} else {
List<String> pathList = new ArrayList<>();
pathList.add(msHTTPElement.getPath());
methodPathMap.put(method, pathList);
}
}
} catch (Exception e) {
LogUtils.error(e);
}
}
});
for (ApiDefinition apiDefinition : apiDefinitions) {
if (methodPathMap.containsKey(apiDefinition.getMethod())) {
String apiPath = apiDefinition.getPath();
List<String> customUrlList = methodPathMap.get(apiDefinition.getMethod());
if (ApiDefinitionUtils.isUrlInList(apiPath, customUrlList)) {
returnList.add(apiDefinition.getId());
}
}
}
return returnList;
}
}

View File

@ -0,0 +1,61 @@
package io.metersphere.api.utils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.Collection;
public class ApiDefinitionUtils {
public static boolean isUrlInList(String apiUrl, Collection<String> customRequestUrlList) {
if (CollectionUtils.isEmpty(customRequestUrlList)) {
return false;
}
String urlSuffix = apiUrl.trim();
if (urlSuffix.startsWith("/")) {
urlSuffix = urlSuffix.substring(1);
}
String[] urlParams = urlSuffix.split("/");
for (String customRequestUrl : customRequestUrlList) {
if (StringUtils.equalsAny(customRequestUrl, apiUrl, "/" + apiUrl)) {
return true;
} else {
if (StringUtils.isEmpty(customRequestUrl)) {
continue;
}
if (customRequestUrl.startsWith("/")) {
customRequestUrl = customRequestUrl.substring(1);
}
if (StringUtils.isNotEmpty(customRequestUrl)) {
String[] customUrlArr = customRequestUrl.split("/");
if (customUrlArr.length >= urlParams.length) {
boolean isFetch = true;
for (int urlIndex = 0; urlIndex < urlParams.length; urlIndex++) {
String urlItem = urlParams[urlIndex];
String customUrlItem = customUrlArr[customUrlArr.length - urlParams.length + urlIndex];
// 不为rest参数的要进行全匹配 而且忽略大小写
if (isRestUrlParam(customUrlItem) && isRestUrlParam(urlItem)) {
if (!StringUtils.equalsIgnoreCase(customUrlItem, urlItem)) {
isFetch = false;
break;
}
}
}
if (isFetch) {
return true;
}
}
}
}
}
return false;
}
private static boolean isRestUrlParam(String urlParam) {
return !StringUtils.startsWith(urlParam, "{") || !StringUtils.endsWith(urlParam, "}") || StringUtils.equals(urlParam, "{}");
}
}

View File

@ -0,0 +1,237 @@
package io.metersphere.api.controller;
import io.metersphere.api.constants.*;
import io.metersphere.api.domain.ApiDefinition;
import io.metersphere.api.domain.ApiDefinitionExample;
import io.metersphere.api.domain.ApiScenario;
import io.metersphere.api.domain.ApiTestCase;
import io.metersphere.api.dto.definition.ApiCoverageDTO;
import io.metersphere.api.dto.definition.ApiDefinitionAddRequest;
import io.metersphere.api.dto.definition.ApiTestCaseAddRequest;
import io.metersphere.api.dto.definition.HttpResponse;
import io.metersphere.api.dto.request.http.MsHTTPElement;
import io.metersphere.api.dto.scenario.ApiScenarioAddRequest;
import io.metersphere.api.dto.scenario.ApiScenarioStepRequest;
import io.metersphere.api.dto.scenario.ApiScenarioUpdateRequest;
import io.metersphere.api.mapper.ApiDefinitionMapper;
import io.metersphere.api.utils.ApiDataUtils;
import io.metersphere.project.domain.Project;
import io.metersphere.project.mapper.ExtBaseProjectVersionMapper;
import io.metersphere.project.mapper.ProjectMapper;
import io.metersphere.sdk.util.CalculateUtils;
import io.metersphere.sdk.util.JSON;
import io.metersphere.system.base.BaseTest;
import io.metersphere.system.dto.AddProjectRequest;
import io.metersphere.system.log.constants.OperationLogModule;
import io.metersphere.system.service.CommonProjectService;
import io.metersphere.system.uid.IDGenerator;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MvcResult;
import java.util.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ApiCalculateTest extends BaseTest {
private static Project project;
@Resource
private CommonProjectService commonProjectService;
@Resource
private ExtBaseProjectVersionMapper extBaseProjectVersionMapper;
@Resource
private ProjectMapper projectMapper;
@Resource
private ApiDefinitionMapper apiDefinitionMapper;
@BeforeEach
public void initTestData() throws Exception {
//测试计划专用项目
if (project == null) {
AddProjectRequest initProject = new AddProjectRequest();
initProject.setOrganizationId("100001");
initProject.setName("文件管理专用项目");
initProject.setDescription("建国创建的文件管理专用项目");
initProject.setEnable(true);
initProject.setUserIds(List.of("admin"));
project = commonProjectService.add(initProject, "admin", "/organization-project/add", OperationLogModule.SETTING_ORGANIZATION_PROJECT);
ArrayList<String> moduleList = new ArrayList<>(List.of(new String[]{"workstation", "testPlan", "bugManagement", "caseManagement", "apiTest", "uiTest", "loadTest"}));
Project updateProject = new Project();
updateProject.setId(project.getId());
updateProject.setModuleSetting(JSON.toJSONString(moduleList));
projectMapper.updateByPrimaryKeySelective(updateProject);
ApiScenarioAddRequest apiScenarioAddRequest = new ApiScenarioAddRequest();
apiScenarioAddRequest.setProjectId(project.getId());
apiScenarioAddRequest.setDescription("desc");
apiScenarioAddRequest.setName("test name");
apiScenarioAddRequest.setModuleId("default");
apiScenarioAddRequest.setGrouped(false);
apiScenarioAddRequest.setTags(List.of("tag1", "tag2"));
apiScenarioAddRequest.setPriority("P0");
apiScenarioAddRequest.setStatus(ApiScenarioStatus.COMPLETED.name());
apiScenarioAddRequest.setSteps(new ArrayList<>());
MvcResult scenarioMvcResult = this.requestPostWithOkAndReturn("/api/scenario/add", apiScenarioAddRequest);
ApiScenario scenario = getResultData(scenarioMvcResult, ApiScenario.class);
// 创建接口定义
Map<String, List<String>> methodAndPath = Map.of(
"GET", List.of(
"/api/get-test/1",
"/api/get-test/2",
"/api/{/get-test}/3/withCase",
"/api/get-test/4/withCase",// 场景关联它的用例
"/api/get-test/5/never-compare",
"/{api}/{/get-test}/{6}",//这个一定会被匹配到
"/api/get-test/{7}",// 这个一定会被匹配到
"/api/get-test/8",// 场景关联它的自定义请求
"/api/get-test/9",// 场景关联这个接口
"/api/get-test/10"),
"POST", List.of(
"/post/api/test/1",
"/post/api/test/2",
"/post/api/{test}/3/withCase",
"/post/api/test/4/withCase",// 场景关联它的用例
"/post/api/test/5/never-compare",
"/{post}/{api}/{/get-test}/{6}", //这个一定会被匹配到
"/post/api/test/{7}", // 这个一定会被匹配到
"/post/api/test/8", // 场景关联它的自定义请求
"/post/api/test/9",// 场景关联这个接口
"/post/api/test/10"
)
);
List<ApiScenarioStepRequest> steps = new ArrayList<>();
Map<String, Object> steptDetailMap = new HashMap<>();
// 创建接口用例
for (Map.Entry<String, List<String>> entry : methodAndPath.entrySet()) {
String method = entry.getKey();
for (String path : entry.getValue()) {
// 创建接口定义
String defaultVersion = extBaseProjectVersionMapper.getDefaultVersion(project.getId());
ApiDefinitionAddRequest apiDefinitionAddRequest = new ApiDefinitionAddRequest();
apiDefinitionAddRequest.setName(method + "_" + path);
apiDefinitionAddRequest.setProtocol(ApiConstants.HTTP_PROTOCOL);
apiDefinitionAddRequest.setProjectId(project.getId());
apiDefinitionAddRequest.setMethod(method);
apiDefinitionAddRequest.setPath(path);
apiDefinitionAddRequest.setStatus(ApiDefinitionStatus.PROCESSING.name());
apiDefinitionAddRequest.setModuleId("default");
apiDefinitionAddRequest.setVersionId(defaultVersion);
apiDefinitionAddRequest.setDescription("描述内容");
apiDefinitionAddRequest.setTags(new LinkedHashSet<>(List.of("tag1", "tag2")));
MsHTTPElement msHttpElement = MsHTTPElementTest.getMsHttpElement();
msHttpElement.setBody(MsHTTPElementTest.getGeneralBody());
msHttpElement.setMethod(method);
msHttpElement.setPath(path);
apiDefinitionAddRequest.setRequest(JSON.parseObject(ApiDataUtils.toJSONString(msHttpElement)));
List<HttpResponse> msHttpResponse = MsHTTPElementTest.getMsHttpResponse();
apiDefinitionAddRequest.setResponse(msHttpResponse);
MvcResult mvcResult = this.requestPostWithOkAndReturn("/api/definition/add", apiDefinitionAddRequest);
ApiDefinition resultData = getResultData(mvcResult, ApiDefinition.class);
if (path.endsWith("/withCase")) {
ApiTestCaseAddRequest request = new ApiTestCaseAddRequest();
request.setApiDefinitionId(resultData.getId());
request.setName(resultData.getName() + "_case");
request.setProjectId(project.getId());
request.setPriority("P0");
request.setStatus(ApiDefinitionStatus.PROCESSING.name());
request.setTags(new LinkedHashSet<>(List.of("tag1", "tag2")));
request.setRequest(JSON.parseObject(ApiDataUtils.toJSONString(msHttpElement)));
MvcResult testCaseResult = this.requestPostWithOkAndReturn("/api/case/add", request);
ApiTestCase apiTestCase = getResultData(testCaseResult, ApiTestCase.class);
if (path.endsWith("/4/withCase")) {
ApiScenarioStepRequest stepRequest = new ApiScenarioStepRequest();
stepRequest.setId(IDGenerator.nextStr());
stepRequest.setVersionId(extBaseProjectVersionMapper.getDefaultVersion(project.getId()));
stepRequest.setConfig(new HashMap<>());
stepRequest.setEnable(true);
stepRequest.setStepType(ApiScenarioStepType.API_CASE.name());
stepRequest.setResourceId(apiTestCase.getId());
stepRequest.setName(apiTestCase.getName() + "_step");
stepRequest.setRefType(ApiScenarioStepRefType.REF.name());
stepRequest.setProjectId(project.getId());
steps.add(stepRequest);
steptDetailMap.put(stepRequest.getId(), JSON.parseObject(ApiDataUtils.toJSONString(msHttpElement)));
}
} else if (path.endsWith("/8")) {
ApiScenarioStepRequest stepRequest = new ApiScenarioStepRequest();
stepRequest.setId(IDGenerator.nextStr());
stepRequest.setVersionId(extBaseProjectVersionMapper.getDefaultVersion(project.getId()));
stepRequest.setConfig(new HashMap<>());
stepRequest.setEnable(true);
stepRequest.setStepType(ApiScenarioStepType.CUSTOM_REQUEST.name());
stepRequest.setName("custom_step");
stepRequest.setRefType(ApiScenarioStepRefType.DIRECT.name());
stepRequest.setProjectId(project.getId());
steps.add(stepRequest);
MsHTTPElement customElement = MsHTTPElementTest.getMsHttpElement();
customElement.setBody(MsHTTPElementTest.getGeneralBody());
customElement.setMethod(method);
customElement.setPath(path);
steptDetailMap.put(stepRequest.getId(), JSON.parseObject(ApiDataUtils.toJSONString(customElement)));
} else if (path.endsWith("/9")) {
ApiScenarioStepRequest stepRequest = new ApiScenarioStepRequest();
stepRequest.setId(IDGenerator.nextStr());
stepRequest.setVersionId(extBaseProjectVersionMapper.getDefaultVersion(project.getId()));
stepRequest.setConfig(new HashMap<>());
stepRequest.setEnable(true);
stepRequest.setStepType(ApiScenarioStepType.API.name());
stepRequest.setResourceId(resultData.getId());
stepRequest.setName(resultData.getName() + "_step");
stepRequest.setRefType(ApiScenarioStepRefType.REF.name());
stepRequest.setProjectId(project.getId());
steps.add(stepRequest);
steptDetailMap.put(stepRequest.getId(), JSON.parseObject(ApiDataUtils.toJSONString(msHttpElement)));
}
}
}
ApiScenarioUpdateRequest apiScenarioUpdateRequest = new ApiScenarioUpdateRequest();
apiScenarioUpdateRequest.setId(scenario.getId());
apiScenarioUpdateRequest.setProjectId(project.getId());
apiScenarioUpdateRequest.setSteps(steps);
apiScenarioUpdateRequest.setStepDetails(steptDetailMap);
this.requestPostWithOkAndReturn("/api/scenario/update", apiScenarioUpdateRequest);
}
}
@Test
public void calculateTest() throws Exception {
ApiDefinitionExample apiDefinitionExample = new ApiDefinitionExample();
apiDefinitionExample.createCriteria().andProjectIdEqualTo(project.getId());
Assertions.assertEquals(apiDefinitionMapper.countByExample(apiDefinitionExample), 20);
MvcResult mvcResult = this.requestGetWithOkAndReturn("/api/definition/rage/" + project.getId());
ApiCoverageDTO apiCoverageDTO = getResultData(mvcResult, ApiCoverageDTO.class);
Assertions.assertEquals(apiCoverageDTO.getAllApiCount(), 20);
Assertions.assertEquals(apiCoverageDTO.getCoverWithApiCase(), 4);
Assertions.assertEquals(apiCoverageDTO.getUnCoverWithApiCase(), 16);
Assertions.assertEquals(apiCoverageDTO.getApiCaseCoverage(), CalculateUtils.reportPercentage(apiCoverageDTO.getCoverWithApiCase(), apiCoverageDTO.getAllApiCount()));
Assertions.assertEquals(apiCoverageDTO.getCoverWithApiScenario(), 8);
Assertions.assertEquals(apiCoverageDTO.getUnCoverWithApiScenario(), 12);
Assertions.assertEquals(apiCoverageDTO.getScenarioCoverage(), CalculateUtils.reportPercentage(apiCoverageDTO.getCoverWithApiScenario(), apiCoverageDTO.getAllApiCount()));
Assertions.assertEquals(apiCoverageDTO.getCoverWithApiDefinition(), 10);
Assertions.assertEquals(apiCoverageDTO.getUnCoverWithApiDefinition(), 10);
Assertions.assertEquals(apiCoverageDTO.getApiCoverage(), CalculateUtils.reportPercentage(apiCoverageDTO.getCoverWithApiDefinition(), apiCoverageDTO.getAllApiCount()));
}
}

View File

@ -173,7 +173,7 @@ public class ApiReportControllerTests extends BaseTest {
Assertions.assertNotNull(returnPager);
//返回值的页码和当前页码相同
Assertions.assertEquals(returnPager.getCurrent(), request.getCurrent());
;
//返回的数据量不超过规定要返回的数据量相同
Assertions.assertTrue(((List<ApiScenarioDTO>) returnPager.getList()).size() <= request.getPageSize());
//过滤

View File

@ -2144,6 +2144,18 @@ public class ApiScenarioControllerTests extends BaseTest {
batchRequest.setEnable(false);
this.requestPostWithOk(batchUrl, batchRequest);
apiScenarioBatchOperationTestService.checkSchedule(BATCH_OPERATION_SCENARIO_ID.getFirst(), batchRequest.isEnable());
// 仅仅是开启/关闭
batchRequest = new ApiScenarioBatchScheduleConfigRequest();
batchRequest.setProjectId(DEFAULT_PROJECT_ID);
batchRequest.setSelectIds(List.of(BATCH_OPERATION_SCENARIO_ID.getFirst()));
batchRequest.setEnable(true);
this.requestPostWithOk(batchUrl, batchRequest);
apiScenarioBatchOperationTestService.checkSchedule(BATCH_OPERATION_SCENARIO_ID.getFirst(), batchRequest.isEnable());
batchRequest.setEnable(false);
this.requestPostWithOk(batchUrl, batchRequest);
apiScenarioBatchOperationTestService.checkSchedule(BATCH_OPERATION_SCENARIO_ID.getFirst(), batchRequest.isEnable());
//增加日志检查
LOG_CHECK_LIST.add(
new CheckLogModel(scenarioId, OperationLogType.UPDATE, "/api/scenario/schedule-config")
@ -2151,17 +2163,6 @@ public class ApiScenarioControllerTests extends BaseTest {
LOG_CHECK_LIST.add(
new CheckLogModel(BATCH_OPERATION_SCENARIO_ID.getFirst(), OperationLogType.UPDATE, "/api/scenario/batch-operation/schedule-config")
);
// 批量定时任务的开关
ApiScenarioBatchEditRequest batchEditRequest = new ApiScenarioBatchEditRequest();
batchEditRequest.setProjectId(DEFAULT_PROJECT_ID);
batchEditRequest.setType("Schedule");
batchEditRequest.setScheduleOpen(true);
batchEditRequest.setSelectIds(List.of(BATCH_OPERATION_SCENARIO_ID.getFirst()));
requestPostAndReturn(BATCH_EDIT, batchEditRequest);
apiScenarioBatchOperationTestService.checkSchedule(BATCH_OPERATION_SCENARIO_ID.getFirst(), batchEditRequest.isScheduleOpen());
batchEditRequest.setScheduleOpen(false);
requestPostAndReturn(BATCH_EDIT, batchEditRequest);
apiScenarioBatchOperationTestService.checkSchedule(BATCH_OPERATION_SCENARIO_ID.getFirst(), batchEditRequest.isScheduleOpen());
//关闭
request.setEnable(false);
result = this.requestPostAndReturn(testUrl, request);
@ -2240,11 +2241,6 @@ public class ApiScenarioControllerTests extends BaseTest {
request.setScenarioId(IDGenerator.nextStr());
this.requestPost(testUrl, request).andExpect(status().is5xxServerError());
//反例不配置cron表达式
request = new ApiScenarioScheduleConfigRequest();
request.setScenarioId(scenarioId);
this.requestPost(testUrl, request).andExpect(status().isBadRequest());
//反例配置错误的cron表达式测试是否会关闭定时任务
request = new ApiScenarioScheduleConfigRequest();
request.setScenarioId(scenarioId);

View File

@ -6,18 +6,7 @@
class="ms-modal-upload ms-modal-medium"
:width="400"
>
<div class="mb-[16px] flex items-center gap-[16px]">
<div
v-for="item of platformList"
:key="item.value"
:class="`import-item ${exportPlatform === item.value ? 'import-item--active' : ''}`"
@click="() => setActiveImportFormat(item.value)"
>
<div class="text-[var(--color-text-1)]">{{ item.name }}</div>
</div>
</div>
<div v-show="exportPlatform === 'MeterSphere'" class="mb-[16px] flex items-center gap-[8px]">
<div class="mb-[16px] flex items-center gap-[8px]">
<a-switch v-model:model-value="exportTypeRadio" size="small"></a-switch>
{{ t('apiScenario.export.type.all') }}
<a-tooltip :content="t('apiScenario.export.simple.tooltip')" position="tl">
@ -58,18 +47,6 @@
import useAppStore from '@/store/modules/app';
import { downloadByteFile, getGenerateId } from '@/utils';
import { RequestImportFormat } from '@/enums/apiEnum';
const platformList: { name: string; value: RequestImportFormat.MeterSphere | RequestImportFormat.Jmeter }[] = [
{
name: 'MeterSphere',
value: RequestImportFormat.MeterSphere,
},
{
name: 'Jmeter',
value: RequestImportFormat.Jmeter,
},
];
const appStore = useAppStore();
const { t } = useI18n();
@ -84,13 +61,9 @@
const exportLoading = ref(false);
const exportTypeRadio = ref(false);
const exportPlatform = ref(RequestImportFormat.MeterSphere);
function cancelExport() {
visible.value = false;
}
function setActiveImportFormat(format: RequestImportFormat.MeterSphere | RequestImportFormat.Jmeter) {
exportPlatform.value = format;
}
const websocket = ref<WebSocket>();
const reportId = ref('');
const isShowExportingMessage = ref(false); //
@ -217,7 +190,7 @@
sort: props.sorter || {},
fileId: reportId.value,
},
exportPlatform.value
'METERSPHERE'
);
showExportingMessage(res);
visible.value = false;