This commit is contained in:
chenjianxing 2020-12-17 14:34:17 +08:00
commit f377bd251a
26 changed files with 105 additions and 47 deletions

View File

@ -6,11 +6,11 @@ ARG MS_VERSION=dev
RUN mkdir -p /opt/apps && mkdir -p /opt/jmeter/lib/junit
COPY backend/target/backend-1.5.jar /opt/apps
COPY backend/target/backend-1.6.jar /opt/apps
COPY backend/target/classes/jmeter/ /opt/jmeter/
ENV JAVA_APP_JAR=/opt/apps/backend-1.5.jar
ENV JAVA_APP_JAR=/opt/apps/backend-1.6.jar
ENV AB_OFF=true

View File

@ -13,10 +13,10 @@
MeterSphere 是一站式开源持续测试平台涵盖测试跟踪、接口测试、性能测试、团队协作等功能兼容JMeter 等开源标准,有效助力开发和测试团队充分利用云弹性进行高度可扩展的自动化测试,加速高质量软件的交付。
- 测试跟踪: 远超 TestLink 的使用体验;
- 接口测试: 类似 Postman 的体验
- 测试跟踪: 远超 TestLink 的使用体验,覆盖从编写用例到生成测试报告的完整流程
- 接口测试: 集 Postman 的易用与 JMeter 的灵活于一体,接口管理、多协议支持、场景自动化,你想要的全都有
- 性能测试: 兼容 JMeter支持 Kubernetes 和云环境,轻松支持高并发、分布式的性能测试;
- 团队协作: 两级租户体系,天然支持团队协作
- 团队协作: 用户管理、租户管理、权限管理、资源管理,无论团队规模如何,总有适合的落地方式
![产品定位](https://metersphere.oss-cn-hangzhou.aliyuncs.com/img/ct-devops.png)
@ -72,12 +72,11 @@ v1.1.0 是 v1.0.0 之后的功能版本。
像其它优秀开源项目一样MeterSphere 将每月发布一个功能版本。
## 技术优势
## 产品优势
- 全生命周期: 能够覆盖从测试计划到测试执行、测试报告分析的不同阶段;
- 自动化 & 扩展性: 支持接口和性能的自动化测试,可以充分利用云弹性实现超大规模的性能测试;
- 持续测试: 能够与持续集成工具无缝集成,支撑企业实现测试左移;
- 团队协作: 支持不同规模的测试团队,小到几个人的测试团队、大到数百人的测试中心。
- 开源基于开源、兼容开源按月发布新版本、日均下载安装超过100次、被大量客户验证
- 一站式:一个产品全面涵盖测试跟踪、接口测试、性能测试等功能并形成联动:其中用例管理是底座需求、接口自动化测试是高频需求、性能测试是专家服务为主工具为辅;一个产品全满足从测试计划、测试执行到测试报告分析的全生命周期需求;
- 持续测试:能将测试融入持续交付和 DevOps 体系;无缝对接 Bug 管理工具和持续集成工具等;支持团队协作和资产沉淀。
## 功能列表

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>metersphere-server</artifactId>
<groupId>io.metersphere</groupId>
<version>1.5</version>
<version>1.6</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -51,6 +51,7 @@ public class APITestController {
public Pager<List<APITestResult>> list(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody QueryAPITestRequest request) {
Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
request.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
request.setProjectId(SessionUtils.getCurrentProjectId());
return PageUtils.setPageInfo(page, apiTestService.list(request));
}

View File

@ -34,8 +34,8 @@ public class ApiAutomationController {
}
@PostMapping(value = "/create")
public void create(@RequestPart("request") SaveApiScenarioRequest request, @RequestPart(value = "files") List<MultipartFile> bodyFiles) {
apiAutomationService.create(request, bodyFiles);
public ApiScenario create(@RequestPart("request") SaveApiScenarioRequest request, @RequestPart(value = "files") List<MultipartFile> bodyFiles) {
return apiAutomationService.create(request, bodyFiles);
}
@PostMapping(value = "/update")

View File

@ -67,6 +67,7 @@ public class MsAssertions extends MsTestElement {
assertion.setProperty(TestElement.TEST_CLASS, ResponseAssertion.class.getName());
assertion.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("AssertionGui"));
assertion.setAssumeSuccess(assertionRegex.isAssumeSuccess());
assertion.addTestString(assertionRegex.getExpression());
assertion.setToContainsType();
switch (assertionRegex.getSubject()) {
case "Response Code":

View File

@ -68,7 +68,6 @@ public class APITestService {
public List<APITestResult> list(QueryAPITestRequest request) {
request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders()));
request.setProjectId(SessionUtils.getCurrentProjectId());
return extApiTestMapper.list(request);
}

View File

@ -100,11 +100,12 @@ public class ApiAutomationService {
apiScenarioMapper.deleteByExample(example);
}
public void create(SaveApiScenarioRequest request, List<MultipartFile> bodyFiles) {
public ApiScenario create(SaveApiScenarioRequest request, List<MultipartFile> bodyFiles) {
request.setId(UUID.randomUUID().toString());
checkNameExist(request);
final ApiScenario scenario = new ApiScenario();
scenario.setId(UUID.randomUUID().toString());
scenario.setId(request.getId());
scenario.setName(request.getName());
scenario.setProjectId(request.getProjectId());
scenario.setTagId(request.getTagId());
@ -132,6 +133,7 @@ public class ApiAutomationService {
List<String> bodyUploadIds = request.getBodyUploadIds();
apiDefinitionService.createBodyFiles(bodyUploadIds, bodyFiles);
return scenario;
}
public void update(SaveApiScenarioRequest request, List<MultipartFile> bodyFiles) {
@ -248,7 +250,7 @@ public class ApiAutomationService {
JSONObject element = JSON.parseObject(item.getScenarioDefinition());
MsScenario scenario = JSONObject.parseObject(item.getScenarioDefinition(), MsScenario.class);
// 多态JSON普通转换会丢失内容需要通过 ObjectMapper 获取
if (element!= null && StringUtils.isNotEmpty(element.getString("hashTree"))) {
if (element != null && StringUtils.isNotEmpty(element.getString("hashTree"))) {
LinkedList<MsTestElement> elements = mapper.readValue(element.getString("hashTree"),
new TypeReference<LinkedList<MsTestElement>>() {
});

View File

@ -166,7 +166,9 @@ public class ApiModuleService extends NodeTreeService<ApiModuleDTO> {
ApiModuleDTO nodeTree = request.getNodeTree();
List<ApiModule> updateNodes = new ArrayList<>();
if (nodeTree == null) {
return;
}
buildUpdateDefinition(nodeTree, apiModule, updateNodes, "/", "0", nodeTree.getLevel());
updateNodes = updateNodes.stream()
@ -179,8 +181,7 @@ public class ApiModuleService extends NodeTreeService<ApiModuleDTO> {
}
private void buildUpdateDefinition(ApiModuleDTO rootNode, List<ApiDefinitionResult> apiDefinitions,
List<ApiModule> updateNodes, String rootPath, String pId, int level) {
List<ApiModule> updateNodes, String rootPath, String pId, int level) {
rootPath = rootPath + rootNode.getName();
if (level > 8) {

View File

@ -13,7 +13,6 @@ import io.metersphere.base.mapper.ApiScenarioModuleMapper;
import io.metersphere.base.mapper.ext.ExtApiScenarioModuleMapper;
import io.metersphere.commons.constants.TestCaseConstants;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.i18n.Translator;
import io.metersphere.service.NodeTreeService;
import org.apache.commons.lang3.StringUtils;
@ -24,7 +23,10 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@ -140,7 +142,9 @@ public class ApiScenarioModuleService extends NodeTreeService<ApiScenarioModuleD
ApiScenarioModuleDTO nodeTree = request.getNodeTree();
List<ApiScenarioModule> updateNodes = new ArrayList<>();
if (nodeTree == null) {
return;
}
buildUpdateDefinition(nodeTree, apiScenarios, updateNodes, "/", "0", nodeTree.getLevel());
updateNodes = updateNodes.stream()

View File

@ -7,6 +7,6 @@
<select id="selectMaxResultByResourceId" parameterType="java.lang.String" resultType="io.metersphere.base.domain.ApiDefinitionExecResult">
select * from api_definition_exec_result
where resource_id = #{resourceId,jdbcType=VARCHAR} ORDER BY update_time DESC LIMIT 1
where resource_id = #{resourceId,jdbcType=VARCHAR} ORDER BY start_time DESC LIMIT 1
</select>
</mapper>

@ -1 +1 @@
Subproject commit bb494fc68a2367359c9048fa7250c7618de4afb6
Subproject commit 61397c16728a63493507679f7e0940d9099f337f

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>metersphere-server</artifactId>
<groupId>io.metersphere</groupId>
<version>1.5</version>
<version>1.6</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -240,7 +240,7 @@
});
},
copy(row) {
row.id = getUUID();
row.copy = true;
this.$emit('edit', row);
},
showReport(row) {

View File

@ -493,6 +493,9 @@
request.enable === undefined ? request.enable = true : request.enable;
request.active = false;
request.resourceId = getUUID();
if (!request.url) {
request.url = "";
}
if (referenced === 'REF' || !request.hashTree) {
request.hashTree = [];
}
@ -557,8 +560,8 @@
copyRow(row, node) {
const parent = node.parent
const hashTree = parent.data.hashTree || parent.data;
let obj = {};
Object.assign(obj, row);
//
let obj = JSON.parse(JSON.stringify(row));
obj.resourceId = getUUID();
hashTree.push(obj);
this.sort();
@ -604,7 +607,7 @@
this.getEnvironments();
},
allowDrop(draggingNode, dropNode, type) {
if (ELEMENTS.get(dropNode.data.type).indexOf(draggingNode.data.type) != -1) {
if (dropNode.data.type === draggingNode.data.type || ELEMENTS.get(dropNode.data.type).indexOf(draggingNode.data.type) != -1) {
return true;
}
return false;
@ -694,9 +697,12 @@
if (valid) {
this.setParameter();
let bodyFiles = this.getBodyUploadFiles(this.currentScenario);
this.$fileUpload(this.path, null, bodyFiles, this.currentScenario, () => {
this.$fileUpload(this.path, null, bodyFiles, this.currentScenario, response => {
this.$success(this.$t('commons.save_success'));
this.path = "/api/automation/update";
if (response.data) {
this.currentScenario.id = response.data.id;
}
this.currentScenario.tagId = JSON.parse(this.currentScenario.tagId);
this.$emit('refresh');
})
@ -719,6 +725,9 @@
this.scenarioDefinition = obj.hashTree;
}
}
if (this.currentScenario.copy) {
this.path = "/api/automation/create";
}
}
})
}

View File

@ -67,7 +67,7 @@
if (Object.prototype.toString.call(this.currentApi.response).match(/\[object (\w+)\]/)[1].toLowerCase() === 'object') {
this.response = this.currentApi.response;
} else {
this.response = new ResponseFactory(JSON.parse(this.currentApi.response));
this.response = JSON.parse(this.currentApi.response);
}
} else {
this.response = {headers: [], body: new Body(), statusCode: [], type: "HTTP"};

View File

@ -26,7 +26,7 @@
<el-form-item :label="$t('commons.password')" prop="password" v-if=" authConfig.verification!=undefined && authConfig.verification !='No Auth'">
<el-input v-model="authConfig.password" :placeholder="$t('commons.password')" show-password autocomplete="off"
maxlength="20" show-word-limit/>
maxlength="50" show-word-limit/>
</el-form-item>
</el-form>

View File

@ -68,7 +68,7 @@
method: [{required: true, message: this.$t('test_track.case.input_maintainer'), trigger: 'change'}],
url: [{required: true, message: this.$t('api_test.definition.request.path_all_info'), trigger: 'blur'}],
},
debugForm: {method: REQ_METHOD[0].id},
debugForm: {method: REQ_METHOD[0].id, environmentId: ""},
options: [],
responseData: {type: 'HTTP', responseResult: {}, subRequestResults: []},
loading: false,

View File

@ -140,7 +140,7 @@
{name: this.$t('api_test.definition.request.batch_edit'), handleClick: this.handleEditBatch}
],
typeArr: [
{id: 'status', name: this.$t('api_test.definition.api_case_status')},
{id: 'status', name: this.$t('api_test.definition.api_status')},
{id: 'method', name: this.$t('api_test.definition.api_type')},
{id: 'userId', name: this.$t('api_test.definition.api_principal')},
],

View File

@ -167,8 +167,7 @@
this.reload();
},
copyRow(row) {
let obj = {};
Object.assign(obj, row);
let obj =JSON.parse(JSON.stringify(row));
obj.id = getUUID();
this.request.hashTree.push(obj);
this.reload();

View File

@ -141,8 +141,7 @@
this.reload();
},
copyRow(row) {
let obj = {};
Object.assign(obj, row);
let obj =JSON.parse(JSON.stringify(row));
obj.id = getUUID();
this.request.hashTree.push(obj);
this.reload();

View File

@ -0,0 +1,42 @@
<template>
<div>
<el-row style="margin: 20px">
<span style="margin-right: 10px">
{{$t('api_test.request.connect_timeout')}}:
</span>
<span style="margin-right: 10px">
<el-input-number size="small" :disabled="isReadOnly" v-model="request.connectTimeout" :placeholder="$t('commons.millisecond')" :max="1000*10000000" :min="0"/>
</span>
<span style="margin-right: 10px">
{{$t('api_test.request.response_timeout')}}:
</span>
<span style="margin-right: 10px">
<el-input-number size="small" :disabled="isReadOnly" v-model="request.responseTimeout" :placeholder="$t('commons.millisecond')" :max="1000*10000000" :min="0"/>
</span>
</el-row>
<el-row style="margin: 20px">
<span style="margin-right: 10px">
<el-checkbox class="follow-redirects-item" v-model="request.followRedirects">{{$t('api_test.request.follow_redirects')}}</el-checkbox>
</span>
<span style="margin-right: 10px">
<el-checkbox class="do-multipart-post" v-model="request.doMultipartPost">{{$t('api_test.request.do_multipart_post')}}</el-checkbox>
</span>
</el-row>
</div>
</template>
<script>
export default {
name: "MsApiAdvancedConfig",
props: {
request: Object,
isReadOnly: {
type: Boolean,
default: false
}
}
}
</script>
<style scoped>
</style>

View File

@ -60,6 +60,10 @@
<ms-api-auth-config :is-read-only="isReadOnly" :request="request"/>
</el-tab-pane>
<el-tab-pane label="其他设置" name="advancedConfig">
<ms-api-advanced-config :is-read-only="isReadOnly" :request="request"/>
</el-tab-pane>
</el-tabs>
</div>
<div v-if="!referenced">
@ -101,13 +105,13 @@
import {REQUEST_HEADERS} from "@/common/js/constants";
import MsApiVariable from "../../ApiVariable";
import MsJsr233Processor from "../../processor/Jsr233Processor";
import MsApiAdvancedConfig from "../../ApiAdvancedConfig";
import {createComponent} from "../../jmeter/components";
import MsApiAssertions from "../../assertion/ApiAssertions";
import MsApiExtract from "../../extract/ApiExtract";
import {Assertions, Body, Extract, KeyValue} from "../../../model/ApiTestModel";
import {getUUID} from "@/common/js/utils";
import BatchAddParameter from "../../basis/BatchAddParameter";
import MsApiAdvancedConfig from "./ApiAdvancedConfig";
export default {
@ -200,8 +204,7 @@
this.reload();
},
copyRow(row) {
let obj = {};
Object.assign(obj, row);
let obj =JSON.parse(JSON.stringify(row));
obj.id = getUUID();
this.request.hashTree.push(obj);
this.reload();

View File

@ -195,8 +195,7 @@
this.reload();
},
copyRow(row) {
let obj = {};
Object.assign(obj, row);
let obj =JSON.parse(JSON.stringify(row));
obj.id = getUUID();
this.request.hashTree.push(obj);
this.reload();

@ -1 +1 @@
Subproject commit a22a3005d9bd254793fcf634d72539cbdf31be3a
Subproject commit d39dafaf84b9c7a56cb51f2caf67dd7dfde5938c

View File

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.metersphere</groupId>
<artifactId>metersphere-server</artifactId>
<version>1.5</version>
<version>1.6</version>
<packaging>pom</packaging>
<parent>