Commit 7d0e52ef authored by xie.qin's avatar xie.qin

REST interface part 3 is done.

parent eb27ae45
......@@ -59,5 +59,22 @@ English, please click [here](./HELP.md)
##### 局限性
- 更多需要讨论的是 API 测试本身的局限性,和本测试框架无关。
#### 1.1.3 构造微服务 mock server,根据不同的请求返回不同的响应结果 (集成测试)
#### 1.1.3 构造微服务 mock server,根据不同的请求返回不同的响应指定结果 (集成测试)
##### 测试方法
1. 准备json格式的请求响应数据;
2. 不同于 1.1.1 和 1.1.2 部分,mock server将无法集成到一个具体的测试用例中,它应该被部署为一个真实服务供客户端服务(在开发中的测试服务对象)调用;
3. 启动命令:java -jar auto-test-1.0.0-SNAPSHOT.jar --mvcmock.response.path=<> #参数为请求响应数据路径
4. 工作原理说明:
- mock server将逐一匹配请求数据的每一个域,最后返回与请求数据匹配度最高的某一个测试数据中response域的值。
- 即,不同的两个测试数据,可以仅有一个域的值不同;mock server进行精确匹配,返回匹配成功的那个数据的response域值。
- [数据格式参考](./mvcmock/backend.json)
##### 用例参考
N/A
##### 优点
- 测试数据支持动态修改,无需重启服务。
- 返回结果支持参数化,即可以根据请求路径中的参数,动态替换返回结果中对应的参数的值。
......@@ -6,8 +6,9 @@ plugins {
}
group = 'com.fuzamei'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
version = '1.0.0-SNAPSHOT'
//sourceCompatibility = '1.8'
sourceCompatibility = '11'
configurations {
compileOnly {
......
{
"backend.001": {
"description": "这是一个微服务mock的例子—PATH中包含变量",
"request": {
"path": "/userservice/fedFromOrg/{orgId}",
"contentType": "application/json",
"headers": {
"User-ID": "",
"User-Name": ""
},
"method": "post",
"body": {
}
},
"response": {
"statusCode": 200,
"body": {
"code": "baas.err.success",
"data": true,
"message": "OK",
"status": "OK"
}
}
},
"backend.002": {
"description": "这是一个微服务mock的例子—PATH中携带查询参数",
"request": {
"path": "/userservice/federation",
"queryInPath": true,
"contentType": "application/json",
"headers": {
"User-ID": "",
"User-Name": ""
},
"method": "post",
"body": {
}
},
"response": {
"statusCode": 200,
"body": {
"code": "baas.err.success",
"data": {
"fedName": "联盟名",
"id": 87654321,
"joinTime": 87654321,
"orgName": "企业名",
"quitTime": 87654321,
"status": 0
},
"message": "OK",
"status": "OK"
}
}
}
}
\ No newline at end of file
package com.fuzamei.autotest.controller;
import org.springframework.web.bind.annotation.*;
@RestController
public class ServiceMockController {
/**
* https://www.baeldung.com/spring-mock-mvc-rest-assured
* @return
*/
@GetMapping("/")
public String getVersion() {
return "1.0";
}
@PostMapping("/")
public String postVersion() {
return "1.0";
}
@PutMapping("/")
public String putVersion() {
return "1.0";
}
}
......@@ -23,4 +23,7 @@ public class SpecificProperties {
@Value("${restassured.sockettimeout}")
private Integer restSocketTimeout;
@Value("${mvcmock.response.path}")
private String mvcMockResponsePath;
}
package com.fuzamei.autotest.servicemock;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Enumeration;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RestEntity {
private String path;
private String method;
private String contentType;
private Enumeration<String> headers;
private JsonNode response;
}
package com.fuzamei.autotest.servicemock;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.Map;
@RestController
@RequestMapping("/**")
@Slf4j
public class RestMockController {
@Autowired
RestMockService restMockService;
@Autowired
private HttpServletRequest request;
@RequestMapping()
@ResponseBody
@ResponseStatus(HttpStatus.OK)
public ResponseEntity<?> getMockService() {
String path = request.getServletPath();
if (request.getQueryString() != null){
path += "?" + request.getQueryString();
}
String httpMethod = request.getMethod();
String contentType = request.getContentType();
Enumeration<String> headers = request.getHeaderNames();
RestEntity restEntity = RestEntity.builder()
.method(httpMethod)
.path(path)
.contentType(contentType)
.headers(headers)
.build();
Map<String, Object> mapResp = null;
try {
mapResp = restMockService.respondAsDefinedJsonFile(restEntity);
log.debug("get return from service. return is {}:", mapResp);
}
catch (Exception ex){
log.error("call service failed as:", ex);
}
if (mapResp.get("response") != null) {
JsonNode response = (JsonNode) mapResp.get("response");
int statusCode = response.get("statusCode").asInt();
HttpStatus httpStatus = null;
switch (statusCode){
case 200:
httpStatus = HttpStatus.OK;
break;
}
JsonNode responseBody = response.get("body");
return new ResponseEntity<>(responseBody, httpStatus);
}
else {
throw new RuntimeException(mapResp.get("failureCause").toString());
}
}
@ResponseBody
@ExceptionHandler(value = RuntimeException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
private String mockServiceExceptionHandler(HttpServletRequest req, RuntimeException ex) {
String url = req.getRequestURI();
return "request error at '" + url + "' as: " + ex;
}
}
package com.fuzamei.autotest.servicemock;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fuzamei.autotest.properties.SpecificProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException;
import java.util.*;
@Service
@Slf4j
public class RestMockService {
@Autowired
SpecificProperties specificProperties;
public Map<String, Object> respondAsDefinedJsonFile(RestEntity request) {
Map<String, Object> mapResp = new HashMap<>();
JsonNode response = null;
JsonNode rootNode = null;
String failureCause = "Cannot find out a valid response to match request. Please check your testing data and request message.";
try{
File file = new File(specificProperties.getMvcMockResponsePath());
ObjectMapper objectMapper = new ObjectMapper();
rootNode = objectMapper.readTree(file);
}
catch (IOException exception) {
log.error("Data file is not existed or it is not a valid json file.", exception);
failureCause = "The data json file [" + specificProperties.getMvcMockResponsePath() + "] is not existed or it is not a valid json file";
mapResp.put("response", null);
mapResp.put("failureCause", failureCause);
return mapResp;
}
Iterator<Map.Entry<String, JsonNode>> it = rootNode.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> eachDataNode = it.next();
log.debug("now retrieving data number: {}", eachDataNode.getKey());
Boolean canRespond = false;
if (eachDataNode.getValue().has("request")) {
JsonNode requestDataNode = eachDataNode.getValue().get("request");
//analyze field path
String expectedPath = requestDataNode.get("path").asText();
String receivedPath = request.getPath();
//received path includes query parameter?
if (receivedPath.contains("?") && !requestDataNode.has("queryInPath")) {
log.debug("path does not match the received request path: {} in data number: {}.", receivedPath, eachDataNode.getKey());
continue;
} else {
receivedPath = receivedPath.split("\\?")[0];
}
if (expectedPath.split("\\/").length != receivedPath.split("\\/").length) {
log.debug("path: {} does not match the received request: {} in data number {}.", expectedPath, receivedPath, eachDataNode.getKey());
continue;
}
String[] expectedPathSeg = expectedPath.split("\\/");
String[] receivedPathSeg = receivedPath.split("\\/");
for (int i = 0; i < expectedPathSeg.length; i++) {
if (!expectedPathSeg[i].contains("{") && !expectedPathSeg[i].equals(receivedPathSeg[i])) {
log.debug("path: {} does not match the received request: {} in data number {} .", expectedPath, receivedPath, eachDataNode.getKey());
continue;
}
}
//analyze contentType
if (requestDataNode.has("contentType") && !requestDataNode.get("contentType").asText().equalsIgnoreCase(request.getContentType())){
log.info("Content-Type mismatch between received: {} and expected: {} for path: {} in data number: {}", request.getContentType(), requestDataNode.get("contentType").asText(), receivedPath, eachDataNode.getKey());
continue;
}
//analyze headers
if (requestDataNode.has("headers")) {
JsonNode expectedHeaders = requestDataNode.get("headers");
Iterator<Map.Entry<String, JsonNode>> iter = expectedHeaders.fields();
Boolean allMatched = true;
while (iter.hasNext()){
Map.Entry<String, JsonNode> header = iter.next();
Enumeration<String> receivedHeaders = request.getHeaders();
Boolean matched = false;
while (receivedHeaders.hasMoreElements()){
if (header.getKey().equalsIgnoreCase(receivedHeaders.nextElement())){
matched = true;
break;
}
}
if (!matched){
allMatched = false;
log.warn("Expected header: {} does not include in received request for path: {} in testing data number: {}", header.getKey(), receivedPath, eachDataNode.getKey());
break;
}
}
if (!allMatched){
continue;
}
}
//analyze method
if (requestDataNode.has("method") && !requestDataNode.get("method").asText().equalsIgnoreCase(request.getMethod())){
log.warn("Http method: {} of received does not match expected: {} for path: {} in testing data number: {}", request.getMethod(), requestDataNode.get("method").asText(), receivedPath, eachDataNode.getKey());
continue;
}
//request analysis done
//return response
canRespond = true;
}
if (canRespond) {
if (eachDataNode.getValue().has("response") &&
eachDataNode.getValue().get("response").has("statusCode") &&
eachDataNode.getValue().get("response").has("body")) {
response = eachDataNode.getValue().get("response");
mapResp.put("response", response);
mapResp.put("failureCause", "succeed");
}
return mapResp;
}
}
mapResp.put("response", null);
mapResp.put("failureCause", failureCause);
return mapResp;
}
}
......@@ -37,4 +37,6 @@ mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
restassured.connecttimeout=10000
restassured.requesttimeout=10000
restassured.sockettimeout=10000
\ No newline at end of file
restassured.sockettimeout=10000
mvcmock.response.path=mvcmock/backend.json
\ No newline at end of file
......@@ -5,6 +5,6 @@ import org.springframework.boot.test.context.SpringBootTest;
@CucumberContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class CucumberSpringConfiguration {
}
package com.fuzamei.autotest.steps.openapi;
import io.cucumber.java.en.Then;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.WebApplicationContext;
public class MockService {
@Then("^Start mock service on port (.*?) with dataNum (.*?)$")
public void startMockServiceOnPort(Integer port, String testDataNum) throws Throwable {
}
}
......@@ -2,8 +2,9 @@
Feature: User Management Test
@tmslink=5 @severity=normal
Scenario: User register verification
# 用户注册分为两步:往指定手机号码发送验证码;填入验证码注册新用户
# 用户注册分为两步:往指定手机号码发送验证码;填入验证码注册新用户
Given The testing RUNTIME data user_management.UserController is ready
Then Send RESTful request with dataNum userservice.code.001 to verify interface 发送验证码
Then Send RESTful request with dataNum userservice.code.002 to verify interface 用户注册
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"userservice.code.001":{
"userservice.code.001": {
"description": "发送验证码-合法手机号码",
"request": {
"service": "backend",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment