fix(接口测试): 接口与用例对比接口

--task=1015861 --user=陈建星 【接口测试】接口用例支持同步更新接口变更-是否与定义不一致查询接口 https://www.tapd.cn/55049933/s/1557706
This commit is contained in:
AgAngle 2024-08-05 14:29:15 +08:00 committed by 刘瑞斌
parent 4033ffea7a
commit f0a3cceb44
10 changed files with 348 additions and 41 deletions

View File

@ -17,10 +17,9 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class XMLUtils {
public static final boolean IS_TRANS = false;
@ -142,4 +141,36 @@ public class XMLUtils {
}
return result;
}
/**
* 递归清空元素的文本内容
*/
public static void clearElementText(Element element) {
// 清空当前元素的文本内容
element.setText(StringUtils.EMPTY);
// 递归处理子元素
Iterator<Element> iterator = element.elementIterator();
while (iterator.hasNext()) {
clearElementText(iterator.next());
}
}
/**
* 使用正则清空元素的文本内容
*/
public static String clearElementText(String text) {
// 正则表达式匹配 XML 标签中的内容
String regex = "(<[^<>]+>)([^<>]*)(</[^<>]+>)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
// 替换内容为空字符串
StringBuffer result = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(result, matcher.group(1) + StringUtils.EMPTY + matcher.group(3));
}
matcher.appendTail(result);
return result.toString();
}
}

View File

@ -4,6 +4,7 @@ import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.api.constants.ApiConstants;
import io.metersphere.api.domain.ApiTestCase;
import io.metersphere.api.dto.ApiCaseCompareData;
import io.metersphere.api.dto.ReferenceDTO;
import io.metersphere.api.dto.ReferenceRequest;
import io.metersphere.api.dto.definition.*;
@ -308,4 +309,12 @@ public class ApiTestCaseController {
public void clearApiChange(@PathVariable String id) {
apiTestCaseService.clearApiChange(id);
}
@GetMapping("/api/compare/{id}")
@Operation(summary = "与接口定义对比")
@RequiresPermissions(value = PermissionConstants.PROJECT_API_DEFINITION_CASE_READ)
@CheckOwner(resourceId = "#id", resourceType = "api_test_case")
public ApiCaseCompareData getApiCompareData(@PathVariable String id) {
return apiTestCaseService.getApiCompareData(id);
}
}

View File

@ -0,0 +1,14 @@
package io.metersphere.api.dto;
import io.metersphere.plugin.api.spi.MsTestElement;
import lombok.Data;
/**
* @Author: jianxing
* @CreateTime: 2024-08-02 17:49
*/
@Data
public class ApiCaseCompareData {
private MsTestElement apiRequest;
private MsTestElement caseRequest;
}

View File

