已知现在已经用Spring boot框架搭建了一个简单的web服务,并且有现成的Controller来处理http请求,以之前搭建的图书管理服务为例,BookController的源码如下:
java
package org.example.controller;
import org.example.domain.Book;
import org.example.service.BookService;
import org.example.vo.ResultVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping(value = "/books")
public class BookController {
@Autowired
private BookService bookService;
/**
* 查询所有图书
*
* @return
*/
@GetMapping(value = "/getAll")
public ResultVo getAll() {
List<Book> books = bookService.getAllBooks();
return new ResultVo().success().data(books);
}
/**
* 更新单个图书信息
*
* @param book
* @return
*/
@PostMapping(value = "/update")
public ResultVo updateBookMessage(@RequestBody Book book) {
Boolean result = bookService.updateBookMessage(book);
ResultVo resultVo = new ResultVo().data(result);
return result ? resultVo.success() : resultVo.fail();
}
}
在搭建一个Http接口功能自动化测试框架之前,我们需要思考几个问题:
1、http请求的发送,使用什么实现?
2、接口返回的json数据,用什么校验?
3、测试数据如何统一管理(数据驱动)?比如测试url、post请求需要的请求体、接口预期的返回结果等。
4、失败用例是否支持重试?
1、http请求工具类
目前网上能查到的比较主流的技术方案包括: java.net包中原生的 HttpURLConnection类、apache名下的http组件 HttpClient,以及Spring框架的 RestTemplate。相比之下,HttpURLConnection使用较为复杂,RestTemplate又不熟,就先选择HttpClient。
首先还是要引入HttpClient依赖(slf4j打log用):
xml
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
</dependency>
工具类源码如下,暂且先满足get 和 post两类请求的发送:
java
package org.example.utils;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class HttpUtil {
public static JSONObject doGet(String url) {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
try {
CloseableHttpResponse response = httpClient.execute(httpGet);
HttpEntity entity = response.getEntity();
String responseText = EntityUtils.toString(entity);
JSONObject responseJson = JSON.parseObject(responseText);
response.close();
httpClient.close();
return responseJson;
} catch (IOException | ParseException e) {
throw new RuntimeException(e);
}
}
public static JSONObject doPost(String url, JSONObject requestBody) {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(url);
StringEntity requestEntity = new StringEntity(requestBody.toString(), StandardCharsets.UTF_8);
httpPost.setEntity(requestEntity);
httpPost.setHeader("Content-Type", "application/json");
try {
CloseableHttpResponse response = httpClient.execute(httpPost);
HttpEntity httpEntity = response.getEntity();
String responseText = EntityUtils.toString(httpEntity);
JSONObject responseJson = JSON.parseObject(responseText);
response.close();
httpClient.close();
return responseJson;
} catch (IOException | ParseException e) {
throw new RuntimeException(e);
}
}
}
2、接口返回校验
这里主要是校验预期的返回体和实际返回体是否完全一致,也就是两个json数据的解析和比对。
json数据的解析工具很多,比如Google的Gson,阿里的fastjson,以及jackson等,这里随便选一个,使用fastjson;
json的比对,据调研有一个开源的org.skyscreamer名下的 jsonassert工具,不过还没有试用。这里自己写一个json diff的工具类,仅判断两份json数据是否相同,暂不考虑性能和其他功能拓展。
首先引入fastjson依赖:
xml
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
json diff 工具类源码如下:
java
package org.example.utils;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
public class JsonDiffUtil {
private static final Logger LOG = LoggerFactory.getLogger(JsonDiffUtil.class);
public static boolean compareJson(JSONObject expect, JSONObject actual) {
return compareJson(expect, actual, true, false);
}
//sorted参数用于判断数组中json object的顺序是否一致
public static boolean compareJson(JSONObject expect, JSONObject actual, boolean sorted) {
return compareJson(expect, actual, true, sorted);
}
//reportError参数:遍历json数组时,不匹配的json对象不需要打印error日志,故使用此参数控制
private static boolean compareJson(JSONObject expect, JSONObject actual, boolean reportError, boolean sorted) {
if (expect == null || actual == null) {
throw new RuntimeException("预期结果或返回结果不能为空");
}
if (expect.toString().equals(actual.toString())) {
return true;
}
Set<String> keySet = expect.keySet();
for (String key : keySet) {
if (!actual.containsKey(key)) {
if (reportError) {
LOG.error("key: {}不存在", key);
LOG.info("完整返回数据: {}", actual);
}
return false;
}
Object o1 = expect.get(key);
Object o2 = actual.get(key);
if (!isSameType(o1, o2)) {
if (reportError) {
LOG.error("类型不匹配, key: {}, 预期值: {}, 实际值: {}", key, o1.getClass().getTypeName(), o2.getClass().getTypeName());
LOG.info("完整返回数据: {}", actual);
}
return false;
}
if (o1 instanceof JSONArray) {
JSONArray ja1 = (JSONArray) o1;
JSONArray ja2 = (JSONArray) o2;
if (ja1.size() != ja2.size()) {
if (reportError) {
LOG.error("json数组长度不匹配, key: {}, 预期值: {}, 实际值: {}", key, o1, o2);
LOG.info("完整返回数据: {}", actual);
}
return false;
}
for (int i = 0; i < ja1.size(); i++) {
JSONObject jo1 = ja1.getJSONObject(i);
for (int j = 0; j < ja2.size(); j++) {
JSONObject jo2 = ja2.getJSONObject(j);
if (compareJson(jo1, jo2, false, sorted)) {
break;
}
if (j == ja2.size() - 1) {
LOG.error("key: {}, json数组中找不到预期的json对象{}", key, jo1);
LOG.info("完整返回数据: {}", actual);
return false;
}
}
}
if (sorted && !ja1.toString().equals(ja2.toString())) {
LOG.error("key: {}, json数组中对象顺序不一致, 预期值: {}, 实际值: {}", key, ja1, ja2);
LOG.info("完整返回数据: {}", actual);
return false;
}
} else if (o1 instanceof JSONObject) {
if (!compareJson((JSONObject) o1, (JSONObject) o2, true, sorted)) {
return false;
}
} else {
String s1 = String.valueOf(o1);
String s2 = String.valueOf(o2);
if (!s1.equals(s2)) {
if (reportError) {
LOG.error("key: {}, 预期值: {}, 实际值: {}", key, o1, o2);
LOG.info("完整返回数据: {}", actual);
}
return false;
}
}
}
return true;
}
private static boolean isSameType(Object o1, Object o2) {
if (o1 == null && o2 == null) {
return true;
}
if (o1 == null || o2 == null) {
return false;
}
return o1.getClass().getTypeName().equals(o2.getClass().getTypeName());
}
}
3、数据驱动
数据驱动是一个概念,可以简单理解为测试数据和测试用例的分离,也就是分开管理,然后数据通过参数的形式注入到测试用例中。
通常我们可以使用testNG框架的@DataProvider注解来实现这一功能。
首先导入testNG依赖(commons-io,用来处理IO的工具类):
xml
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.11</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8.0</version>
</dependency>
DataProvider工具类的源码如下:
它的作用是根据当前需要提供数据的Method,找到相关的测试数据(json文件),将文件内容解析为方法参数,传递给测试case。
java
package org.example.utils;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.apache.commons.io.FileUtils;
import org.testng.annotations.DataProvider;
import java.io.*;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;
public class DataProviderUtil {
@DataProvider(name = "dataProvider")
public static Object[] provide(Method method) throws IOException {
String className = method.getDeclaringClass().getSimpleName();
String methodName = method.getName();
String fileName = "src/main/java/org/example/data/" + className + ".json";
File file = new File(fileName);
String data = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
if (!JSON.isValid(data)) {
throw new RuntimeException("json格式校验失败, 文件名: " + fileName);
}
JSONArray jsonArray = JSON.parseArray(data);
List<Object> list = jsonArray.stream().filter(obj -> methodName.equals(((JSONObject) obj).getString("caseName"))).collect(Collectors.toList());
JSONObject jsonObject = (JSONObject) list.get(0);
String url = jsonObject.getString("url");
JSONObject requestBody = jsonObject.getJSONObject("requestBody");
JSONObject expectResponse = jsonObject.getJSONObject("expectResponse");
if (url == null) {
throw new RuntimeException("参数 url 不能为空, 用例: " + className + "." + methodName);
}
if (expectResponse == null) {
throw new RuntimeException("参数 expectResponse 不能为空, 用例: " + className + "." + methodName);
}
return new Object[][]{{url, requestBody, expectResponse}};
}
}
然后测试用例方法的@Test注解中需要指定该类作为数据提供者,比如:
@Test(dataProvider = "dataProvider", dataProviderClass = DataProviderUtil.class)
小结:
解决了以上三个核心问题之后,实际上我们已经可以轻松的编写测试用例了,比如要测试BookController相关的http接口,建立一个BookControllerTest作为测试类,里面为每个接口设计一个case,源码如下:
java
package org.example.testcase;
import com.alibaba.fastjson2.JSONObject;
import org.example.utils.DataProviderUtil;
import org.example.utils.HttpUtil;
import org.example.utils.JsonDiffUtil;
import org.testng.Assert;
import org.testng.annotations.Test;
public class BookControllerTest {
@Test(dataProvider = "dataProvider", dataProviderClass = DataProviderUtil.class)
public void getAllTest(String url, JSONObject requestBody, JSONObject expectResponse) {
JSONObject response = HttpUtil.doGet(url);
Assert.assertTrue(JsonDiffUtil.compareJson(expectResponse, response));
}
@Test(dataProvider = "dataProvider", dataProviderClass = DataProviderUtil.class)
public void updateTest(String url, JSONObject requestBody, JSONObject expectResponse) {
JSONObject response = HttpUtil.doPost(url, requestBody);
Assert.assertTrue(JsonDiffUtil.compareJson(expectResponse, response));
}
}
4、testNG对失败重试的支持
失败用例的重试,需要定义两个类,分别需要实现 IAnnotationTransformer监听器接口,来指定重试处理类;以及 IRetryAnalyzer接口,来制定重试规则。源码如下:
java
package org.example.listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.*;
import org.testng.annotations.ITestAnnotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class RetryListener implements IAnnotationTransformer {
private static final Logger LOG = LoggerFactory.getLogger(RetryListener.class);
@Override
public void transform(ITestAnnotation annotation, Class testClass, Constructor testConstructor, Method testMethod) {
IRetryAnalyzer analyzer = annotation.getRetryAnalyzer();
if (analyzer == null) {
annotation.setRetryAnalyzer(Retry.class);
}
}
public static class Retry implements IRetryAnalyzer {
private int currentRetryCount = 0;
@Override
public boolean retry(ITestResult result) {
//重试2次
if (currentRetryCount < 2) {
currentRetryCount++;
LOG.info("准备第" + currentRetryCount + "次重试");
return true;
}
return false;
}
}
}
然后需要在制定测试规则的 testng.xml文件中,使用 listener 标签添加该监听器。
testng.xml文件如下:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Default Suite">
<test name="Default Test">
<classes>
<class name="org.example.testcase.BookControllerTest">
<methods>
<include name="getAllTest"/>
<include name="updateTest"/>
</methods>
</class>
</classes>
</test>
<listeners>
<listener class-name="org.example.listener.RetryListener"/>
</listeners>
</suite>
到这里,重试功能就添加完毕了。运行效果如下,最后一次重试失败,则最终定性为失败,之前的按 ignored处理。
最后,整个项目的结构图: