feat(接口测试): 单接口调试

This commit is contained in:
chenjianxing 2020-08-10 21:59:11 +08:00
parent 18eb1827ef
commit 4e0b340c20
14 changed files with 201 additions and 115 deletions

View File

@ -60,13 +60,13 @@ public class APITestController {
}
@PostMapping(value = "/create", consumes = {"multipart/form-data"})
public void create(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "files") List<MultipartFile> files) {
apiTestService.create(request, files);
public void create(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "file") MultipartFile file) {
apiTestService.create(request, file);
}
@PostMapping(value = "/update", consumes = {"multipart/form-data"})
public void update(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "files") List<MultipartFile> files) {
apiTestService.update(request, files);
public void update(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "file") MultipartFile file) {
apiTestService.update(request, file);
}
@PostMapping(value = "/copy")
@ -89,9 +89,9 @@ public class APITestController {
return apiTestService.run(request);
}
@PostMapping(value = "/run/independent", consumes = {"multipart/form-data"})
public String runIndependent(@RequestBody SaveAPITestRequest request, @RequestPart(value = "files") List<MultipartFile> files) {
return apiTestService.runIndependent(request, files);
@PostMapping(value = "/run/debug", consumes = {"multipart/form-data"})
public String runDebug(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "file") MultipartFile file) {
return apiTestService.runDebug(request, file);
}
@PostMapping(value = "/import", consumes = {"multipart/form-data"})

View File