@ -16,8 +16,6 @@ import org.apache.jmeter.protocol.http.util.HTTPFileArg;
import java.io.File;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Author: jianxing
@ -80,29 +78,6 @@ public abstract class MsBodyConverter<T> {
return fileArg;
}
/**
* 将文本中的 @xxx 转换成 ${__Mock(@xxx)}
*
* @param text
* @return
*/
protected String parseTextMock(String text) {
String pattern = "[\"\\s:]@[a-zA-Z\\\\(|,'-\\\\d ]*[a-zA-Z)-9),\\\\\"]";
Pattern regex = Pattern.compile(pattern);
Matcher matcher = regex.matcher(text);
while (matcher.find()) {
//取出group的最后一个字符 主要是防止 @string|number @string 这种情况
//如果是 或者, 结尾的 需要截取
String group = matcher.group();
if (group.endsWith(",") || group.endsWith("\"")) {
group = group.substring(0, group.length() - 1);
}
// 去掉第一个字符因为第一个字符是 " : 或者空格
group = group.substring(1, group.length());
text = text.replace(group, StringUtils.join("${__Mock(", group.replace(",", "\\,"), ")}"));
}
return text;
}
/**
* 处理raw格式参数
* 包含了 json 等格式

View File

@ -60,6 +60,30 @@ public class MsJsonBodyConverter extends MsBodyConverter<JsonBody> {
return jsonStr;
}
/**
* 将文本中的 @xxx 转换成 ${__Mock(@xxx)}
*
* @param text
* @return
*/
protected String parseTextMock(String text) {
String pattern = "[\"\\s:]@[a-zA-Z\\\\(|,'-\\\\d ]*[a-zA-Z)-9),\\\\\"]";
Pattern regex = Pattern.compile(pattern);
Matcher matcher = regex.matcher(text);
while (matcher.find()) {
//取出group的最后一个字符 主要是防止 @string|number @string 这种情况
//如果是 或者, 结尾的 需要截取
String group = matcher.group();
if (group.endsWith(",") || group.endsWith("\"")) {
group = group.substring(0, group.length() - 1);
}
// 去掉第一个字符因为第一个字符是 " : 或者空格
group = group.substring(1, group.length());
text = text.replace(group, StringUtils.join("${__Mock(", group.replace(",", "\\,"), ")}"));
}
return text;
}
private void parseMock(List list) {
Map<Integer, String> replaceDataMap = new HashMap<>();
for (int index = 0; index < list.size(); index++) {

View File

@ -81,11 +81,11 @@ public class ApiReportSendNoticeService {
ApiExecuteResourceType.API_SCENARIO.name(), ApiExecuteResourceType.TEST_PLAN_API_SCENARIO.name(), ApiExecuteResourceType.PLAN_RUN_API_SCENARIO.name())) {
ApiScenario scenario = null;
switch (ApiExecuteResourceType.valueOf(noticeDTO.getResourceType())) {
case ApiExecuteResourceType.API_SCENARIO ->
case API_SCENARIO ->
scenario = apiScenarioMapper.selectByPrimaryKey(noticeDTO.getResourceId());
case ApiExecuteResourceType.TEST_PLAN_API_SCENARIO ->
case TEST_PLAN_API_SCENARIO ->
scenario = extApiScenarioMapper.getScenarioByResourceId(noticeDTO.getResourceId());
case ApiExecuteResourceType.PLAN_RUN_API_SCENARIO ->
case PLAN_RUN_API_SCENARIO ->
scenario = extApiScenarioMapper.getScenarioByReportId(noticeDTO.getResourceId());
default -> {
}
@ -109,11 +109,11 @@ public class ApiReportSendNoticeService {
ApiExecuteResourceType.API_CASE.name(), ApiExecuteResourceType.TEST_PLAN_API_CASE.name(), ApiExecuteResourceType.PLAN_RUN_API_CASE.name())) {
ApiTestCase testCase = null;
switch (ApiExecuteResourceType.valueOf(noticeDTO.getResourceType())) {
case ApiExecuteResourceType.API_CASE ->
case API_CASE ->
testCase = apiTestCaseMapper.selectByPrimaryKey(noticeDTO.getResourceId());
case ApiExecuteResourceType.TEST_PLAN_API_CASE ->
case TEST_PLAN_API_CASE ->
testCase = extApiTestCaseMapper.getCaseByResourceId(noticeDTO.getResourceId());
case ApiExecuteResourceType.PLAN_RUN_API_CASE ->
case PLAN_RUN_API_CASE ->
testCase = extApiTestCaseMapper.getCaseByReportId(noticeDTO.getResourceId());
default -> {
}

View File

@ -7,6 +7,7 @@ import io.metersphere.api.dto.debug.ApiFileResourceUpdateRequest;
import io.metersphere.api.dto.debug.ApiResourceRunRequest;
import io.metersphere.api.dto.definition.*;
import io.metersphere.api.dto.request.ApiTransferRequest;
import io.metersphere.api.dto.request.http.MsHTTPElement;
import io.metersphere.api.mapper.*;
import io.metersphere.api.service.ApiCommonService;
import io.metersphere.api.service.ApiExecuteService;
@ -241,14 +242,14 @@ public class ApiTestCaseService extends MoveNodeService {
example.createCriteria().andCaseIdEqualTo(id).andUserIdEqualTo(userId);
List<ApiTestCaseFollower> followers = apiTestCaseFollowerMapper.selectByExample(example);
apiTestCaseDTO.setFollow(CollectionUtils.isNotEmpty(followers));
AbstractMsTestElement msTestElement = ApiDataUtils.parseObject(new String(testCaseBlob.getRequest()), AbstractMsTestElement.class);
AbstractMsTestElement msTestElement = getTestElement(testCaseBlob);
apiCommonService.setLinkFileInfo(id, msTestElement);
apiCommonService.setEnableCommonScriptProcessorInfo(msTestElement);
apiCommonService.setApiDefinitionExecuteInfo(msTestElement, apiDefinition);
apiTestCaseDTO.setRequest(msTestElement);
ApiDefinitionBlob apiDefinitionBlob = apiDefinitionBlobMapper.selectByPrimaryKey(apiDefinition.getId());
AbstractMsTestElement apiMsTestElement = ApiDataUtils.parseObject(new String(apiDefinitionBlob.getRequest()), AbstractMsTestElement.class);
AbstractMsTestElement apiMsTestElement = getApiMsTestElement(apiDefinitionBlob);
apiTestCaseDTO.setInconsistentWithApi(HttpRequestParamDiffUtils.isRequestParamDiff(apiMsTestElement, msTestElement));
return apiTestCaseDTO;
}
@ -649,7 +650,7 @@ public class ApiTestCaseService extends MoveNodeService {
if (apiTestCaseBlob == null) {
return;
}
AbstractMsTestElement msTestElement = ApiDataUtils.parseObject(new String(apiTestCaseBlob.getRequest()), AbstractMsTestElement.class);
AbstractMsTestElement msTestElement = getTestElement(apiTestCaseBlob);
// 获取接口中需要更新的文件
List<ApiFile> updateFiles = apiCommonService.getApiFilesByFileId(originFileAssociation.getFileId(), msTestElement);
// 替换文件的Id和name
@ -695,7 +696,7 @@ public class ApiTestCaseService extends MoveNodeService {
ApiTestCaseBlob apiTestCaseBlob = apiTestCaseBlobMapper.selectByPrimaryKey(id);
ApiResourceRunRequest runRequest = new ApiResourceRunRequest();
runRequest.setTestElement(ApiDataUtils.parseObject(new String(apiTestCaseBlob.getRequest()), AbstractMsTestElement.class));
runRequest.setTestElement(getTestElement(apiTestCaseBlob));
return executeRun(runRequest, apiTestCase, reportId, userId);
}
@ -784,7 +785,7 @@ public class ApiTestCaseService extends MoveNodeService {
apiParamConfig.setRetryOnFail(request.getRunModeConfig().getRetryOnFail());
apiParamConfig.setRetryConfig(request.getRunModeConfig().getRetryConfig());
AbstractMsTestElement msTestElement = ApiDataUtils.parseObject(new String(apiTestCaseBlob.getRequest()), AbstractMsTestElement.class);
AbstractMsTestElement msTestElement = getTestElement(apiTestCaseBlob);
// 设置 method 等信息
apiCommonService.setApiDefinitionExecuteInfo(msTestElement, BeanUtils.copyBean(new ApiDefinitionExecuteInfo(), apiDefinition));
@ -951,4 +952,30 @@ public class ApiTestCaseService extends MoveNodeService {
apiTestCase.setApiChange(false);
apiTestCaseMapper.updateByPrimaryKeySelective(apiTestCase);
}
public ApiCaseCompareData getApiCompareData(String id) {
ApiTestCase apiTestCase = checkResourceExist(id);
ApiDefinition apiDefinition = getApiDefinition(apiTestCase.getApiDefinitionId());
ApiDefinitionBlob apiDefinitionBlob = apiDefinitionBlobMapper.selectByPrimaryKey(apiDefinition.getId());
AbstractMsTestElement apiMsTestElement = getApiMsTestElement(apiDefinitionBlob);
ApiTestCaseBlob apiTestCaseBlob = apiTestCaseBlobMapper.selectByPrimaryKey(id);
AbstractMsTestElement apiTestCaseMsTestElement = getTestElement(apiTestCaseBlob);
// 其他协议不处理
if (apiMsTestElement instanceof MsHTTPElement apiHttpTestElement && apiTestCaseMsTestElement instanceof MsHTTPElement apiCaseHttpTestElement) {
apiMsTestElement = HttpRequestParamDiffUtils.getCompareHttpElement(apiHttpTestElement);
apiTestCaseMsTestElement = HttpRequestParamDiffUtils.getCompareHttpElement(apiCaseHttpTestElement);
}
ApiCaseCompareData apiCaseCompareData = new ApiCaseCompareData();
apiCaseCompareData.setApiRequest(apiMsTestElement);
apiCaseCompareData.setCaseRequest(apiTestCaseMsTestElement);
return apiCaseCompareData;
}
private AbstractMsTestElement getApiMsTestElement(ApiDefinitionBlob apiDefinitionBlob) {
return ApiDataUtils.parseObject(new String(apiDefinitionBlob.getRequest()), AbstractMsTestElement.class);
}
private AbstractMsTestElement getTestElement(ApiTestCaseBlob apiTestCaseBlob) {
return ApiDataUtils.parseObject(new String(apiTestCaseBlob.getRequest()), AbstractMsTestElement.class);
}
}

View File

@ -4,13 +4,18 @@ import io.metersphere.api.dto.request.http.MsHTTPElement;
import io.metersphere.api.dto.request.http.body.Body;
import io.metersphere.api.dto.request.http.body.JsonBody;
import io.metersphere.api.dto.request.http.body.XmlBody;
import io.metersphere.plugin.api.spi.AbstractMsTestElement;
import io.metersphere.project.api.KeyValueParam;
import io.metersphere.sdk.util.EnumValidator;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.XMLUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Element;
import org.dom4j.io.XMLWriter;
import java.io.StringWriter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -81,6 +86,7 @@ public class HttpRequestParamDiffUtils {
Object json2 = JSON.parseObject(jsonValue2);
return !getBlankJon(json1).equals(getBlankJon(json2));
} catch (Exception e) {
LogUtils.info("json 解析异常json1: {}, json2: {}", jsonValue1, jsonValue2);
return !getJsonKeys(jsonValue1).equals(getJsonKeys(jsonValue2));
}
}
@ -163,4 +169,64 @@ public class HttpRequestParamDiffUtils {
.collect(Collectors.toSet());
return !keSet1.equals(keSet2);
}
/**
* json xml 属性值置空
* 便于前端比较差异
* @param httpElement
* @return
*/
public static AbstractMsTestElement getCompareHttpElement(MsHTTPElement httpElement) {
Body body = httpElement.getBody();
if (body == null) {
return httpElement;
}
if (StringUtils.equals(body.getBodyType(), Body.BodyType.JSON.name())) {
try {
String jsonValue = body.getJsonBody().getJsonValue();
jsonValue = replaceIllegalJsonWithMock(jsonValue);
Object blankJon = getBlankJon(JSON.parseObject(jsonValue));
body.getJsonBody().setJsonValue(JSON.toJSONString(blankJon));
} catch (Exception e) {
LogUtils.info("json 解析异常json: {}", body.getJsonBody().getJsonValue());
}
}
if (StringUtils.equals(body.getBodyType(), Body.BodyType.XML.name())) {
String xml = body.getXmlBody().getValue();
try {
Element element = XMLUtils.stringToDocument(xml).getRootElement();
XMLUtils.clearElementText(element);
StringWriter stringWriter = new StringWriter();
XMLWriter writer = new XMLWriter(stringWriter);
writer.write(element);
xml = stringWriter.toString();
} catch (Exception e) {
LogUtils.info("xml 解析异常xml: {}", body.getXmlBody().getValue());
xml = XMLUtils.clearElementText(xml);
}
body.getXmlBody().setValue(xml);
}
return httpElement;
}
/**
* 如果 json 串中包含了非字符串的 mock 函数
* 例如
* {"a": @integer(1, 2)}
* 替换成空字符串
* {"a": ""}
* 避免 json 序列化失败
* @param text
* @return
*/
public static String replaceIllegalJsonWithMock(String text) {
String pattern = ":\\s*(@\\w+(\\(\\s*\\w*\\s*,?\\s*\\w*\\s*\\))*)";
Pattern regex = Pattern.compile(pattern);
Matcher matcher = regex.matcher(text);
while (matcher.find()) {
// 这里连同:一起替换避免替换了其他合规的参数值
text = text.replaceFirst(pattern, ":\"\"");
}
return text;
}
}

View File

@ -110,7 +110,8 @@ public class ApiTestCaseControllerTests extends BaseTest {
private static final String RUN_GET = "run/{0}";
private static final String RUN_POST = "run";
private static final String BATCH_RUN = "batch/run";
private static final String API_CHANGE_CLEAR = "api-change/clear/{id}";
private static final String API_CHANGE_CLEAR = "api-change/clear/{0}";
private static final String API_COMPARE = "api/compare/{0}";
private static final ResultMatcher ERROR_REQUEST_MATCHER = status().is5xxServerError();
private static ApiTestCase apiTestCase;
@ -438,6 +439,18 @@ public class ApiTestCaseControllerTests extends BaseTest {
Assertions.assertEquals(apiTestCaseMapper.selectByPrimaryKey(apiTestCase.getId()).getApiChange(), true);
}
@Test
@Order(3)
public void getApiCompareData() throws Exception {
// @@请求成功
MvcResult mvcResult = this.requestGetWithOkAndReturn(API_COMPARE, apiTestCase.getId());
Map apiCaseCompareData = (Map) parseResponse(mvcResult).get("data");
Assertions.assertNotNull(apiCaseCompareData.get("apiRequest"));
Assertions.assertNotNull(apiCaseCompareData.get("caseRequest"));
// @@校验权限
requestGetPermissionTest(PermissionConstants.PROJECT_API_DEFINITION_CASE_READ, API_COMPARE, apiTestCase.getId());
}
@Test
@Order(3)
public void clearApiChange() throws Exception {

View File

@ -7,9 +7,13 @@ import io.metersphere.api.dto.request.http.RestParam;
import io.metersphere.api.dto.request.http.body.*;
import io.metersphere.project.api.KeyValueParam;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.XMLUtils;
import org.dom4j.Element;
import org.dom4j.io.XMLWriter;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -332,4 +336,148 @@ public class HttpRequestParamDiffUtilsTests {
msHTTPElement1.setBody(new Body());
Assertions.assertTrue(HttpRequestParamDiffUtils.isRequestParamDiff(msHTTPElement1, msHTTPElement2));
}
@Test
public void replaceIllegalJsonWithMock() {
String replaceJon = HttpRequestParamDiffUtils.replaceIllegalJsonWithMock("""
{
"id": "dddd",
"age": 10,
"name": @integer(1, 10),
"category": {
"id":@integer(1),
"name": @integer,
"title": @integer(1, 10)
}
}
""");
Object result = JSON.parseObject("""
{
"id": "dddd",
"age": 10,
"name": "",
"category": {
"id": "",
"name": "",
"title": ""
}
}""");
Assertions.assertEquals(JSON.parseObject(replaceJon), result);
}
@Test
public void clearElementText() throws Exception {
String xml = """
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.8</version>
<relativePath/>
</parent>
</project>
""";
Element element = XMLUtils.stringToDocument(xml).getRootElement();
XMLUtils.clearElementText(element);
StringWriter stringWriter = new StringWriter();
XMLWriter writer = new XMLWriter(stringWriter);
writer.write(element);
stringWriter.toString();
String result = """
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion></modelVersion><parent><groupId></groupId><artifactId></artifactId><version></version><relativePath></relativePath></parent></project>""";
Assertions.assertEquals(stringWriter.toString(), result);
String regexResult = """
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion></modelVersion>
<parent>
<groupId></groupId>
<artifactId></artifactId>
<version></version>
<relativePath/></parent>
</project>
""";
Assertions.assertEquals(XMLUtils.clearElementText(xml), regexResult);
}
@Test
public void getCompareHttpElement() {
MsHTTPElement msHTTPElement = new MsHTTPElement();
HttpRequestParamDiffUtils.getCompareHttpElement(msHTTPElement);
msHTTPElement.setBody(new Body());
Body body = msHTTPElement.getBody();
body.setJsonBody(new JsonBody());
body.setBodyType(Body.BodyType.RAW.name());
HttpRequestParamDiffUtils.getCompareHttpElement(msHTTPElement);
body.setBodyType(Body.BodyType.JSON.name());
String jsonValue = """
{
"id": "dddd",
"age": 10,
"name": @integer(1, 10),
"category": {
"id":@integer(1),
"name": @integer,
"title": @integer(1, 10)
}
}
""";
body.getJsonBody().setJsonValue(jsonValue);
HttpRequestParamDiffUtils.getCompareHttpElement(msHTTPElement);
Assertions.assertEquals(JSON.parseObject(body.getJsonBody().getJsonValue()), JSON.parseObject("""
{
"id": "",
"age": "",
"name": "",
"category": {
"id": "",
"name": "",
"title": ""
}
}
"""));
String xmlValue = """
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org</groupId>
<artifactId>spring</artifactId>
<version>3.2.8</version>
<relativePath/>
</parent>
</project>
""";
msHTTPElement.getBody().setBodyType(Body.BodyType.XML.name());
body.setXmlBody(new XmlBody());
body.getXmlBody().setValue(xmlValue);
HttpRequestParamDiffUtils.getCompareHttpElement(msHTTPElement);
xmlValue = """
dx
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org</groupId>
<artifactId>spring</artifactId>
<version>3.2.8</version>
<relativePath/>
</parent>
</project>
""";
body.getXmlBody().setValue(xmlValue);
HttpRequestParamDiffUtils.getCompareHttpElement(msHTTPElement);
}
}