赞
踩
已知现在已经用Spring boot框架搭建了一个简单的web服务,并且有现成的Controller来处理http请求,以之前搭建的图书管理服务为例,BookController的源码如下:
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、失败用例是否支持重试?
目前网上能查到的比较主流的技术方案包括: java.net包中原生的 HttpURLConnection类、apache名下的http组件 HttpClient,以及Spring框架的 RestTemplate。相比之下,HttpURLConnection使用较为复杂,RestTemplate又不熟,就先选择HttpClient。
首先还是要引入HttpClient依赖(slf4j打log用):
<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两类请求的发送:
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); } } }
这里主要是校验预期的返回体和实际返回体是否完全一致,也就是两个json数据的解析和比对。
json数据的解析工具很多,比如Google的Gson,阿里的fastjson,以及jackson等,这里随便选一个,使用fastjson;
json的比对,据调研有一个开源的org.skyscreamer名下的 jsonassert工具,不过还没有试用。这里自己写一个json diff的工具类,仅判断两份json数据是否相同,暂不考虑性能和其他功能拓展。
首先引入fastjson依赖:
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
json diff 工具类源码如下:
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()); } }
数据驱动是一个概念,可以简单理解为测试数据和测试用例的分离,也就是分开管理,然后数据通过参数的形式注入到测试用例中。
通常我们可以使用testNG框架的@DataProvider注解来实现这一功能。
首先导入testNG依赖(commons-io,用来处理IO的工具类):
<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。
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,源码如下:
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)); } }
失败用例的重试,需要定义两个类,分别需要实现 IAnnotationTransformer监听器接口,来指定重试处理类;以及 IRetryAnalyzer接口,来制定重试规则。源码如下:
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 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处理。
最后,整个项目的结构图:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。