@ -2,7 +2,9 @@ package io.metersphere.api.jmeter;
import io.metersphere.api.service.APIReportService;
import io.metersphere.api.service.APITestService;
import io.metersphere.base.domain.ApiTestReport;
import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import org.apache.commons.lang3.StringUtils;
@ -31,12 +33,16 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
private APIReportService apiReportService;
public String runMode = ApiRunMode.RUN.name();
// 测试ID
private String testId;
private String debugReportId;
@Override
public void setupTest(BackendListenerContext context) throws Exception {
this.testId = context.getParameter(TEST_ID);
setParam(context);
apiTestService = CommonBeanFactory.getBean(APITestService.class);
if (apiTestService == null) {
LogUtil.error("apiTestService is required");
@ -99,8 +105,14 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
testResult.getScenarios().addAll(scenarios.values());
testResult.getScenarios().sort(Comparator.comparing(ScenarioResult::getId));
apiTestService.changeStatus(testId, APITestStatus.Completed);
apiReportService.complete(testResult);
ApiTestReport report = null;
if (StringUtils.equals(this.runMode, ApiRunMode.DEBUG.name())) {
report = apiReportService.get(debugReportId);
} else {
apiTestService.changeStatus(testId, APITestStatus.Completed);
report = apiReportService.getRunningReport(testResult.getTestId());
}
apiReportService.complete(testResult, report);
queue.clear();
super.teardownTest(context);
@ -153,6 +165,15 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
}
}
private void setParam(BackendListenerContext context) {
this.testId = context.getParameter(TEST_ID);
this.runMode = context.getParameter("runMode");
this.debugReportId = context.getParameter("debugReportId");
if (StringUtils.isBlank(this.runMode)) {
this.runMode = ApiRunMode.RUN.name();
}
}
private ResponseAssertionResult getResponseAssertionResult(AssertionResult assertionResult) {
ResponseAssertionResult responseAssertionResult = new ResponseAssertionResult();
responseAssertionResult.setMessage(assertionResult.getFailureMessage());

View File

@ -1,9 +1,11 @@
package io.metersphere.api.jmeter;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.config.JmeterProperties;
import io.metersphere.i18n.Translator;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.util.JMeterUtils;
@ -21,7 +23,7 @@ public class JMeterService {
@Resource
private JmeterProperties jmeterProperties;
public void run(String testId, InputStream is) {
public void run(String testId, String debugReportId, InputStream is) {
String JMETER_HOME = jmeterProperties.getHome();
String JMETER_PROPERTIES = JMETER_HOME + "/bin/jmeter.properties";
JMeterUtils.loadJMeterProperties(JMETER_PROPERTIES);
@ -29,7 +31,7 @@ public class JMeterService {
try {
Object scriptWrapper = SaveService.loadElement(is);
HashTree testPlan = getHashTree(scriptWrapper);
addBackendListener(testId, testPlan);
addBackendListener(testId, debugReportId, testPlan);
LocalRunner runner = new LocalRunner(testPlan);
runner.run();
@ -45,11 +47,15 @@ public class JMeterService {
return (HashTree) field.get(scriptWrapper);
}
private void addBackendListener(String testId, HashTree testPlan) {
private void addBackendListener(String testId, String debugReportId, HashTree testPlan) {
BackendListener backendListener = new BackendListener();
backendListener.setName(testId);
Arguments arguments = new Arguments();
arguments.addArgument(APIBackendListenerClient.TEST_ID, testId);
if (StringUtils.isNotBlank(debugReportId)) {
arguments.addArgument("runMode", ApiRunMode.DEBUG.name());
arguments.addArgument("debugReportId", debugReportId);
}
backendListener.setArguments(arguments);
backendListener.setClassname(APIBackendListenerClient.class.getCanonicalName());
testPlan.add(testPlan.getArray()[0], backendListener);

View File

@ -10,10 +10,12 @@ import io.metersphere.base.mapper.ApiTestReportDetailMapper;
import io.metersphere.base.mapper.ApiTestReportMapper;
import io.metersphere.base.mapper.ext.ExtApiTestReportMapper;
import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.constants.ReportTriggerMode;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.ServiceUtils;
import io.metersphere.dto.DashboardTestDTO;
import io.metersphere.i18n.Translator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -73,8 +75,7 @@ public class APIReportService {
apiTestReportDetailMapper.deleteByExample(detailExample);
}
public void complete(TestResult result) {
ApiTestReport report = getRunningReport(result.getTestId());
public void complete(TestResult result, ApiTestReport report) {
if (report == null) {
MSException.throwException(Translator.get("api_report_is_null"));
}
@ -87,10 +88,12 @@ public class APIReportService {
// report
report.setUpdateTime(System.currentTimeMillis());
if (result.getError() > 0) {
report.setStatus(APITestStatus.Error.name());
} else {
report.setStatus(APITestStatus.Success.name());
if (!StringUtils.equals(report.getStatus(), APITestStatus.Debug.name())) {
if (result.getError() > 0) {
report.setStatus(APITestStatus.Error.name());
} else {
report.setStatus(APITestStatus.Success.name());
}
}
apiTestReportMapper.updateByPrimaryKeySelective(report);
@ -106,6 +109,12 @@ public class APIReportService {
return report.getId();
}
public String createDebugReport(ApiTest test) {
ApiTestReport report = buildReport(test, ReportTriggerMode.MANUAL.name(), APITestStatus.Debug.name());
apiTestReportMapper.insert(report);
return report.getId();
}
public ApiTestReport buildReport(ApiTest test, String triggerMode, String status) {
ApiTestReport report = new ApiTestReport();
report.setId(UUID.randomUUID().toString());

View File

@ -12,10 +12,7 @@ import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.ApiTestFileMapper;
import io.metersphere.base.mapper.ApiTestMapper;
import io.metersphere.base.mapper.ext.ExtApiTestMapper;
import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.constants.FileType;
import io.metersphere.commons.constants.ScheduleGroup;
import io.metersphere.commons.constants.ScheduleType;
import io.metersphere.commons.constants.*;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.commons.utils.LogUtil;
@ -73,21 +70,21 @@ public class APITestService {
return extApiTestMapper.list(request);
}
public void create(SaveAPITestRequest request, List<MultipartFile> files) {
if (files == null || files.isEmpty()) {
public void create(SaveAPITestRequest request, MultipartFile file) {
if (file == null) {
throw new IllegalArgumentException(Translator.get("file_cannot_be_null"));
}
ApiTest test = createTest(request);
saveFile(test.getId(), files);
saveFile(test.getId(), file);
}
public void update(SaveAPITestRequest request, List<MultipartFile> files) {
if (files == null || files.isEmpty()) {
public void update(SaveAPITestRequest request, MultipartFile file) {
if (file == null) {
throw new IllegalArgumentException(Translator.get("file_cannot_be_null"));
}
deleteFileByTestId(request.getId());
ApiTest test = updateTest(request);
saveFile(test.getId(), files);
saveFile(test.getId(), file);
}
public void copy(SaveAPITestRequest request) {
@ -157,7 +154,7 @@ public class APITestService {
String reportId = apiReportService.create(apiTest, request.getTriggerMode());
changeStatus(request.getId(), APITestStatus.Running);
jMeterService.run(request.getId(), is);
jMeterService.run(request.getId(), null, is);
return reportId;
}
@ -204,14 +201,12 @@ public class APITestService {
return test;
}
private void saveFile(String testId, List<MultipartFile> files) {
files.forEach(file -> {
final FileMetadata fileMetadata = fileService.saveFile(file);
ApiTestFile apiTestFile = new ApiTestFile();
apiTestFile.setTestId(testId);
apiTestFile.setFileId(fileMetadata.getId());
apiTestFileMapper.insert(apiTestFile);
});
private void saveFile(String testId, MultipartFile file) {
final FileMetadata fileMetadata = fileService.saveFile(file);
ApiTestFile apiTestFile = new ApiTestFile();
apiTestFile.setTestId(testId);
apiTestFile.setFileId(fileMetadata.getId());
apiTestFileMapper.insert(apiTestFile);
}
private void deleteFileByTestId(String testId) {
@ -330,13 +325,17 @@ public class APITestService {
return schedules;
}
public String runIndependent(SaveAPITestRequest request, List<MultipartFile> files) {
if (files == null || files.isEmpty()) {
public String runDebug(SaveAPITestRequest request, MultipartFile file) {
if (file == null) {
throw new IllegalArgumentException(Translator.get("file_cannot_be_null"));
}
// ApiTest test = createTest(request);
// saveFile(test.getId(), files);
MultipartFile file = files.get(0);
updateTest(request);
APITestResult apiTest = get(request.getId());
if (SessionUtils.getUser() == null) {
apiTest.setUserId(request.getUserId());
}
String reportId = apiReportService.createDebugReport(apiTest);
InputStream is = null;
try {
is = new ByteArrayInputStream(file.getBytes());
@ -344,14 +343,7 @@ public class APITestService {
LogUtil.error(e);
}
APITestResult apiTest = get(request.getId());
if (SessionUtils.getUser() == null) {
apiTest.setUserId(request.getUserId());
}
String reportId = apiReportService.create(apiTest, request.getTriggerMode());
changeStatus(request.getId(), APITestStatus.Running);
jMeterService.run(request.getId(), is);
jMeterService.run(request.getId(), reportId, is);
return reportId;
}
}

View File

@ -1,5 +1,5 @@
package io.metersphere.commons.constants;
public enum APITestStatus {
Saved, Starting, Running, Reporting, Completed, Error, Success
Saved, Starting, Running, Reporting, Completed, Debug, Error, Success
}

View File

@ -0,0 +1,5 @@
package io.metersphere.commons.constants;
public enum ApiRunMode {
RUN, DEBUG
}

View File

@ -32,7 +32,7 @@
components: {MsScenarioResult},
props: {
scenarios: Array
scenarios: Array,
}
}

View File

@ -55,7 +55,7 @@
<ms-schedule-config :schedule="test.schedule" :is-read-only="isReadOnly" :save="saveCronExpression" @scheduleChange="saveSchedule" :check-open="checkScheduleEdit"/>
</el-row>
</el-header>
<ms-api-scenario-config :is-read-only="isReadOnly" :scenarios="test.scenarioDefinition" :project-id="test.projectId" ref="config"/>
<ms-api-scenario-config :debug-report-id="debugReportId" @runDebug="runDebug" :is-read-only="isReadOnly" :scenarios="test.scenarioDefinition" :project-id="test.projectId" ref="config"/>
</el-container>
</el-card>
</div>
@ -86,7 +86,8 @@
projects: [],
change: false,
test: new Test(),
isReadOnly: false
isReadOnly: false,
debugReportId: ''
}
},
@ -149,9 +150,12 @@
}
this.change = false;
let url = this.create ? "/api/create" : "/api/update";
this.result = this.$request(this.getOptions(url), () => {
this.create = false;
let jmx = this.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], jmx.name);
this.result = this.$fileUpload(url, file, this.test,response => {
if (callback) callback();
this.create = false;
});
},
saveTest() {
@ -183,26 +187,6 @@
cancel() {
this.$router.push('/api/test/list/all');
},
getOptions(url) {
let formData = new FormData();
let requestJson = JSON.stringify(this.test);
formData.append('request', new Blob([requestJson], {
type: "application/json"
}));
let jmx = this.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
formData.append("files", new File([blob], jmx.name));
return {
method: 'POST',
url: url,
data: formData,
headers: {
'Content-Type': undefined
}
};
},
handleCommand(command) {
switch (command) {
case "report":
@ -251,6 +235,30 @@
return false;
}
return true;
},
runDebug(scenario) {
if (this.create) {
this.$warning(this.$t('api_test.environment.please_save_test'));
return;
}
let url = "/api/run/debug";
let runningTest = new Test();
Object.assign(runningTest, this.test);
runningTest.scenarioDefinition = [];
runningTest.scenarioDefinition.push(scenario);
let validator = runningTest.isValid();
if (!validator.isValid) {
this.$warning(this.$t(validator.info));
return;
}
let jmx = runningTest.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], jmx.name);
this.$fileUpload(url, file, this.test,response => {
this.debugReportId = response.data;
});
}
},

View File

@ -36,7 +36,7 @@
<el-main class="scenario-main">
<div class="scenario-form">
<ms-api-scenario-form :is-read-only="isReadOnly" :scenario="selected" :project-id="projectId" v-if="isScenario"/>
<ms-api-request-form :is-read-only="isReadOnly" :request="selected" v-if="isRequest"/>
<ms-api-request-form :debug-report-id="debugReportId" @runDebug="runDebug" :is-read-only="isReadOnly" :request="selected" v-if="isRequest"/>
</div>
</el-main>
</el-container>
@ -70,13 +70,15 @@
isReadOnly: {
type: Boolean,
default: false
}
},
debugReportId: String
},
data() {
return {
activeName: 0,
selected: [Scenario, Request]
selected: [Scenario, Request],
currentScenario: {}
}
},
@ -118,9 +120,14 @@
break;
}
},
select: function (obj) {
select: function (obj, scenario) {
this.selected = null;
this.$nextTick(function () {
if (obj instanceof Scenario) {
this.currentScenario = obj;
} else {
this.currentScenario = scenario;
}
this.selected = obj;
});
},
@ -145,6 +152,13 @@
});
});
}
},
runDebug(request) {
let scenario = new Scenario();
Object.assign(scenario, this.currentScenario);
scenario.requests = [];
scenario.requests.push(request);
this.$emit('runDebug', scenario);
}
},

View File

@ -39,6 +39,8 @@
</el-switch>
</el-form-item>
<el-button class="debug-button" size="small" type="primary" @click="runDebug">{{$t('load_test.save_and_run')}}</el-button>
<el-tabs v-model="activeName">
<el-tab-pane :label="$t('api_test.request.parameters')" name="parameters">
<ms-api-key-value :is-read-only="isReadOnly" :items="request.parameters"
@ -154,6 +156,9 @@
}
}
return url;
},
runDebug() {
this.$emit('runDebug');
}
},
@ -192,4 +197,12 @@
color: #F56C6C;
}
.debug-button {
margin-left: auto;
display: block;
/*margin-bottom: -30px;*/
margin-right: 10px;
z-index: 1999;
}
</style>

