上篇讲了什么是接口,如何通过postman去执行单接口。还不了解的,建议先看下接口的表现curl 直通上文
这篇文章重点记录下如何用自动化实现快速接口自动化。整个项目思路就是使用testng编写测试用例,引入springboot,使用注解快速实现。Jenkins调用xml去执行测试用例,最后生成报告。
通过持续集成Jenkins去执行并生成测试报告
本文讲述尽我可能的详细,小白对于个别注解或者引入包觉得模糊不清的,自行去补充下。文章比较基础,接下来我们一起梳理下吧。
项目创建流程
1.创建一个Maven 项目
2. 在pom.xml文件中引入testng,springboot等的jar包
maven的好处就是无需手动寻找jar包引入,直接pom文件引入就可以,maven会自动解决重复和冲突问题,统一管理所有的依赖包
该项目中所用到的jar,大家根据自己的情况选择性引入
<dependencies>
<!--引入springboot相关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vinmtage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--java自动构造器,需手动引入插件,自动生成getter/setter、equals、hashcode、toString-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--java集合框架-->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<!--testng执行接口进行断言-->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.13.1</version>
<scope>test</scope>
</dependency>
<!--jason转换-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<!--http请求-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
<dependency>
<!--配置allure,生成测试报告-->
<groupId>io.qameta.allure</groupId>
<artifactId>allure-testng</artifactId>
<version>2.13.0</version>
<scope>test</scope>
</dependency>
<!--链接mysql:jdbc,进程少,为了更快速,没有引入mybatis-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
<!--解析yaml-->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.17</version>
</dependency>
<!--redis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!--Kafka-->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.11</artifactId>
<version>2.2.0</version>
</dependency>
</dependencies>
3.整体项目目录介绍
4.新建即将运行xml,所含内容
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="hema_hotel_stage2" parallel="classes" thread-count="1">
<!-- 域名配置:预发环境,suite名字随意命名-->
<!-- 预发CASE配置 -->
<test name="henmaHotel Test Cases">
<!-- 接口url头、header信息参数配置,通过testng直接引入,方便统一管理 -->
<parameter name="apiHost" value="http://xxxxxx.xx.com/xx"/>
<parameter name="token" value="5dfc2b8f9725eb0007d0e9ea"/>
<parameter name="traceSource" value="publicactivity"/>
<!--配置数据库域名,及数据库name,通过接口的方式获得数据库返回结果json数据。线上 专用-->
<parameter name="dburl" value="http://gateway.qcenter.xxxx.com/gateway/data/select"></parameter>
<parameter name="dbname" value="TEArsenalMarketingSingleProduct"></parameter>
<!--配置redis组,通过redis_properties.yaml找到对应的ip,获取redis中短信验证码-->
<parameter name="redisGroup" value="redis-arsenal-produck"></parameter>
<!--接口请求体中用到的参数,如果很多接口共用一个参数,也通过xml进行统一管理-->
<parameter name="hotel_block" value="00101276"/>
<parameter name="block_member" value="240000000804143402"/>
<classes>
<!--calsses中可以维护多个class类,运行时按照这里的顺序依次执行-->
<!--短信验证码-->
<class name="com.ly.qa.api.arsenal.hemaHotel.SendCode"></class>
<class name="com.ly.qa.api.arsenal.hemaHotel.GetSendCode"></class>
<!--裂变用户注册-->
<class name="com.ly.qa.api.arsenal.hemaHotel.AgentWithVerifyPhone_Clint"></class>
</classes>
</test>
<listeners>
<!--为了解决一个类中多个方法可以按照已定义的顺序执行,引入监听,该文件网上一搜一堆-->
<!--配置监听-->
<listener class-name="com.ly.qa.api.arsenal.common.RePrioritizingListener"/>
</listeners>
</suite>
5.原生httpclient实现get请求
实现接口deleteById 对应curl:
curl --location --request GET 'http://arsenalgw.elong.com/xxxxx/deleteById?token=nopass.2&id=0001' \
--header 'token: 5dfb21xxxxxxx40f' \
--header 'traceSource: pubxxxxxtage' \
--header 'Cookie: H5CookieId=af7c197f-0558-47b6-b376-2ee166a6fedf' \
--data-raw ''
java实现类 deleteById
package com.ly.qa.api.arsenal.hemaHotel;
import com.ly.qa.api.core.RestUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.testng.Assert;
import java.io.IOException;
import java.net.URISyntaxException;
@Service
public class DeleteUserInformation extends AbstractTestNGSpringContextTests {
@Autowired
RestUtils requests;
private String api = "/xxxxx/deleteById";
/**
* 描述:删除BD及裂变用户注册信息
* 预期:1.结果状态为success
* 备注:获取用户的id
*/
//接口所需要url,headers,以及删除用户头部信息,都通过xml配置文件传递过来,同名匹配规则。
@Test(priority = 1) //方法执行顺序
@Parameters({"apiHost", "headers", "userid"})
public void deleteBd_clint(String apiHost, HttpHeaders headers, String userid) {
//拼接url路径
String url = apiHost.concat(api);
//创建一个httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//地址拼接参数
URIBuilder uriBuilder = null;
try {
uriBuilder = new URIBuilder(url);
//添加get请求 parameter
uriBuilder.addParameter("token", "nopass.2");
uriBuilder.addParameter("id", userid);
HttpGet httpGet = new HttpGet(uriBuilder.build());
//添加header
httpGet.setHeader("token", headers.getFirst("token"));
httpGet.setHeader("traceSource", headers.getFirst("traceSource"));
//执行请求
HttpResponse httpResponse = httpClient.execute(httpGet);
//获取响应结果
HttpEntity httpEntity = httpResponse.getEntity();
String result = EntityUtils.toString(httpEntity, "utf-8");
//断言结果为成功
Assert.assertEquals(result, "success");
} catch (URISyntaxException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
6.进一步封装,通过restTemplate实现post请求,进行简单的响应结果断言,
restTemolate有好多种实现方法,自行了解下,这里只引入本案例用到的
- 还是一样,先来看一下即将要实现的接口curl
curl --location --request POST 'http://arsenalgw.qa.elong.com/xx/x/xxx/agentWithVerifyPhone' \
--header 'Cookie: SessionToken=txEY4M/+xxx+u92yQzQx3XNFXmQw=; H5CookieId=af7c197f-0558-47b6-b376-xx' \
--data-raw '{
"registerPhone": "15400000001",
"inviteAgentId": "547",
"role": "client"
}'
- 实现接口代码片段
@SpringBootTest //继承接口实现了请求头以及数据库。进程进来时,会先执行继承类,获取到里面的值
public class AgentWithVerifyPhone_Clint extends ArsenalBaseTestNG {
//自动注入,免去new对象的麻烦,直接调用类 DeleteUserInformation,往上翻看就会发现这是上面介绍的删除类
@Autowired
DeleteUserInformation deleteUserInformation;
// 请求接口
private String api = "/xx/create/agentWithVerifyPhone";
/**
* 接口描述:裂变用户注册
* 方法:POST
* 预期:注册成功与邀请人形成上下级关系
*/
@Test(priority = 1)
@Parameters({"seesionToken", "phoneNo", "inviteAgentId"})
public void testClint(String seesionToken, String phoneNo, String inviteAgentId) {
deleteuserid();
// 请求完整url拼接
String url = apiHost.concat(api);
// 拼接header
HttpHeaders headers = new HttpHeaders();
headers.add("Cookie", "SessionToken=" + seesionToken);
headers.add("token", token);
headers.add("traceSource", traceSource);
//获取请求参数jsonbody
JSONObject params = new JSONObject();
params.put("registerPhone", "client");
params.put("role", "client");
params.put("inviteAgentId", "112");
ResEntity resEntity = post(url, headers, params);
System.out.println("代理商注册响应:" + resEntity);
//断言请求接口响应200
Assert.assertEquals(resEntity.getCode(), 200, "请求状态非200");
String code = resEntity.getBody().getString("code");
Assert.assertEquals(code, "0", "注册失败");
}
//执行接口,可以抽出来封装一下
RestTemplate restTemplate;
public ResEntity post(String url, HttpHeaders headers, JSONObject jsonBody) {
//创建实例类,方便后续get
ResEntity resEntity = new ResEntity();
HttpEntity<String> httpEntity = new HttpEntity(jsonBody, headers);
ResponseEntity<JSONObject> responseEntity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, JSONObject.class);
HttpStatus status = responseEntity.getStatusCode();
//把响应code码、body内容存放到bean里
resEntity.setCode(status.value());
resEntity.setBody(responseEntity.getBody());
return resEntity;
}
//
// @Data
// @NoArgsConstructor
// @AllArgsConstructor
// public class ResEntity {
//
// private int code;
// private JSONObject body;
//
// }
}
- 父类代码 ArsenalBaseTestNG
@SpringBootTest
public class ArsenalBaseTestNG extends AbstractTestNGSpringContextTests {
@Parameters({"apiHost", "token", "traceSource", "dburl", "dbname"})
@BeforeClass //执行类之前的运行方法
public void initParameters(String apiHost, String token, String traceSource, String dburl, String dbname) {
//可以把接口中通用的url拼接,header头信息等放到这里,统一管理,就不用每次写接口都重复写一遍重复的代码
}
@BeforeMethod //方法执行之前执行注解,只是为了日志输出漂亮点的分割线
public void testStart(Method method) {
System.out.println(">>>>>>>>>>>>>>>>>>>> Test case >>> " + method.getName());
}
@AfterClass //类执行后的方法
public void finishTest() {
System.out.println("====== End Test Class ".concat(this.getClass().getName()).concat(" ======"));
}
}
7. 引入jdbc链接数据库
package com.ly.qa.api.arsenal.common;
import org.springframework.context.annotation.Configuration;
import java.sql.*;
import java.util.*;
/**
* 数据库操作工具
*
* @author junjun.dai
*/
@Configuration
public class DbUtilsQA {
static Connection conn = null;
public static String driverClassName = "com.mysql.jdbc.Driver";
public static String url = "jdbc:mysql://10.160.172.45:3046/数据库名称";
public static String username = "TEArsenalMarketing";
public static String password = "xxx";
/**
* 执行sql
*
* @param sql sql语句
* @return
*/
public static List<Map<String, String>> getDataList(String sql,String[] columns) {
List<Map<String, String>> paramList = new ArrayList<Map<String, String>>();
Map<String, String> param = new HashMap<>();
Statement stmt = null;
try {
// 注册 JDBC 驱动
Class.forName(driverClassName);
// 打开链接
conn = DriverManager.getConnection(url, username, password);
// 执行查询
stmt = conn.createStatement();
ResultSet rs = null;
rs = stmt.executeQuery(sql);
// 展开结果集数据库
while (rs.next()) {
Map<String, String> map = new LinkedHashMap<String, String>();
for (int i = 0; i < columns.length; i++) {
String cellData = rs.getString(columns[i]);
map.put(columns[i], cellData);
}
paramList.add(map);
}
// 完成后关闭
rs.close();
stmt.close();
conn.close();
} catch (SQLException se) {
// 处理 JDBC 错误
System.out.println("处理 JDBC 错误!");
} catch (Exception e) {
// 处理 Class.forName 错误
System.out.println("处理 Class.forName 错误");
} finally {
// 关闭资源
try {
if (stmt != null) stmt.close();
if (conn != null) conn.close();
} catch (SQLException se) {
se.printStackTrace();
}
}
return paramList;
}
/**
* 更新sql
*
* @param sql
* @return
*/
public static Boolean executeSQL(String sql) {
Statement stmt = null;
boolean execute = Boolean.FALSE;
try {
// 注册 JDBC 驱动
Class.forName(driverClassName);
// 打开链接
conn = DriverManager.getConnection(url, username, password);
// 执行查询
stmt = conn.createStatement();
execute = stmt.execute(sql);
// 完成后关闭
stmt.close();
conn.close();
} catch (SQLException se) {
// 处理 JDBC 错误
System.out.println("处理 JDBC 错误!");
} catch (Exception e) {
// 处理 Class.forName 错误
System.out.println("处理 Class.forName 错误");
} finally {
// 关闭资源
try {
if (stmt != null) stmt.close();
if (conn != null) conn.close();
} catch (SQLException se) {
se.printStackTrace();
}
}
return execute;
}
public List<Map<String, String>> dbDataMethod( String sql,String[] columns) {
// String sql = "SELECT * FROM `activity_trace` WHERE template_id='552'";
List<Map<String, String>> result = getDataList(sql,columns);
return result;
}
public Boolean dbDataIsNull(String sql, String[] columns) {
List<Map<String, String>> result = getDataList(sql,columns);
return result.isEmpty();
}
public Boolean execute(String sql) {
return executeSQL(sql);
}
}
8.接口类中,查询库数据与接口响应body做对比,断言数据库
@SpringBootTest
public class AgentCodeClientAgentId extends AbstractTestNGSpringContextTests {
@Autowired
RestUtils requests;
@Autowired //引入数据库
DbUtilsQA dbUtils;
// 请求接口
private String api = "/hotel/xxx/xxx/agentcode";
/**
* 预期:裂变用户注册
* 1、通过seesionToken判断用户是否已存在,已存在进行删除
* 2、裂变用户进行注册。响应成功 msg:success。注册成功与邀请人形成上下级关系
* 备注:˙注册成功,裂变用户为相对二级用户。一级用户id:112(agent表中用户id)
*/
@Test(groups = "client",priority = 3)
@Parameters({"apiHost", "seesionToken", "token", "traceSource"})
public void testClient(String apiHost, String seesionToken, String token, String traceSource) {
// 请求完整url拼接
String url = apiHost.concat(api);
// 拼接header
HttpHeaders headers = new HttpHeaders();
headers.add("Cookie", "SessionToken=" + seesionToken);
headers.add("token", token);
headers.add("traceSource", traceSource);
//获取请求参数jsonbody
JSONObject params = new JSONObject();
params.put("role", "client");
params.put("inviteAgentId", "112");
ResEntity resEntity = requests.post(url, headers, params);
System.out.println("代理商注册响应:" + resEntity);
//断言请求接口响应200
Assert.assertEquals(resEntity.getCode(), 200, "请求状态200");
//返回数据是json串,取json中key:date。
//{"code":"codeExpire","msg":"success","data":{"agentId":"202007081038"},"responseTime":"2021-04-27 16:41:54"}
String agentId = resEntity.getBody().getJSONObject("data").getString("agentId");
String[] columns_role = {"agent_role", "parent_agent_id"};
String querySql = "select * from distribution_agent where agent_member_id =" + agentId;
List<Map<String, String>> mapList = dbUtils.dbDataMethod(querySql, columns_role);
Assert.assertFalse(mapList.isEmpty());
for (Map<String, String> map : mapList) {
Assert.assertEquals(map.get("agent_role"), "裂变用户", "角色注册不正确");
Assert.assertEquals(map.get("parent_agent_id"), "190000000032799250", "父级关联关系不正确");
}
System.out.println("-------------裂变用户角色注册 自动化测试通过----------------");
System.out.println();
}
9.公共类参数,把接口中的数据,存放在公共类中,用以其他需要的接口的请求入参传入
存放和取值的代码片段
//把接口响应数据,存到公共变量里
distribution_param.inviteIdentityCode = data;
//获取公共类中的参数。一般是一个xml执行时,顺序存放,顺序取值。
String pagedata = distribution_param.inviteIdentityCode;
10. 右键xml,run执行接口
11. tips:
- 入参json层次比较深的话,用object对象代码会比较清晰
post接口请求body
对应取值方式,反向解析
- 如果层级不深或者后续使用参数较少,可以直接text全部引入
- 这里也可以引用mock的数据,当做入参。
比如,在mock平台维护好需要的参数 (一般用作参数改动频繁,或者依赖外部参数传递,保证内部实现逻辑正确的情况)
mock维护的数据展示
通过RestTemplate方法调用执行接口,贴代码片段
@SpringBootTest
public class OrderSync extends ArsenalBaseTestNG {
// 请求接口
private String api = "/hotel/xxx/xxx/sync";
//mock配置的接口地址信息
private String caseDataApi = "/arsenal/cases/hotel/distribution/order/sync";
public String apiHost = "http://mock.17usoft.com";
//订单号id随机数。利用时间戳13位
Date date = new Date();
String bizOrderNo = String.valueOf(date.getTime());
JSONObject raw;
@BeforeTest
public void initTestData() {
// rul = http://mock.17usoft.com/hotel/arsenal/cases/hotel/distribution/order/sync
String url = apiHost.concat(caseDataApi);
//类似于 Apache的HttpClient,使用更简单粗暴
//getForObject这三个参数分别代表 请求地址、HTTP响应转换被转换成的对象类型。
RestTemplate restTemplate = new RestTemplate();
//获取到mock中维护的json,存放到raw对象中
raw = restTemplate.getForObject(url, JSONObject.class);
}
/**
* 描述:操作订单推送
* 预期,code=0
*/
@Test
@Parameters({"seesionToken", "instanceid"})
public void testSync(String seesionToken, String instanceid) {
// 拼接header
headers.add("Cookie", "SessionToken=" + seesionToken);
//参数一直变化的,单独put进来
raw.put("instanceId",instanceid);
raw.put("updateTimestamp","1594958297883");
raw.put("bizOrderNo",bizOrderNo);
//该请求方式,上面的代码片段中描述过,依然是使用restTmplate的一个封装调用
ResEntity resEntity = requests.postFromApi(api, headers, raw);
//断言请求接口响应200
Assert.assertEquals(resEntity.getCode(), 200, "请求状态200");
//断言响应code=0,推送成功。
Assert.assertEquals(resEntity.getBody().getString("code"), "0", "订单推送操作");
}
}
main 方法测试一下,不难发现raw中的数据,就是在mock中配置好的。