feat(接口测试): json-schema自动生成测试数据

--story=1015366 --user=陈建星 【接口测试】json-schema组件 https://www.tapd.cn/55049933/s/1547748
This commit is contained in:
AgAngle 2024-07-15 18:04:52 +08:00 committed by Craftsman
parent e4864184f8
commit f4b277125c
14 changed files with 215 additions and 52 deletions

View File

@ -94,6 +94,12 @@
</exclusion>
</exclusions>
</dependency>
<!-- 根据正则表达式,生成随机字符串 -->
<dependency>
<groupId>com.github.krraghavan</groupId>
<artifactId>xeger</artifactId>
<version>${xeger.version}</version>
</dependency>
</dependencies>
<build>

View File

@ -278,13 +278,20 @@ public class ApiDefinitionController {
return apiFileResourceService.transfer(request, SessionUtils.getUserId(), apiDefinitionDir);
}
@PostMapping("/preview")
@PostMapping("/json-schema/preview")
@Operation(summary = "接口测试-接口管理-接口-json-schema-预览")
@RequiresPermissions(PermissionConstants.PROJECT_API_DEFINITION_READ)
public String preview(@RequestBody JsonSchemaItem jsonSchemaItem) {
return apiDefinitionService.preview(jsonSchemaItem);
}
@PostMapping("/json-schema/auto-generate")
@Operation(summary = "接口测试-接口管理-接口-json-schema-自动生成测试数据")
@RequiresPermissions(PermissionConstants.PROJECT_API_DEFINITION_READ)
public String jsonSchemaAutoGenerate(@RequestBody JsonSchemaItem jsonSchemaItem) {
return apiDefinitionService.jsonSchemaAutoGenerate(jsonSchemaItem);
}
@PostMapping("/debug")
@Operation(summary = "接口调试")
@RequiresPermissions(PermissionConstants.PROJECT_API_DEFINITION_EXECUTE)

View File

@ -106,7 +106,7 @@ public class JsonSchemaItem {
/**
* 参数值的枚举
*/
private List<? extends Object> enumValues;
private List<String> enumValues;
/**
* 是否启用
*/

View File

@ -310,7 +310,7 @@ public class Swagger3Parser extends ApiImportAbstractParser<ApiDefinitionImport>
} else {
String jsonString = JSON.toJSONString(jsonSchemaItem);
if (StringUtils.isNotBlank(jsonString)) {
jsonBody.setJsonValue(JsonSchemaBuilder.jsonSchemaToJson(jsonString));
jsonBody.setJsonValue(JsonSchemaBuilder.jsonSchemaToJson(jsonString, true));
}
}
return jsonBody;
@ -663,7 +663,10 @@ public class Swagger3Parser extends ApiImportAbstractParser<ApiDefinitionImport>
jsonSchemaItem.setFormat(StringUtils.isNotBlank(integerSchema.getFormat()) ? integerSchema.getFormat() : StringUtils.EMPTY);
jsonSchemaItem.setMaximum(integerSchema.getMaximum());
jsonSchemaItem.setMinimum(integerSchema.getMinimum());
jsonSchemaItem.setEnumValues(integerSchema.getEnum());
List<Number> enumValues = integerSchema.getEnum();
if (CollectionUtils.isNotEmpty(enumValues)) {
jsonSchemaItem.setEnumValues(enumValues.stream().map(item -> item.toString()).toList());
}
return jsonSchemaItem;
}

View File