View File

@ -112,7 +112,7 @@
}
request.dubboConfig = this.scenario.dubboConfig;
this.selected = request;
this.$emit("select", request);
this.$emit("select", request, this.scenario);
}
},

View File

@ -1,7 +1,7 @@
<template>
<div>
<component :is="component" :is-read-only="isReadOnly" :request="request"/>
<!-- <ms-scenario-results :scenarios="content.scenarios"/>-->
<component @runDebug="runDebug" :is="component" :is-read-only="isReadOnly" :request="request"/>
<ms-scenario-results v-loading="debugReportLoading" v-if="isCompleted" :scenarios="isCompleted ? request.debugReport.scenarios : []"/>
</div>
</template>
@ -19,12 +19,15 @@
isReadOnly: {
type: Boolean,
default: false
}
},
debugReportId: String
},
data() {
return {
reportId: "",
content:{}
content: {scenarios:[]},
debugReportLoading: false,
showDebugReport: false
}
},
computed: {
@ -38,38 +41,51 @@
name = "MsApiHttpRequestForm";
}
return name;
},
isCompleted() {
return !!this.request.debugReport;
}
},
created() {
this.getReport();
watch: {
debugReportId() {
this.getReport();
}
},
methods: {
getReport() {
// // this.reportId = "00143d36-a58a-477e-a05a-556c1d48046c";
// if (this.reportId) {
// let url = "/api/report/get/" + this.reportId;
// this.$get(url, response => {
// let report = response.data || {};
// if (response.data) {
// // if (this.isNotRunning) {
// try {
// this.content = JSON.parse(report.content);
// } catch (e) {
// console.log(report.content)
// throw e;
// }
// // this.getFails();
// // this.loading = false;
// // }
// // else {
// // setTimeout(this.getReport, 2000)
// // }
// } else {
// this.loading = false;
// this.$error(this.$t('api_report.not_exist'));
// }
// });
// }
if (this.debugReportId) {
this.debugReportLoading = true;
this.showDebugReport = true;
this.request.debugReport = {};
let url = "/api/report/get/" + this.debugReportId;
this.$get(url, response => {
let report = response.data || {};
let res = {};
if (response.data) {
try {
res = JSON.parse(report.content);
} catch (e) {
console.log(report.content)
throw e;
}
if (res) {
this.debugReportLoading = false;
this.request.debugReport = res;
this.deleteReport(this.debugReportId)
} else {
setTimeout(this.getReport, 2000)
}
} else {
this.debugReportLoading = false;
}
});
}
},
deleteReport(reportId) {
this.$post('/api/report/delete', {id: reportId});
},
runDebug() {
this.$emit('runDebug', this.request);
}
}
}
@ -78,7 +94,7 @@
<style scoped>
.scenario-results {
margin-top: 15px;
margin-top: 20px;
}
</style>

View File

@ -297,6 +297,7 @@ export class HttpRequest extends Request {
this.extract = undefined;
this.environment = undefined;
this.useEnvironment = undefined;
this.debugReport = undefined;
this.set(options);
this.sets({parameters: KeyValue, headers: KeyValue}, options);
@ -370,6 +371,7 @@ export class DubboRequest extends Request {
this.extract = new Extract(options.extract);
// Scenario.dubboConfig
this.dubboConfig = undefined;
this.debugReport = undefined;
this.sets({args: KeyValue, attachmentArgs: KeyValue}, options);
}
@ -683,7 +685,7 @@ const JMX_ASSERTION_CONDITION = {
class JMXHttpRequest {
constructor(request, environment) {
if (request && request instanceof HttpRequest && (request.url || request.path)) {
if (request && request instanceof HttpRequest) {
this.useEnvironment = request.useEnvironment;
this.method = request.method;
if (!request.useEnvironment) {