项目名称:
基于SpringBoot的金刚模板化接口自动化测试项目
说明:
- 用到的模板要根据具体的项目进行定制
- 这个项目有些模块代码写的比较粗糙一些,后续逐步优化调整
- 有问题可邮箱联系沟通,邮箱:[email protected]
说明:
- 下面是一个样例,提前约定好各个模块的包名,方便团队成员协同维护用例
- 所有模块的用例集维护在com.jingang.springinterfacetest.cases下面
模块中文名 | 模块对应的包名 | 脚本维护负责人 | 备注 |
---|---|---|---|
控制台--首页 | homePage | ||
控制台--全部产品 | allProduct | ||
控制台--我的应用 | myApp | ||
主要模块:用例维护、配置管理、公共方法、数据管理、报告模块、告警通知
模块 | 支持的功能 | 备注 |
---|---|---|
用例维护 | 静态数据用例、动态数据用例 | 动态数据用例基于DataProvider实现 |
配置管理 | 手工加载配置、springBoot自动加载配置 | 静态基于Excel实现 |
通用模块 | 维护与业务相关的公共模块或数据准备方法 | |
测试工具类 | http请求类、DB资源加载、JsonSchemaByFge、常用数据加工方法、DNS配置等待处理方法 | |
数据管理 | 基于properties的数据管理、基于Excel的数据管理、基于Yaml的数据管理等 | |
告警通知 | Email通知、企业微信群机器人通知 |
├── jg-api-autotest XX接口自动化测试项目
│ ├── src/main/java/com.jingang.springinterfacetest
│ │ ├── cases(维护测试用例)
│ │ │ ├── demoCaseModule(用例demo)
│ │ │ ├── TestNGSpringBootCaseConfig.java(测试用例父类)
│ │ │ ├── XXX(各个用例模块)
│ │ ├── common(通用模块,一般维护与具体业务相关的一些公共模块类、通用的一些方法)
│ │ │ ├── GetTestData.java(获取测试数据通用类)
│ │ │ ├── SendNotification.java(发送通知的公共类)
│ │ ├── config(测试配置类)
│ │ │ ├── ExtentTestNGIReporterListener.java(生成测试报告的监听类)
│ │ │ ├── JGTestNgListener.java(自定义的testNg的监听类,实现某些后续操作)
│ │ │ ├── TestConfig.java(笨方式,用于ResourceBundle读取属性配置,设置静态的配置实体类,不建议使用)
│ │ │ ├── TestDataConfig.java(测试数据配置的实体类,自动从properties中读取data前缀的预制数据)
│ │ │ ├── UrlConfig.java(接口路径类配置实体类,自动从properties中读取url前缀的接口路径)
│ │ ├── control(维护对外提供调用功能的api接口)
│ │ ├── model(维护一些参数对象模型)
│ │ ├── untils(维护工具类)
│ │ │ ├── XXX……(各类测试常用工具类)
│ ├── src/main/resources
│ │ ├── excelFiles(存放Excel数据文件)
│ │ ├── jsonSchema(存放jsonSchema)
│ │ ├── mapper(存放sqlMapper文件)
│ │ ├── static(存放一些静态资源)
│ │ ├── templates(存放一些模板)
│ │ ├── application.properties(属性配置文件)
│ │ ├── application-prod.properties(用于生产环境的属性配置文件)
│ │ ├── application-test.properties(用于测试环境的属性配置文件)
│ │ ├── databaseConfig.xml(数据库连接配置)
│ │ ├── log4j2.xml(日志配置)
│ │ ├── testng.xml(TestNG执行配置)
│ ├── test-output(存储测试产出,如:报告、覆盖率文件)
建立Template Group,例如起名:JGGroup
新建Live Template,设置Abbreviation:caseFalseByCode,设置Description:生成基于code验证码的失败用例通用模块模板
public boolean caseFalseCode(,Integer errCode){
Map<String, String> headers=new HashMap<String, String>();
headers.putAll(getTestData.getLoginCookies());
// headers.put("Accept", "application/json, text/plain, */*");
// get方式
Map<String, String> param=new HashMap<String, String>();
param.put("",);
httpresult=JGHttp.doGet(urlConfig. + urlConfig.,param,headers);
// post方式
// String param = String.format("", );
// httpresult = JGHttp.doPostJson(urlConfig. + urlConfig., param, headers);
// 通过响应码校验结果
if(httpresult.getResponseCode() != 200){
return false;
}
// 通过响应体中的code码来判断结果
if(JSONObject.parseObject(httpresult.getResponseBody()).getInteger("code") != errCode){
return false;
}
return true;
}
新建Live Template,设置caseFalseModuleByMsg,设置Description:生成基于错误信息msg验证的失败用例通用模块模板
public boolean caseFalseMsg(,String msgerr) {
Map<String, String> headers = new HashMap<String, String>();
headers.putAll(getTestData.getLoginCookies());
// headers.put("Accept", "application/json, text/plain, */*");
// get方式
Map<String, String> param = new HashMap<String, String>();
param.put("", );
httpresult = JGHttp.doGet(urlConfig.apiDesignerUrl + urlConfig., param, headers);
// post方式
// String param = String.format("", );
// httpresult = JGHttp.doPostJson(urlConfig.apiDesignerUrl +, param, headers);
// 通过响应码校验结果
if (httpresult.getResponseCode() != 200) {
return false;
}
// 通过响应体中的message是否出现预期错误信息来判断结果
if (!JSONObject.parseObject(httpresult.getResponseBody()).getString("message").equals(msgerr)) {
return false;
}
// 模糊匹配关键字来判断结果
// if(!JGString.matchString(".*" + msgerr + ".*",httpresult.getResponseBody())){
// return false;
// }
return true;
}
新建Live Template,设置Abbreviation:caseTrueModule,设置Description:生成正例通用模块模板 public JGHttpResult httpresult;
// 存储请求接口返回的响应头和消息体
public JGHttpResult httpresult;
public boolean caseTrue(,){
Map<String, String> headers=new HashMap<String, String>();
// 设置cookie
headers.putAll(getTestData.getLoginCookies());
// headers.put("Accept", "application/json, text/plain, */*");
// get方式
Map<String, String> param=new HashMap<String, String>();
param.put("",);
httpresult=JGHttp.doGet(urlConfig.apiDesignerUrl+urlConfig.,param,headers);
// post方式
// String param = String.format("", );
// httpresult = JGHttp.doPostJson(urlConfig.apiDesignerUrl +, param, headers);
// 通过响应码校验结果
if(httpresult.getResponseCode()!=200){
return false;
}
// 通过响应体中的code码来判断结果
if(JSONObject.parseObject(httpresult.getResponseBody()).getInteger("code")!=20000000){
return false;
}
// 通过响应体中的message来判断结果
if(!JSONObject.parseObject(httpresult.getResponseBody()).getString("message").equals("成功")){
return false;
}
// 模糊匹配关键字来判断结果
// if(!JGString.matchString("",httpresult.getResponseBody())){
// return false;
// }
// 通过JsonSchema校验接口数据返回格式
// if (!JGJsonSchemaByFge.AssertJsonSchemaByJsonStringAndJsonFile(httpresult.getResponseBody(),"src/main/resources/jsonSchema/.json"))
// {
// return false;
// }
return true;
}
新建Live Template,设置Abbreviation:testCase,设置Description:生成基于DDT的Case模板
@DataProvider
public Object[][] dataT$num$(){
int row = 1;
int col = 1;
Object[][] objects = new Object[row][col];
// 数据处理如下
objects[0][0] = "正例:";
objects[0][1] = ;
objects[0][2] = ;
objects[0][3] = ;
objects[0][4] = ;
objects[0][5] = ;
objects[0][6] = ;
objects[0][7] = ;
// for (int i = 0;i < row;i++){
//
// }
return objects;
}
@Test(dataProvider = "dataT$num$",priority = $num$,groups = {"default"})
public void testT$num$(String title,,) {
// 打印用例标题
log.info(title);
// 中间处理
// 断言
Assert.assertTrue(caseTrue(,));
}
新建Live Template,设置Abbreviation:classComment,设置Description:生成类注释
**
* @description: $description$
* @author: $user$
* @date: $date$-$time$
**/
新建Live Template,设置Abbreviation:methodComment,设置Description:生成方法的注释
**
* @description: $description$
* @param: $params$
* @return: $returns$
* @author: $user$
* @date: $date$-$time$
**/
设置Name:JG-CaseClass,Extension:java
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
#parse("File Header.java")
import com.alibaba.fastjson.JSONObject;
import com.jingang.springinterfacetest.cases.TestNGSpringBootCaseConfig;
import com.jingang.springinterfacetest.utils.JGHttp;
import com.jingang.springinterfacetest.utils.JGHttpResult;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.HashMap;
import java.util.Map;
/**
* @program: ${PROJECT_NAME}
* @description: ${description}
* @author: ${USER}
* @create: ${YEAR}-${MONTH}-${DAY} ${HOUR}:${MINUTE}
**/
@Slf4j
@Component
@Data
public class ${NAME} extends TestNGSpringBootCaseConfig {
public JGHttpResult httpresult;
public boolean caseTrue(String cookie,) {
Map<String, String> headers = new HashMap<String, String>();
// headers.putAll(getTestData.getLoginCookies());
// headers.put("Accept", "application/json, text/plain, */*");
// 南京控制台需要使用的header
headers.put("Accept", "application/json, text/plain, */*");
headers.put("X-Requested-With", "XMLHttpRequest");
headers.put("Content-Type", "application/json;charset=UTF-8");
headers.put("Cookie", cookie);
headers.put("User-Agent", testDataConfig.userAgent);
// get方式
Map<String, String> param = new HashMap<String, String>();
param.put("", );
httpresult = JGHttp.doGet(urlConfig. + urlConfig., param, headers);
// post方式
// String param = String.format("", );
// httpresult = JGHttp.doPostJson(urlConfig. + urlConfig. , param, headers);
// 【断言方式】通过响应码校验结果
if (httpresult.getResponseCode() != 200) {
return false;
}
// 【断言方式】通过响应体中的code码来判断结果
if (JSONObject.parseObject(httpresult.getResponseBody()).getInteger("code") != 20000000) {
return false;
}
// 通过响应体中的code码来判断结果--南京控制台
if (JSONObject.parseObject(httpresult.getResponseBody()).getInteger("resultCode") != 0) {
return false;
}
// 通过响应体中的message来判断结果--南京控制台
// if (!JSONObject.parseObject(httpresult.getResponseBody()).getString("resultMsg").equals("成功")) {
// return false;
// }
// 【断言方式】通过响应体中的message来判断结果
if (!JSONObject.parseObject(httpresult.getResponseBody()).getString("message").equals("成功")) {
return false;
}
// 【断言方式】模糊正则匹配关键字来判断结果
// if(!JGString.matchString("",httpresult.getResponseBody())){
// return false;
// }
// 【断言方式】通过JsonSchema校验接口响应报文格式
// if (!JGJsonSchemaByFge.AssertJsonSchemaByJsonStringAndJsonFile(httpresult.getResponseBody(),"src/main/resources/jsonSchema/.json"))
// {
// return false;
// }
// 【断言方式】通过查询数据库匹配核对检查数据
// 【断言方式】通过相关接口辅助验证
return true;
}
/**
* 前置步骤:维护本类下所有用例执行前需要准备的前置条件
**/
@BeforeClass
public void beforeClass(){
log.info("提示:在该类(${PACKAGE_NAME}.${NAME})下所有用例执行前前执行");
// 前置条件准备
}
/**
* 用例维护步骤:基于DDT思路,维护该接口的用例case
**/
/**
* 【测试场景P1】:基础检查(随机一组正确入参,检查接口连通性、是否返回预期关键字段)
**/
@DataProvider
public Object[][] dataT1(){
int row = 1;
int col = 3;
Object[][] objects = new Object[row][col];
// 数据处理如下
objects[0][0] = "正例:${description}基础检查";
objects[0][1] = ;
objects[0][2] = ;
return objects;
}
@Test(dataProvider = "dataT1",priority = 1,groups = {"default"})
public void testT(String title,,) {
// 打印用例标题
log.info(title);
// 中间处理
// 断言
Assert.assertTrue(caseTrue(,));
}
/**
* 【测试场景P2】:正常多角度(各种业务角度正常场景的参数组合)
**/
/**
* 【测试场景P3】:异常多角度(各种业务角度异常场景的参数组合)
**/
/**
* 【测试场景P4】:必输项检查(入参必输项场景组合检查)
**/
/**
* 【测试场景P5】:边界值检查(入参进行特殊字符、字符串长度边界、数字大小边界等场景检查)
**/
/**
* 后置步骤:维护本类下所有用例执行后需要进行的后置处理
**/
@AfterClass
public void afterClass(){
log.info("提示:在该类(${PACKAGE_NAME}.${NAME})下所有用例执行完毕后执行");
// 后置处理
}
}
设置Name:JG-BusinessFlowCaseClass,Extension:java
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
#parse("File Header.java")
import com.jingang.springinterfacetest.cases.TestNGSpringBootCaseConfig;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
* @program: ${PROJECT_NAME}
* @description: ${PACKAGE_NAME}模块下的业务流用例
* @author: ${USER}
* @create: ${YEAR}-${MONTH}-${DAY} ${HOUR}:${MINUTE}
**/
@Slf4j
@Data
public class ABusinessFlow extends TestNGSpringBootCaseConfig {
/** 业务流用例的指导原则:
* 1)测试单独某个接口的场景,前置依赖较多的相关接口,可将多个接口串成业务流用例
* 2)接口所测业务场景的业务流属性较强,适合多个接口串在一起测试
* 3)原则上业务流用例不要跨模块,需要依赖其他模块接口的,通过维护前置条件方法来准备
* 4)其他情况不太适合一个一个接口独立测试的case,可维护成业务流用例
*
**/
/**
* 步骤1:统一维护自动装配用到的接口对象
**/
// eg: 业务用例需要使用ApiList接口
// @Autowired
// public ApiList apiList;
/**
* 步骤2:维护本类下所有业务流用例执行前需要准备的前置条件
**/
// @BeforeClass
// public void beforeClass() {
// log.info("提示:在该类(${PACKAGE_NAME})下所有用例执行前前执行");
// // 前置条件准备
//
// }
/**
* 步骤3:准备某业务流用例需要的测试数据
**/
// @DataProvider
// public Object[][] dataT1() {
// int row = 1;
// int col = 1;
// Object[][] objects = new Object[row][col];
// // 数据处理如下
// objects[0][0] = "正例:";
// objects[0][1] =;
// objects[0][2] =;
// objects[0][3] =;
// objects[0][4] =;
// objects[0][5] =;
// objects[0][6] =;
// objects[0][7] =;
// // for (int i = 0;i < row;i++){
// //
// // }
// return objects;
// }
/**
* 步骤4:维护业务流用例case模块
**/
// @Test(dataProvider = "dataT1", priority = 1, groups = {"default"})
// public void testT1(String title,,) {
// // 打印用例标题
// log.info(title);
// // 业务流处理
//
//
// // 断言
// Assert.assertTrue();
// }
/**
* 步骤5:维护本类下所有业务流用例执行后需要进行的后置处理
**/
// @AfterClass
// public void afterClass() {
// log.info("提示:在该类(${PACKAGE_NAME})下所有用例执行完毕后执行");
// // 后置处理
//
// }
}
需要安装如下插件:
1)GsonFormat
快捷键:alt+s
替代方案:https://www.bejson.com/json2javapojo/new/
2)Lombok
例如针对如下被测接口:
在properties中,变量名要从接口路径中摘取较唯一的名字来命名:
建议命名为:
url.projectMemberList
不建议命名为:url.list,因为容易重复不唯一,所以取路径的最后两节来组成变量名。
**命名规则:**路径最后1个或2个节点名拼接到一起构成属性名称(确保属性名称全局唯一)
在cases包下,新建接口用例类名时,要保持和前面接口路径变量名一致。
建议命名为:ProjectMemberList.java(注意首字母大写)
**命名规则:**接口用例类的名称要和接口路径的属性名称一致,但首字母要大写
如果需要定义接口入参的JavaBean实体类
建议命名为:ProjectMemberListParam.java(后面加上Param)
**命名规则:**接口入参的JavaBean类名为对应的接口用例类名+Param组成
1)建议每完成一个接口的用例,就push到远程代码仓库,要求每天下班前必须把当天编写的代码push到远程仓库。
2)每次Push前一定要先pull一下,更新一下本地代码,并确保工程可编译执行成功才可以上传(执行一个test方法确保可以编译执行成功即可)。
3)commit代码时,一定要写上准确的message,描述本次提交的内容
功能 | 功能介绍 | 演示demo | 备注 |
---|---|---|---|
动态数据用例模板 | 该模板支持动态测试数据的用例维护 | com.jingang.springinterfacetest.cases.demoCaseModule.ActiveDataCase | 无 |
jsonSchema功能 | jsonSchema功能用于接口格式的详细验证 | com.jingang.springinterfacetest.cases.demoCaseModule.JsonSchemaDemo | |
企业微信通知功能 | 通过监听testNG的执行,对失败的用例发送企业微信通知 | com.jingang.springinterfacetest.cases.demoCaseModule.NotificationCaseDemo |
- 通过对testNG添加监听器(com.jingang.springinterfacetest.config.JGTestNgListener),当用例执行失败时,触发企业微信通知。
- 在properties中设置开关:notificationSwitch=on,当设为on时才会发送通知,否则并不发送。
- 使用前需要设置类(com.jingang.springinterfacetest.utils.Notification)中常量webHookUrl,填写企业微信的webhook的url地址。
- 该功能涉及的类如下:com.jingang.springinterfacetest.utils.Notification、com.jingang.springinterfacetest.utils.ConfigFile、com.jingang.springinterfacetest.config.TestConfig、com.jingang.springinterfacetest.config.JGTestNgListener、com.jingang.springinterfacetest.common.SendNotification
//json字符串转java简单对象
public static void jsonStrToJavaBean() {
String s ="{\"action\":\"add\",\"id\":\"1\",\"ordinal\":8,\"organUnitFullName\":\"testJSON\",\"parent\":\"0\",\"suborderNo\":\"58961\"}";
Data data = JSON.parseObject(s, Data.class);
System.out.println("data对象"+data.toString());
System.out.println("action="+data.getAction()+"---id="+data.getId());
//data对象Data{id='1', suborderNo='58961', organUnitType='null', action='add', parent='0', organUnitFullName='testJSON', ordinal=8}
//action=add---id=1
/**
* 另一种方式转对象
*/
Data dd = JSON.parseObject(s, new TypeReference<Data>() {});
System.out.println("另一种方式获取data对象"+dd.toString());
System.out.println("另一种方式获取="+dd.getAction()+"---id="+dd.getId());
//另一种方式获取data对象Data{id='1', suborderNo='58961', organUnitType='null', action='add', parent='0', organUnitFullName='testJSON', ordinal=8}
//另一种方式获取=add---id=1
}