feat(接口测试): sql 请求

This commit is contained in:
chenjianxing 2020-09-15 11:26:29 +08:00
parent 37bf65ce07
commit cbb1976f77
12 changed files with 324 additions and 34 deletions

View File

@ -165,6 +165,12 @@
<version>${jmeter.version}</version>
</dependency>
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_jdbc</artifactId>
<version>${jmeter.version}</version>
</dependency>
<!-- Zookeeper -->
<dependency>
<groupId>org.apache.dubbo</groupId>

View File

@ -0,0 +1,16 @@
package io.metersphere.api.dto.scenario;
import lombok.Data;
@Data
public class DatabaseConfig {
private String id;
private String name;
private long poolMax;
private long timeout;
private String driver;
private String dbUrl;
private String username;
private String password;
}

View File

@ -16,5 +16,6 @@ public class Scenario {
private List<KeyValue> headers;
private List<Request> requests;
private DubboConfig dubboConfig;
private List<DatabaseConfig> databaseConfigs;
private Boolean enable;
}

View File

@ -7,8 +7,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = HttpRequest.class, name = RequestType.HTTP),
@JsonSubTypes.Type(value = DubboRequest.class, name = RequestType.DUBBO)
@JsonSubTypes.Type(value = DubboRequest.class, name = RequestType.DUBBO),
@JsonSubTypes.Type(value = SqlRequest.class, name = RequestType.SQL)
})
@JSONType(seeAlso = {HttpRequest.class, DubboRequest.class}, typeKey = "type")
@JSONType(seeAlso = {HttpRequest.class, DubboRequest.class, SqlRequest.class}, typeKey = "type")
public interface Request {
}

View File

@ -5,4 +5,6 @@ public class RequestType {
public static final String HTTP = "HTTP";
public static final String DUBBO = "DUBBO";
public static final String SQL = "SQL";
}

View File

@ -0,0 +1,40 @@
package io.metersphere.api.dto.scenario.request;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.scenario.assertions.Assertions;
import io.metersphere.api.dto.scenario.extract.Extract;
import io.metersphere.api.dto.scenario.processor.JSR223PostProcessor;
import io.metersphere.api.dto.scenario.processor.JSR223PreProcessor;
import lombok.Data;
@Data
@JSONType(typeName = RequestType.SQL)
public class SqlRequest implements Request {
// type 必须放最前面以便能够转换正确的类
private String type = RequestType.SQL;
@JSONField(ordinal = 1)
private String id;
@JSONField(ordinal = 2)
private String name;
@JSONField(ordinal = 3)
private String dataSource;
@JSONField(ordinal = 4)
private String query;
@JSONField(ordinal = 5)
private long queryTimeout;
@JSONField(ordinal = 6)
private Boolean useEnvironment;
@JSONField(ordinal = 7)
private Assertions assertions;
@JSONField(ordinal = 8)
private Extract extract;
@JSONField(ordinal = 9)
private Boolean enable;
@JSONField(ordinal = 10)
private Boolean followRedirects;
@JSONField(ordinal = 11)
private JSR223PreProcessor jsr223PreProcessor;
@JSONField(ordinal = 12)
private JSR223PostProcessor jsr223PostProcessor;
}

View File

@ -42,6 +42,7 @@
<el-radio-group v-model="type" @change="createRequest">
<el-radio :label="types.HTTP">HTTP</el-radio>
<el-radio :label="types.DUBBO">DUBBO</el-radio>
<el-radio :label="types.SQL">SQL</el-radio>
</el-radio-group>
<el-button slot="reference" :disabled="isReadOnly"
class="request-create" type="primary" size="mini" icon="el-icon-plus" plain/>

View File