@ -1195,6 +1195,14 @@ public class ApiDefinitionService extends MoveNodeService {
return JsonSchemaBuilder.preview(JSON.toJSONString(jsonSchemaItem));
}
public String jsonSchemaAutoGenerate(JsonSchemaItem jsonSchemaItem) {
if (BooleanUtils.isFalse(jsonSchemaItem.getEnable())) {
return "{}";
}
filterDisableItem(jsonSchemaItem);
return JsonSchemaBuilder.jsonSchemaAutoGenerate(JSON.toJSONString(jsonSchemaItem));
}
private void filterDisableItem(JsonSchemaItem jsonSchemaItem) {
if (isObjectItem(jsonSchemaItem)) {
Map<String, JsonSchemaItem> properties = jsonSchemaItem.getProperties();

View File

@ -2,26 +2,26 @@ package io.metersphere.api.utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.*;
import io.metersphere.jmeter.mock.Mock;
import io.metersphere.project.constants.PropertyConstant;
import nl.flotsam.xeger.Xeger;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.RandomStringGenerator;
import org.jetbrains.annotations.NotNull;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Random;
public class JsonSchemaBuilder {
public static String jsonSchemaToJson(String jsonSchemaString) {
public static String jsonSchemaToJson(String jsonSchemaString, boolean isPreview) {
// 解析 JSON Schema 字符串为 JsonNode
JsonNode jsonSchemaNode = ApiDataUtils.readTree(jsonSchemaString);
Map<String, String> processMap = new HashMap<>();
// 生成符合 JSON Schema JSON
JsonNode jsonNode = generateJson(jsonSchemaNode, processMap);
JsonNode jsonNode = generateJson(jsonSchemaNode, processMap, isPreview);
String jsonString = ApiDataUtils.writerWithDefaultPrettyPrinter(jsonNode);
if (MapUtils.isNotEmpty(processMap)) {
for (String str : processMap.keySet()) {
@ -31,13 +31,13 @@ public class JsonSchemaBuilder {
return jsonString;
}
private static JsonNode generateJson(JsonNode jsonSchemaNode, Map<String, String> processMap) {
private static JsonNode generateJson(JsonNode jsonSchemaNode, Map<String, String> processMap, boolean isPreview) {
ObjectNode jsonNode = ApiDataUtils.createObjectNode();
if (jsonSchemaNode instanceof NullNode) {
return NullNode.getInstance();
}
String type = jsonSchemaNode.get(PropertyConstant.TYPE) == null ? StringUtils.EMPTY : jsonSchemaNode.get(PropertyConstant.TYPE).asText();
String type = getPropertyTextValue(jsonSchemaNode, PropertyConstant.TYPE);
if (StringUtils.equals(type, PropertyConstant.OBJECT)) {
JsonNode propertiesNode = jsonSchemaNode.get(PropertyConstant.PROPERTIES);
// 遍历 properties
@ -46,7 +46,8 @@ public class JsonSchemaBuilder {
String propertyName = entry.getKey();
JsonNode propertyNode = entry.getValue();
// 根据属性类型生成对应的值
JsonNode valueNode = generateValue(entry.getKey(), propertyNode, processMap);
JsonNode valueNode = isPreview ? generateValueForPreview(entry.getKey(), propertyNode, processMap)
: generateValue(entry.getKey(), propertyNode, processMap);
// 将属性和值添加到 JSON 对象节点
jsonNode.set(propertyName, valueNode);
});
@ -55,23 +56,27 @@ public class JsonSchemaBuilder {
JsonNode items = jsonSchemaNode.get(PropertyConstant.ITEMS);
if (items != null) {
ArrayNode arrayNode = new ArrayNode(JsonNodeFactory.instance);
items.forEach(item -> arrayNode.add(generateValue(null, item, processMap)));
items.forEach(item -> {
JsonNode valueNode = isPreview ? generateValueForPreview(null, item, processMap)
: generateValue(null, item, processMap);
arrayNode.add(valueNode);
});
return arrayNode;
}
}
return jsonNode;
}
private static JsonNode generateValue(String propertyName, JsonNode propertyNode, Map<String, String> processMap) {
private static JsonNode generateValueForPreview(String propertyName, JsonNode propertyNode, Map<String, String> processMap) {
// 获取属性类型
if (propertyNode instanceof NullNode) {
return NullNode.getInstance();
}
String type = propertyNode.get(PropertyConstant.TYPE) == null ? StringUtils.EMPTY : propertyNode.get(PropertyConstant.TYPE).asText();
String value = propertyNode.get(PropertyConstant.EXAMPLE) == null ? StringUtils.EMPTY : propertyNode.get(PropertyConstant.EXAMPLE).asText();
String type = getPropertyTextValue(propertyNode, PropertyConstant.TYPE);
String value = getPropertyTextValue(propertyNode, PropertyConstant.EXAMPLE);
return switch (type) {
case PropertyConstant.STRING ->
new TextNode(!StringUtils.equals(value, PropertyConstant.NULL) ? value : "string");
new TextNode(StringUtils.isBlank(value) ? "string" : value);
case PropertyConstant.INTEGER -> {
if (isVariable(value)) {
yield getJsonNodes(propertyName, processMap, value);
@ -97,8 +102,112 @@ public class JsonSchemaBuilder {
yield BooleanNode.valueOf(propertyNode.get(PropertyConstant.EXAMPLE).asBoolean());
}
}
case PropertyConstant.OBJECT -> generateJson(propertyNode, processMap, true);
case PropertyConstant.ARRAY -> {
ArrayNode arrayNode = new ArrayNode(JsonNodeFactory.instance);
JsonNode items = propertyNode.get(PropertyConstant.ITEMS);
if (items != null) {
items.forEach(item -> arrayNode.add(generateValueForPreview(null, item, processMap)));
}
yield arrayNode;
}
default -> NullNode.getInstance();
};
case PropertyConstant.OBJECT -> generateJson(propertyNode, processMap);
}
private static JsonNode generateValue(String propertyName, JsonNode propertyNode, Map<String, String> processMap) {
// 获取属性类型
if (propertyNode instanceof NullNode) {
return NullNode.getInstance();
}
String type = getPropertyTextValue(propertyNode, PropertyConstant.TYPE);
String value = getPropertyTextValue(propertyNode, PropertyConstant.EXAMPLE);
return switch (type) {
case PropertyConstant.STRING -> {
if (StringUtils.isBlank(value)) {
JsonNode enumValues = propertyNode.get(PropertyConstant.ENUM_VALUES);
JsonNode defaultValue = propertyNode.get(PropertyConstant.DEFAULT_VALUE);
JsonNode pattern = propertyNode.get(PropertyConstant.PATTERN);
JsonNode maxLength = propertyNode.get(PropertyConstant.MAX_LENGTH);
JsonNode minLength = propertyNode.get(PropertyConstant.MIN_LENGTH);
int max = isTextNotBlank(maxLength) ? maxLength.asInt() : 20;
int min = isTextNotBlank(minLength) ? minLength.asInt() : 1;
if (enumValues != null && enumValues instanceof ArrayNode) {
value = enumValues.get(new Random().nextInt(enumValues.size())).asText();
} else if (isTextNotBlank(defaultValue)) {
value = defaultValue.asText();
} else if (isTextNotBlank(pattern)) {
Xeger generator = new Xeger(pattern.asText());
value = generator.generate();
} else {
value = RandomStringGenerator.builder().withinRange('0', 'z').build().generate(new Random().nextInt(max - min + 1) + min);
}
}
yield new TextNode(value);
}
case PropertyConstant.INTEGER -> {
if (StringUtils.isBlank(value)) {
JsonNode enumValues = propertyNode.get(PropertyConstant.ENUM_VALUES);
JsonNode defaultValue = propertyNode.get(PropertyConstant.DEFAULT_VALUE);
if (enumValues != null && enumValues instanceof ArrayNode) {
value = enumValues.get(new Random().nextInt(enumValues.size())).asText();
} else if (isTextNotBlank(defaultValue)) {
value = defaultValue.asText();
} else {
JsonNode maximum = propertyNode.get(PropertyConstant.MAXIMUM);
JsonNode minimum = propertyNode.get(PropertyConstant.MINIMUM);
int max = isTextNotBlank(maximum) ? maximum.asInt() : Integer.MAX_VALUE;
int min = isTextNotBlank(minimum) ? minimum.asInt() : Integer.MIN_VALUE;
// 这里减去负数可能超过整型最大值使用 Long 类型
value = new Random().nextLong(Long.valueOf(max) - Long.valueOf(min)) + min + StringUtils.EMPTY;
}
} else {
if (isVariable(value)) {
yield getJsonNodes(propertyName, processMap, value);
}
}
try {
yield new IntNode(Integer.valueOf(value));
} catch (Exception e) {
yield new IntNode(propertyNode.get(PropertyConstant.EXAMPLE).asInt());
}
}
case PropertyConstant.NUMBER -> {
if (StringUtils.isBlank(value)) {
JsonNode enumValues = propertyNode.get(PropertyConstant.ENUM_VALUES);
JsonNode defaultValue = propertyNode.get(PropertyConstant.DEFAULT_VALUE);
if (enumValues != null && enumValues instanceof ArrayNode) {
value = enumValues.get(new Random().nextInt(enumValues.size())).asText();
} else if (isTextNotBlank(defaultValue)) {
value = defaultValue.asText();
} else {
JsonNode maximum = propertyNode.get(PropertyConstant.MAXIMUM);
JsonNode minimum = propertyNode.get(PropertyConstant.MINIMUM);
BigDecimal max = isTextNotBlank(maximum) ? new BigDecimal(maximum.asText()) : new BigDecimal(String.valueOf(Float.MAX_VALUE));
BigDecimal min = isTextNotBlank(minimum) ? new BigDecimal(minimum.asText()) : new BigDecimal(String.valueOf(Float.MIN_VALUE));
BigDecimal randomBigDecimal = min.add(new BigDecimal(String.valueOf(Math.random())).multiply(max.subtract(min)));
yield new DecimalNode(randomBigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP));
}
} else {
if (isVariable(value)) {
yield getJsonNodes(propertyName, processMap, value);
}
}
try {
yield new DecimalNode(new BigDecimal(value));
} catch (Exception e) {
yield new DecimalNode(propertyNode.get(PropertyConstant.EXAMPLE).decimalValue());
}
}
case PropertyConstant.BOOLEAN -> {
if (isVariable(value)) {
yield getJsonNodes(propertyName, processMap, value);
} else {
yield BooleanNode.valueOf(propertyNode.get(PropertyConstant.EXAMPLE).asBoolean());
}
}
case PropertyConstant.OBJECT -> generateJson(propertyNode, processMap, true);
case PropertyConstant.ARRAY -> {
ArrayNode arrayNode = new ArrayNode(JsonNodeFactory.instance);
JsonNode items = propertyNode.get(PropertyConstant.ITEMS);
@ -112,6 +221,14 @@ public class JsonSchemaBuilder {
}
private static boolean isTextNotBlank(JsonNode jsonNode) {
return jsonNode != null && !(jsonNode instanceof NullNode) && StringUtils.isNotBlank(jsonNode.asText());
}
private static String getPropertyTextValue(JsonNode propertyNode, String key) {
return propertyNode.get(key) == null ? StringUtils.EMPTY : propertyNode.get(key).asText();
}
private static boolean isVariable(String value) {
return !StringUtils.equals(value, PropertyConstant.NULL) && (value.startsWith("@") || value.startsWith("${"));
}
@ -125,25 +242,10 @@ public class JsonSchemaBuilder {
}
public static String preview(String jsonSchema) {
String jsonString = jsonSchemaToJson(jsonSchema);
//需要匹配到mock函数 然后换成mock数据
if (StringUtils.isNotBlank(jsonString)) {
String pattern = "@[a-zA-Z\\\\(|,'-\\\\d ]*[a-zA-Z)-9),\\\\\"]";
Pattern regex = Pattern.compile(pattern);
Matcher matcher = regex.matcher(jsonString);
while (matcher.find()) {
//取出group的最后一个字符 主要是防止 @string|number @string 这种情况
String group = matcher.group();
String lastChar = null;
if (group.endsWith(",") || group.endsWith("\"")) {
lastChar = group.substring(group.length() - 1);
group = group.substring(0, group.length() - 1);
}
jsonString = jsonString.replace(matcher.group(),
StringUtils.join(Mock.calculate(group), lastChar));
}
}
return jsonString;
return jsonSchemaToJson(jsonSchema, true);
}
public static String jsonSchemaAutoGenerate(String jsonString) {
return jsonSchemaToJson(jsonString, false);
}
}

View File

@ -22,5 +22,12 @@ public class PropertyConstant {
public final static String MOCK = "mock";
public final static String BODY_TYPE = "bodyType";
public final static String PARAM_TYPE = "paramType";
public final static String ENUM_VALUES = "enumValues";
public final static String DEFAULT_VALUE = "defaultValue";
public final static String PATTERN = "pattern";
public final static String MAX_LENGTH = "maxLength";
public final static String MIN_LENGTH = "minLength";
public final static String MINIMUM = "minimum";
public final static String MAXIMUM = "maximum";
}

View File

@ -55,6 +55,7 @@ import {
GetTrashModuleCountUrl,
GetTrashModuleTreeUrl,
ImportDefinitionUrl,
JsonSchemaAutoGenerateUrl,
MockDetailUrl,
MoveModuleUrl,
OperationHistoryUrl,
@ -308,6 +309,11 @@ export function convertJsonSchemaToJson(data: JsonSchema) {
return MSR.post({ url: ConvertJsonSchemaToJsonUrl, data });
}
// json-schema 生成测试数据
export function jsonSchemaAutoGenerate(data: JsonSchema) {
return MSR.post({ url: JsonSchemaAutoGenerateUrl, data });
}
/**
* Mock
*/

View File

@ -36,7 +36,8 @@ export const OperationHistoryUrl = '/api/definition/operation-history'; // 接
export const SaveOperationHistoryUrl = '/api/definition/operation-history/save'; // 接口定义-另存变更历史为指定版本
export const RecoverOperationHistoryUrl = '/api/definition/operation-history/recover'; // 接口定义-变更历史恢复
export const DefinitionReferenceUrl = '/api/definition/get-reference'; // 获取接口引用关系
export const ConvertJsonSchemaToJsonUrl = '/api/definition/preview'; // 将json-schema转换为 json 数据
export const ConvertJsonSchemaToJsonUrl = '/api/definition/json-schema/preview'; // 将json-schema转换为 json 数据
export const JsonSchemaAutoGenerateUrl = '/api/definition/json-schema/auto-generate'; // 将json-schema转换为 json 数据
/**
* Mock

View File

@ -427,7 +427,7 @@
<template v-if="activeRecord.type === 'string'">
<a-form-item :label="t('ms.json.schema.regex')">
<a-input
v-model:model-value="activeRecord.regex"
v-model:model-value="activeRecord.pattern"
:placeholder="t('ms.json.schema.regexPlaceholder', { reg: '/<title(.*?)</title>' })"
@change="handleSettingFormChange"
/>
@ -821,8 +821,8 @@
},
{
title: t('ms.json.schema.regex'),
dataIndex: 'regex',
slotName: 'regex',
dataIndex: 'pattern',
slotName: 'pattern',
inputType: 'input',
size: 'medium',
addLineDisabled: true,

View File

@ -185,7 +185,7 @@
import batchAddKeyVal from '@/views/api-test/components/batchAddKeyVal.vue';
import paramTable, { type ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
import { convertJsonSchemaToJson } from '@/api/modules/api-test/management';
import { jsonSchemaAutoGenerate } from '@/api/modules/api-test/management';
import { requestBodyTypeMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
@ -387,7 +387,7 @@
const schema = parseTableDataToJsonSchema(innerParams.value.jsonBody.jsonSchemaTableData?.[0]);
if (schema) {
// json schema json
const res = await convertJsonSchemaToJson(schema);
const res = await jsonSchemaAutoGenerate(schema);
innerParams.value.jsonBody.jsonValue = res;
emit('change');
} else {

View File

@ -254,7 +254,7 @@
import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
import popConfirm from '@/views/api-test/components/popConfirm.vue';
import { convertJsonSchemaToJson } from '@/api/modules/api-test/management';
import { jsonSchemaAutoGenerate } from '@/api/modules/api-test/management';
import { responseHeaderOption } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
@ -431,7 +431,7 @@
}
if (schema) {
// json schema json
const res = await convertJsonSchemaToJson(schema);
const res = await jsonSchemaAutoGenerate(schema);
activeResponse.value.body.jsonBody.jsonValue = res;
} else {
Message.warning(t('apiTestManagement.pleaseInputJsonSchema'));

View File

@ -85,7 +85,7 @@
<commons-jexl3.version>3.3</commons-jexl3.version>
<revision>3.x</revision>
<monitoring-engine.revision>3.0</monitoring-engine.revision>
<xeger.version>1.0.0-RELEASE</xeger.version>
</properties>
<modules>