@ -13,10 +13,11 @@ import MsApiHttpRequestForm from "./ApiHttpRequestForm";
import MsApiDubboRequestForm from "./ApiDubboRequestForm";
import MsScenarioResults from "../../../report/components/ScenarioResults";
import MsRequestResultTail from "../../../report/components/RequestResultTail";
import MsApiSqlRequestForm from "./ApiSqlRequestForm";
export default {
name: "MsApiRequestForm",
components: {MsRequestResultTail, MsScenarioResults, MsApiDubboRequestForm, MsApiHttpRequestForm},
components: {MsApiSqlRequestForm, MsRequestResultTail, MsScenarioResults, MsApiDubboRequestForm, MsApiHttpRequestForm},
props: {
scenario: Scenario,
request: Request,
@ -41,6 +42,9 @@ export default {
case RequestFactory.TYPES.DUBBO:
name = "MsApiDubboRequestForm";
break;
case RequestFactory.TYPES.SQL:
name = "MsApiSqlRequestForm";
break;
default:
name = "MsApiHttpRequestForm";
}

View File

@ -0,0 +1,118 @@
<template>
<el-form :model="request" :rules="rules" ref="request" label-width="100px" :disabled="isReadOnly">
<el-form-item :label="$t('api_test.request.name')" prop="name">
<el-input v-model="request.name" maxlength="300" show-word-limit/>
</el-form-item>
<el-form-item :label="'连接池'" prop="dataSource">
<el-select v-model="request.dataSource">
<el-option v-for="(item, index) in scenario.databaseConfigs" :key="index" :value="item.name" :label="item.name"/>
</el-select>
</el-form-item>
<!--<el-form-item :label="'查询类型'" prop="protocol">-->
<!--<el-select v-model="request.queryType">-->
<!--<el-option label="dubbo://" :value="protocols.DUBBO"/>-->
<!--</el-select>-->
<!--</el-form-item>-->
<el-form-item :label="'超时时间'" prop="queryTimeout">
<el-input-number :disabled="isReadOnly" size="mini" v-model="request.queryTimeout" :placeholder="$t('commons.millisecond')" :max="1000*10000000" :min="0"/>
</el-form-item>
<el-button :disabled="!request.enable || !scenario.enable || isReadOnly" class="debug-button" size="small" type="primary" @click="runDebug">{{$t('api_test.request.debug')}}</el-button>
<el-tabs v-model="activeName">
<el-tab-pane :label="'sql脚本'" name="sql">
<div class="sql-content" >
<ms-code-edit mode="sql" :read-only="isReadOnly" :modes="['sql']" :data.sync="request.query" theme="eclipse" ref="codeEdit"/>
</div>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.assertions.label')" name="assertions">
<ms-api-assertions :is-read-only="isReadOnly" :assertions="request.assertions"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.extract.label')" name="extract">
<ms-api-extract :is-read-only="isReadOnly" :extract="request.extract"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.processor.pre_exec_script')" name="beanShellPreProcessor">
<ms-jsr233-processor :is-read-only="isReadOnly" :jsr223-processor="request.jsr223PreProcessor"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.processor.post_exec_script')" name="beanShellPostProcessor">
<ms-jsr233-processor :is-read-only="isReadOnly" :jsr223-processor="request.jsr223PostProcessor"/>
</el-tab-pane>
</el-tabs>
</el-form>
</template>
<script>
import MsApiKeyValue from "../ApiKeyValue";
import MsApiAssertions from "../assertion/ApiAssertions";
import {DubboRequest, Scenario, SqlRequest} from "../../model/ScenarioModel";
import MsApiExtract from "../extract/ApiExtract";
import ApiRequestMethodSelect from "../collapse/ApiRequestMethodSelect";
import MsDubboInterface from "@/business/components/api/test/components/request/dubbo/Interface";
import MsDubboRegistryCenter from "@/business/components/api/test/components/request/dubbo/RegistryCenter";
import MsDubboConfigCenter from "@/business/components/api/test/components/request/dubbo/ConfigCenter";
import MsDubboConsumerService from "@/business/components/api/test/components/request/dubbo/ConsumerAndService";
import MsJsr233Processor from "../processor/Jsr233Processor";
import MsCodeEdit from "../../../../common/components/MsCodeEdit";
export default {
name: "MsApiSqlRequestForm",
components: {
MsCodeEdit,
MsJsr233Processor,
MsDubboConsumerService,
MsDubboConfigCenter,
MsDubboRegistryCenter,
MsDubboInterface, ApiRequestMethodSelect, MsApiExtract, MsApiAssertions, MsApiKeyValue
},
props: {
request: SqlRequest,
scenario: Scenario,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
activeName: "sql",
rules: {
name: [
{required: true, message: this.$t('commons.input_name'), trigger: 'blur'},
{max: 300, message: this.$t('commons.input_limit', [1, 300]), trigger: 'blur'},
],
dataSource: [
{required: true, message: this.$t('commons.input_name'), trigger: 'blur'},
],
}
}
},
methods: {
useEnvironmentChange(value) {
if (value && !this.request.environment) {
this.$error(this.$t('api_test.request.please_add_environment_to_scenario'), 2000);
this.request.useEnvironment = false;
}
this.$refs["request"].clearValidate();
},
runDebug() {
this.$emit('runDebug');
}
},
computed: {}
}
</script>
<style scoped>
.sql-content {
height: calc(100vh - 570px);
}
</style>

View File

@ -8,7 +8,7 @@
</el-form-item>
<el-form-item :label="'数据库连接URL'" prop="dbUrl">
<el-input v-model="config.dbUrl" maxlength="300" show-word-limit
<el-input v-model="config.dbUrl" maxlength="500" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
@ -24,7 +24,7 @@
</el-form-item>
<el-form-item :label="'密码'" prop="password">
<el-input v-model="config.password" maxlength="300" show-word-limit
<el-input v-model="config.password" maxlength="200" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
@ -67,7 +67,6 @@
data() {
return {
drivers: DatabaseConfig.DRIVER_CLASS,
// config: new DatabaseConfig(),
rules: {
name: [
{required: true, message: this.$t('commons.input_name'), trigger: 'blur'},
@ -95,7 +94,6 @@
this.$refs['databaseFrom'].validate((valid) => {
if (valid) {
this.$emit('save', this.config);
// this.config = new DatabaseConfig();
} else {
return false;
}

View File

@ -274,6 +274,38 @@ export class DubboSample extends DefaultTestElement {
}
}
export class JDBCSampler extends DefaultTestElement {
constructor(testName, request = {}) {
super('JDBCSampler', 'TestBeanGUI', 'JDBCSampler', testName);
this.stringProp("dataSource", request.dataSource);
this.stringProp("query", request.query);
this.stringProp("queryTimeout", request.queryTimeout);
this.stringProp("queryArguments");
this.stringProp("queryArgumentsTypes");
this.stringProp("resultSetMaxRows");
this.stringProp("resultVariable");
this.stringProp("variableNames");
this.stringProp("resultSetHandler", 'Store as String');
this.stringProp("queryType", 'Callable Statement');
}
}
// <JDBCSampler guiclass="TestBeanGUI" testclass="JDBCSampler" testname="JDBC Request" enabled="true">
// <stringProp name="dataSource">test</stringProp>
// <stringProp name="query">select id from test_plan;
// select name from test_plan;
// </stringProp>
// <stringProp name="queryArguments"></stringProp>
// <stringProp name="queryArgumentsTypes"></stringProp>
// <stringProp name="queryTimeout"></stringProp>
// <stringProp name="queryType">Callable Statement</stringProp>
// <stringProp name="resultSetHandler">Store as String</stringProp>
// <stringProp name="resultSetMaxRows"></stringProp>
// <stringProp name="resultVariable"></stringProp>
// <stringProp name="variableNames"></stringProp>
// </JDBCSampler>
export class HTTPSamplerProxy extends DefaultTestElement {
constructor(testName, options = {}) {
super('HTTPSamplerProxy', 'HttpTestSampleGui', 'HTTPSamplerProxy', testName);
@ -515,6 +547,29 @@ export class DNSCacheManager extends DefaultTestElement {
}
}
export class JDBCDataSource extends DefaultTestElement {
constructor(testName, datasource) {
super('JDBCDataSource', 'TestBeanGUI', 'JDBCDataSource', testName);
this.boolProp('autocommit', true);
this.boolProp('keepAlive', true);
this.boolProp('preinit', false);
this.stringProp('dataSource', datasource.name);
this.stringProp('dbUrl', datasource.dbUrl);
this.stringProp('driver', datasource.driver);
this.stringProp('username', datasource.username);
this.stringProp('password', datasource.password);
this.stringProp('poolMax', datasource.poolMax);
this.stringProp('timeout', datasource.timeout);
this.stringProp('connectionAge', '5000');
this.stringProp('trimInterval', '60000');
this.stringProp('transactionIsolation', 'DEFAULT');
this.stringProp('checkQuery');
this.stringProp('initQuery');
this.stringProp('connectionProperties');
}
}
export class Arguments extends DefaultTestElement {
constructor(testName, args) {
super('Arguments', 'ArgumentsPanel', 'Arguments', testName);

View File

@ -8,7 +8,7 @@ import {
HashTree,
HeaderManager,
HTTPSamplerArguments, HTTPsamplerFiles,
HTTPSamplerProxy,
HTTPSamplerProxy, JDBCDataSource, JDBCSampler,
JSONPathAssertion,
JSONPostProcessor, JSR223PostProcessor, JSR223PreProcessor,
RegexExtractor,
@ -211,7 +211,7 @@ export class Scenario extends BaseConfig {
this.environment = undefined;
this.enableCookieShare = false;
this.enable = true;
this.databaseConfigs = undefined;
this.databaseConfigs = [];
this.set(options);
this.sets({variables: KeyValue, headers: KeyValue, requests: RequestFactory, databaseConfigs: DatabaseConfig}, options);
@ -273,6 +273,7 @@ export class RequestFactory {
static TYPES = {
HTTP: "HTTP",
DUBBO: "DUBBO",
SQL: "SQL",
}
constructor(options = {}) {
@ -280,6 +281,8 @@ export class RequestFactory {
switch (options.type) {
case RequestFactory.TYPES.DUBBO:
return new DubboRequest(options);
case RequestFactory.TYPES.SQL:
return new SqlRequest(options);
default:
return new HttpRequest(options);
}
@ -460,6 +463,60 @@ export class DubboRequest extends Request {
}
}
export class SqlRequest extends Request {
constructor(options = {}) {
super(RequestFactory.TYPES.SQL);
this.id = options.id || uuid();
this.name = options.name;
this.dataSource = options.dataSource;
this.query = options.query;
// this.queryType = options.queryType;
this.queryTimeout = options.queryTimeout;
this.enable = options.enable === undefined ? true : options.enable;
this.assertions = new Assertions(options.assertions);
this.extract = new Extract(options.extract);
this.jsr223PreProcessor = new JSR223Processor(options.jsr223PreProcessor);
this.jsr223PostProcessor = new JSR223Processor(options.jsr223PostProcessor);
this.sets({args: KeyValue, attachmentArgs: KeyValue}, options);
}
isValid() {
if (this.enable) {
if (!this.name) {
return {
isValid: false,
info: 'name'
}
}
if (!this.dataSource) {
return {
isValid: false,
info: 'dataSource'
}
}
}
return {
isValid: true
}
}
showType() {
return "SQL";
}
showMethod() {
return "SQL";
}
clone() {
return new SqlRequest(this);
}
}
export class ConfigCenter extends BaseConfig {
static PROTOCOLS = ["zookeeper", "nacos", "apollo"];
@ -502,24 +559,6 @@ export class DatabaseConfig extends BaseConfig {
// options.id = options.id || uuid();
return options;
}
// <JDBCDataSource guiclass="TestBeanGUI" testclass="JDBCDataSource" testname="JDBC Connection Configurationqqq" enabled="true">
// <boolProp name="autocommit">true</boolProp>
// <stringProp name="checkQuery"></stringProp>
// <stringProp name="connectionAge">5000</stringProp>
// <stringProp name="connectionProperties"></stringProp>
// <stringProp name="dataSource">test</stringProp>
// <stringProp name="dbUrl">jdbc:mysql://localhost:3306/metersphere?autoReconnect=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;characterSetResults=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;allowMultiQueries=true</stringProp>
// <stringProp name="driver">com.mysql.jdbc.Driver</stringProp>
// <stringProp name="initQuery"></stringProp>
// <boolProp name="keepAlive">true</boolProp>
// <stringProp name="password">root</stringProp>
// <stringProp name="poolMax">10</stringProp>
// <boolProp name="preinit">false</boolProp>
// <stringProp name="timeout">10000</stringProp>
// <stringProp name="transactionIsolation">DEFAULT</stringProp>
// <stringProp name="trimInterval">60000</stringProp>
// <stringProp name="username">root</stringProp>
// </JDBCDataSource>
isValid() {
return !!this.name || !!this.poolMax || !!this.timeout || !!this.driver || !!this.dbUrl || !!this.username || !!this.password;
@ -929,6 +968,8 @@ class JMXGenerator {
// 放在计划或线程组中,不建议放具体某个请求中
this.addDNSCacheManager(threadGroup, scenario.requests[0]);
this.addJDBCDataSource(threadGroup, scenario);
scenario.requests.forEach(request => {
if (request.enable) {
if (!request.isValid()) return;
@ -936,9 +977,7 @@ class JMXGenerator {
if (request instanceof DubboRequest) {
sampler = new DubboSample(request.name || "", new JMXDubboRequest(request, scenario.dubboConfig));
}
if (request instanceof HttpRequest) {
} else if (request instanceof HttpRequest) {
sampler = new HTTPSamplerProxy(request.name || "", new JMXHttpRequest(request, scenario.environment));
this.addRequestHeader(sampler, request);
if (request.method.toUpperCase() === 'GET') {
@ -946,6 +985,8 @@ class JMXGenerator {
} else {
this.addRequestBody(sampler, request, testId);
}
} else if (request instanceof SqlRequest) {
sampler = new JDBCSampler(request.name || "", request);
}
this.addRequestExtractor(sampler, request);
@ -1009,6 +1050,13 @@ class JMXGenerator {
}
}
addJDBCDataSource(threadGroup, scenario) {
scenario.databaseConfigs.forEach(config => {
let name = config.name + "JDBCDataSource";
threadGroup.put(new JDBCDataSource(name, config));
});
}
addScenarioHeaders(threadGroup, scenario) {
let environment = scenario.environment;
if (environment) {