自动化测试中,经常需要构造请求参数,例如JSON格式的参数,简单的好说,可以手工修改或是用 Postman、Jmeter 等工具结合简单的代码进行处理, 但当数据传输对象(DTO)很复杂,部分字段依赖性很强、强校验时,就不能单靠手工修改来满足自动化测试需求了,这时候就需要利用代码来解决了,本文给大家分享一种构造测试数据的方法 ====> 通过枚举、1:1 模仿前端请求逻辑接口以及程序解析数据来构造复杂 JSON。
背景
本次示例接口为业务开发中常见的新增保存接口,参数相对比较简单,为方便理解,我这里画个草图讲下大概的业务逻辑:
总体,业务逻辑 为创建采购计划选择不同的采购方式 并发起采购过程,采购过程由节点 1线性流程一直操作到节点 N。其中需特殊说明的:
采购方式、采购类别、议标类型、采购过程节点(以下简称:节点)均具备各自的相关属性(面向对象),属于配置数据(其他业务模块已配置),且具备关联关系:采购类别关联多个采购方式;采购方式关联多个采购节点。
整体后端环境为微服务,服务交互采用 Feign 调用,供应商类别取自其他服务(供应商),项目信息、人员信息取自外部服务(用户中心、基础平台)
整体分析过程重点在分析构造数据,忽略涉及的权限、参数配置、各节点 OA 回调处理等问题,以及具体信息已脱敏。
开发语言采用Java
整体思路
一、 分析接口(业务逻辑)
首先,根据编写功能测试用例时,对业务逻辑的理解,以及后端提供的接口文档来分析
接口文档
接口名称:新增计划 (草稿)
请求方式:POST
接口路径:/***/purchasePlan/save
请求参数:
Headers:
参数含义 | 参数值 | 是否必须 |
---|---|---|
Content-Type | application/json | 是 |
Authorization | Bearer ${token} | 是 |
Body:
{
"id":"",//id新增不传,编辑传
"planName":"测试计划",//计划名称
"projectId":159211111111111193569,//项目id
"projectName":"云贵川渝片区",//项目名称
"estimatedAmount":2530000000.99,//预计签约金额
"purchaseCategoryId":"1599605780716195842",//采购类别id
"purchaseCategoryName":"采购类别111",//采购类别name
"purchaseMethodCode":"inviteBid",//采购方式code
"purchaseMethodName":"邀请招标", //采购方式name
"providerCategoryId":"159706qqq2812802",//供应商类别
"providerCategoryName":"建筑方案设计",//供应商类别名称
"recordTime":"2022-12-01 19:00:15",//入场时间
"planStartTime":"2022-11-25 12:00:15",//计划开始时间
"planFinishTime":"2023-03-25 13:00:15",//计划完成时间
"bidTypeName":"议标类型1",//议标name
"bidTypeValue":"bidType01",//议标值
"chargeUserId":14341111111111111681,//招标经办人id
"chargeUserCode":"ATE002",//招标经办人code
"chargeUserName":"NHATE-员工B",//招标经办人name
"temporaryPlan":true,//否临时采购计划,1是0否
"supplementBid":true,//是否补标,1是0否
"ifStrategic":true,//用于战采协议
"if2n":true,//是否启用2n+1控制
"ifSignMoneyControl":false,//是否启用预计签约金额控制
"ifEvaluationStaffOdd":false,//技术标评标人员是否奇数
"ifEvaluationStaffGteThree":false,//技术标评标人员是否大于等于3
"ifControlPriceFloor":true,//是否控制价下限控制
"nodes":[//节点list
{
"id":"",//id新增不传,编辑传
"name":"招标策划",//节点名称
"planId":"",//计划id
"purchaseMethodCode":"inviteBid",//采购关联方式编码
"purchaseMethodNodeCode":"zbch",//采购关联方式节点编码
"startTime":"2022-12-01 19:00:15",//计划开始时间
"finishTime":"2022-12-30 19:00:15",//计划完成时间
"realFinishTime":"",//实际完成时间
"chargeUserId":1434111111111150722,//责任人id
"chargeUserCode":"ATE001",//责任usercode
"chargeUserName":"NHATE-员工A",//责任人name
"chargeOrg":"责任部门1",//责任部门
"chargeOrgValue":"responsibleDept02",//部门code
"ifNecessity":true,//是否必须
"sort":1//排序
}
]}
返回数据
{
"body":"1599941367004532737",
"code":"0000",
"message":"操作成功",
"status":true}
分析方法
1.分析参数结构,嵌套层级关系
首先,请求参数 json 外层是 采购计划对象相关属性,都是一对一(key-value)的关系
其次,采购过程节点(nodes)字段 的数据类型是列表(List<采购过程节点对象>),即该字段会存在多个,根据业务理解,节点数量=采购方式下配置的节点数量,且每个节点都有不同的节点名称定义(枚举、数据库配置)
2.找出哪些字段不具备依赖性,该部分字段可以通过 随机、写死、自定义方式设置 value,该部分字段处理较简单
计划名称、预计签约金额、各种时间、否临时采购计划、是否补标 字段不具备强校验性
,该部分字段优先采用调用接口形式取值,如果处理起来麻烦,可采用枚举形式,在限定范围内取值
项目 id、name,供应商类别 id、name 取自不同外部服务(项目权限、供应商库),影响后续业务逻辑。
采购类别 id、name,采购方式 id、name 取自配置,具备关联性,影响后续业务逻辑
议标类型 name、value 取自字典项配置,具备弱关联性,影响较小,但为了数据合法性,仍需要取真实有效的配置
是否用于战采协议、是否启用 2n+1 控制、是否启用预计签约金额控制、技术标评标人员是否奇数、技术标评标人员是否大于等于 3、是否控制价下限控制 取自采购方式中配置的自有属性,影响后续业务逻辑(存在冗余字段),为数据合法性,需取真实配置有效的配置
招标经办人、各节点责任人 涉及 userId、userCode、userName 取自用户中心账号体系(已提前配置测试账号)
各节点中 节点名称、采购关联方式编码、采购关联方式节点编码、责任部门 code、name、是否必须、序号 均取自节点配置数据,影响后续业务逻辑,需取真实有效配置
二、 编码过程
01 首先创建数据DTO对象类
或者直接复制后端代码文件,这属于Java 基础 - 面向对象,本文就不细介绍,直接贴代码:
PurchasePlanDto 采购计划对象:
@Datapublic class PurchasePlanDto implements Serializable {
private Long id;
@NotNull(message = "计划名称不能为空")
private String planName;
@NotNull(message = "项目不能为空")
private Long projectId;
private String projectName;
/**
* 预计签约金额,元
*/
@NotNull(message = "预计签约金额不能为空")
@DecimalMax(value = "99999999999999.99", message = "预计签约金额超限")
private BigDecimal estimatedAmount;
/**
* 采购类别id
*/
@NotNull(message = "采购类别不能为空")
private Long purchaseCategoryId;
/**
* 采购类别名称
*/
private String purchaseCategoryName;
/**
* 采购方式编码
*/
@NotBlank(message = "采购方式不能为空")
private String purchaseMethodCode;
/**
* 采购方式名称
*/
private String purchaseMethodName;
/**
* 供应商类别编码
*/
@NotBlank(message = "供应商类别不能为空")
private String providerCategoryId;
/**
* 供应商类别名称
*/
private String providerCategoryName;
/**
* 入场时间
*/
private Date recordTime;
/**
* 计划开始时间
*/
@NotNull(message = "计划开始时间不能为空")
private Date planStartTime;
/**
* 计划完成时间
*/
@NotNull(message = "计划完成时间不能为空")
private Date planFinishTime;
/**
* 经办人id
*/
@NotNull(message = "经办人不能为空")
private Long chargeUserId;
/**
* 经办人账号
*/
private String chargeUserCode;
/**
* 经办人姓名
*/
private String chargeUserName;
/**
* 议标名称
*/
private String bidTypeName;
/**
* 议标值
*/
private String bidTypeValue;
/**
* 是否临时采购计划,1是0否
*/
private Boolean temporaryPlan;
/**
* 是否补标,1是0否
*/
private Boolean supplementBid;
/**
* 用于战采协议
*/
private Boolean ifStrategic;
/**
* 是否启用2n+1控制
*/
private Boolean ifTn;
/**
* 是否启用预计签约金额控制
*/
private Boolean ifSignMoneyControl;
/**
* 技术标评标人员是否奇数
*/
private Boolean ifEvaluationStaffOdd;
/**
* 技术标评标人员是否大于等于3
*/
private Boolean ifEvaluationStaffGteThree;
/**
* 是否控制价下限控制
*/
private Boolean ifControlPriceFloor;
/**
* 节点list
*/
@Valid
@NotEmpty(message = "节点不能为空")
private List<PlanNodeDto> nodes;}
PlanNodeDto 采购过程节点对象:
@Datapublic class PlanNodeDto implements Serializable {
private Long id;
/**
* 名称
*/
private String name;
/**
* 计划id
*/
private Long planId;
/**
* 采购关联方式编码
*/
private String purchaseMethodCode;
/**
* 采购关联方式节点编码
*/
private String purchaseMethodNodeCode;
/**
* 计划开始时间
*/
@NotNull(message = "计划开始日期不能为空")
private Date startTime;
/**
* 计划完成时间
*/
@NotNull(message = "计划完成日期不能为空")
private Date finishTime;
/**
* 实际完成时间
*/
private Date realFinishTime;
/**
* 负责人id
*/
@NotNull(message = "责任人不能为空")
private Long chargeUserId;
/**
* 负责人账号
*/
private String chargeUserCode;
/**
* 负责人姓名
*/
private String chargeUserName;
/**
* 责任部门
*/
private String chargeOrg;
/**
* 责任部门值
*/
private String chargeOrgValue;
/**
* 是否必要节点
*/
private Boolean ifNecessity;
/**
* 排序
*/
private Integer sort;}
接着,搭建逻辑框架,工作量梳理后按 TODO 分解清晰
public class PlanTest extends TestBase {
private static final ReportLog reportLog = new ReportLog(PlanTest.class);
//获取系统token 封装成请求头
public Map<String,String> header = getBackTokenHeader("ATE***","****");
@Test(description = "TestNG 测试- 提交保存采购计划")
void testAddPlan() {
//创建入参 采购计划DTO对象
PurchasePlanDto planDto = buildPlanDto();
//提交保存计划接口
String rs = HttpUtils.doPost(HostLH.TEST_HOST.concat(ApiBid.PLAN_SAVE), header, JSONObject.toJSONString(planDto));
//输出响应结果日志信息
reportLog.info(" PLAN_SAVE ====> {}",JSONObject.parseObject(rs));
//省略。。。后续验证逻辑 或 其他目的性测试
}
//创建采购计划DTO PurchasePlanDto
PurchasePlanDto buildPlanDto() {
PurchasePlanDto planDto = new PurchasePlanDto();
//TODO 设置 不具备依赖性的字段值
//TODO 设置项目id、name
//TODO 设置供应商类别id、name
//TODO 设置采购类别id、name,采购方式id、name 及 是否用于战采协议、是否启用2n+1控制、是否启用预计签约金额控制、技术标评标人员是否奇数、技术标评标人员是否大于等于3、是否控制价下限控制
//TODO 设置招标经办人
//采购节点List
planDto.setNodes(buildPlanNodeDtoList());
return planDto;
}
// 创建采购计划(过程)节点DTO List<PlanNodeDto>
List<PlanNodeDto> buildPlanNodeDtoList() {
List<PlanNodeDto> nodeDtoList = new ArrayList<>();
//TODO 循环遍历 设置 各节点属性值-节点名称、采购关联方式编码、采购关联方式节点编码、责任部门code、name、是否必须、序号
//TODO 循环遍历 设置 各节点责任人
return nodeDtoList;
}}
02 编写创建采购计划(过程)节点DTO逻辑
怎么开始呢? ==> 这里作者 建议从里层向外层,先简单后复杂进行编写 即优先编写buildPlanNodeDtoList()方法
我们分析下,既然节点有多个,我们方法里为了优雅且快捷,肯定采用循环的办法去设置,但是这个循环次数(节点数量)方法内部是没法感知的,即要么设置全局变量(也要定义循环次数),要么由入参告诉我们(作者采用)。那什么样的入参能告诉我们呢,通过业务理解知道,不同的采购方式,对应设置了具体的采购节点,那么我们可以和前端逻辑一致,页面会先选取采购方式,即拿到了采购方式的编码,那我们用这个编码去调用接口获取采购方式详情信息 - 并获取其关联的节点信息,方法扩展入参为buildPlanNodeDtoList(String purchaseMethod)。
先用 postman 或其他工具调用 API:根据采购方式编码获取采购方式详细信息,查看响应 body 数据结构,分析数据层级结构、哪些值是我们需要的、以及如何取目标值(后续遇到均采用这种模式去分析):
{
"body":{
"code":"inviteBid",
"createDate":null,
"createUser":"",
"delFlag":false,
"id":1,
"ifControlPriceFloor":true,
"ifEvaluationStaffGteThree":true,
"ifEvaluationStaffOdd":true,
"ifSignMoneyControl":true,
"ifStrategic":true,
"ifTn":true,
"name":"邀请招标",
"purchaseMethodNodes":[
{
"code":"zbch",
"createDate":null,
"createUser":"",
"delFlag":false,
"id":1,
"ifHide":false,
"ifNecessity":true,
"ifSyncAgent":true,
"name":"招标策划",
"purchaseMethodCode":"inviteBid",
"purchaseMethodId":1,
"responsibleDept":"responsibleDept01",
"responsibleDeptName":"责任部门_默认",
"sort":1,
"updateDate":null,
"updateUser":""
}
//...此处省略多个节点
],
"remark":"说明",
"status":true,
"updateDate":"2022-12-05 14:36:17",
"updateUser":"ATE001"
},
"code":"0000",
"message":"操作成功",
"status":true}
我们发现 返回的节点purchaseMethodNodes中有我们需要的所有信息,以及外层还有采购方式的 Boolean 值涉及的字段值,即我们的目标就是去解析这个 json 循环遍历拿到对应的节点各属性值
即:
//创建采购计划(过程)节点DTO List<PlanNodeDto>
List<PlanNodeDto> buildPlanNodeDtoList(String purchaseMethod) {
//调用接口(API功能-根据采购方式编码获取详情信息-含关联的采购节点信息)
String apiUrl = String.format(HostLH.TEST_HOST.concat(ApiBid.PURCHASE_METHOD_DETAIL), purchaseMethod);
String rs = HttpUtils.doGet(apiUrl, header);
//解析响应json拿到所有的节点List
List<JSONObject> purchaseMethodNodes = JSON.parseObject(rs).getJSONObject("body").getJSONArray("purchaseMethodNodes").toJavaList(JSONObject.class);
//基本逻辑:完成时间必须 > 开始时间, 后节点开始时间需 > 前节点完成时间 这里使用原子类来控制每次加30天
DateTime now = DateUtil.date();
AtomicInteger loopInt = new AtomicInteger(1);
List<PlanNodeDto> nodeDtoList = new ArrayList<>(purchaseMethodNodes.size());
purchaseMethodNodes.forEach(node -> {
PlanNodeDto nodeDto = new PlanNodeDto();
//取json中各字段值,因大部分字段名存在差异(各接口定义、业务出发点不一样,属正常现象),单独每个字段设置值
nodeDto.setName(node.getString("name")); //节点名称
nodeDto.setPurchaseMethodCode(node.getString("purchaseMethodCode")); //关联采购方式编码
nodeDto.setPurchaseMethodNodeCode(node.getString("code")); //关联采购方式节点编码
//责任部门
nodeDto.setChargeOrg(node.getString("responsibleDept"));
nodeDto.setChargeOrgValue(node.getString("responsibleDeptName"));
nodeDto.setIfNecessity(node.getBoolean("ifNecessity")); //是否必要节点
nodeDto.setSort(node.getInteger("sort")); //排序
nodeDto.setStartTime(DateUtil.offsetDay(now,loopInt.getAndAdd(30))); //节点计划开始时间
nodeDto.setFinishTime(DateUtil.offsetDay(now,loopInt.getAndAdd(30))); //节点计划完成时间
//TODO 循环遍历 设置 各节点责任人
nodeDtoList.add(nodeDto);
});
return nodeDtoList;
}
接下来,需要设置节点责任人了。 采用策略:每个节点独立设置一个账号,与真实业务场景保持一致,每个测试账号编码与节点编码保持同步,进行拼接(ATE 拼接编码)PS:ATE — Auto Test Engineer 首字母
出于自动化需要,测试用户账号已缩小范围,我们不需要从数据库中取(本文均未连接数据库,后续章节会举例 连接库来辅助测试),即采用枚举方式将账号定义写死,后续业务测试需要账号则从该枚举中取
测试账号枚举类 TestUserEnum
@Getter@AllArgsConstructorpublic enum TestUserEnum {
ATEzbch("XXXX19962050001","ATEzbch","T-****","13655666001","1","zbch"),
ATEct("XXXX19962050002","ATEct","T-****","13655666002","1","chut"),
ATEjswjsb("XXXX19962050003","ATEjswjsb","T-****","13655666003","1","jswjsb"),
ATEjswjxg("XXXX19912050001","ATEjswjxg","T-****","13655616665","1","jswjxg"),
ATEjxwjzj("XXXX19922050001","ATEjxwjzj","T-****","13655626665","1","jswjzj"),
ATEcqd("XXXX19962050004","ATEcqd","T-****","13655666004","1","cqd"),
ATEzbwj("XXXX19962050005","ATEzbwj","T-****","13655666005","1","zbwj"),
ATEdwrw("XXXX19962050006","ATEdwrw","T-****","13655666006","1","dwrw"),
ATEfb("XXXX19962050007","ATEfb","T-****","13655666007","1","fab"),
ATEdy("XXXX19962050008","ATEdy","T-****","13655666008","1","day"),
ATEhb("XXXX19962050009","ATEhb","T-****","13655666009","1","huib"),
ATEpjsb("XXXX19962050010","ATEpjsb","T-****","13655666010","1","pjsb"),
ATEpswb("XXXX19962050011","ATEpswb","T-****","13655666011","1","pswb"),
ATEelhb("XXXX19962050012","ATEelhb","T-****","13655666012","1","huib2"),
ATEelpb("XXXX19962050013","ATEelpb","T-****","13655666013","1","pswb2"),
ATEdb("XXXX19962050014","ATEdb","T-****","13655666014","1","dingb"),
ATEfqd("XXXX19962050015","ATEfqd","T-****","13655666015","1",""),
ATEqy("XXXX19962050016","ATEqy","T-****","13655666016","1","qiany"),
ATEhtjd("XXXX19962050017","ATEhtjd","T-****","13655666017","1",""),
ATEzbjbr("XXXX19932050001","ATEzbjbr","T-****","13655636665","2",""),
ATEswpbr1("XXXX19962150001","ATEswpbr1","T-****1","13655666018","3",""),
ATEswpbr2("XXXX19962150002","ATEswpbr2","T-****2","13655666019","3",""),
ATEswpbr3("XXXX19962150003","ATEswpbr3","T-****3","13655666020","3",""),
ATEswpbr4("XXXX19962150004","ATEswpbr4","T-****4","13655666021","3",""),
ATEswpbr5("XXXX19962150005","ATEswpbr5","T-****5","13655666022","3",""),
ATEswpbr6("XXXX19962150006","ATEswpbr6","T-****6","13655666023","3",""),
ATEswpbr7("XXXX19962150007","ATEswpbr7","T-****7","13655666024","3",""),
ATEswpbr8("XXXX19962150008","ATEswpbr8","T-****8","13655666025","3",""),
ATEswpbr9("XXXX19962150009","ATEswpbr9","T-****9","13655666026","3",""),
ATEjspbr1("XXXX19962250001","ATEjspbr1","T-****1","13655666027","3",""),
ATEjspbr2("XXXX19962250002","ATEjspbr2","T-****2","13655666028","3",""),
ATEjspbr3("XXXX19962250003","ATEjspbr3","T-****3","13655666029","3",""),
ATEjspbr4("XXXX19962250004","ATEjspbr4","T-****4","13655666030","3",""),
ATEjspbr5("XXXX19962250005","ATEjspbr5","T-****5","13655666031","3",""),
ATEjspbr6("XXXX19962250006","ATEjspbr6","T-****6","13655666032","3",""),
ATEjspbr7("XXXX19962250007","ATEjspbr7","T-****7","13655666033","3",""),
ATEjspbr8("XXXX19962250008","ATEjspbr8","T-****8","13655666034","3",""),
ATEjspbr9("XXXX19962250009","ATEjspbr9","T-****9","13655666035","3",""),
;
private String userId;
private String username;
private String realname;
private String phone;
// 1-采购过程节点经办人 2-计划经办人(采购过程经办人)(采购外层) 3-其他参与人
private String type;
private String bidNodeCode;}
账号源设置好了,为了方便,我们将所有测试账号转成List对象,并定义成全局变量,提供给后续使用:
//全局变量public List<BidUcUser> allTestUsers = Arrays.stream(TestUserEnum.values()).map(u -> {
BidUcUser bidUcUser = new BidUcUser();
bidUcUser.setType(u.getType());
bidUcUser.setBidNodeCode(u.getBidNodeCode());
bidUcUser.setUserId(Long.parseLong(u.getUserId()));
bidUcUser.setRealName(u.getRealname());
bidUcUser.setUserName(u.getUsername());
return bidUcUser;
}).collect(Collectors.toList());
这里涉及 User 对象,UcUser与BidUcUser,其中UcUser是示例系统用户中心用户对象(注意,这里对象是测试自己定义的,与后端的区分开,这里是测试需要的属性组成的对象),BidUcUser在UcUser的基础上扩展了本次测试需要的属性(节点 code-为了循环中与节点 code 进行识别绑定,type-测试定义的类型)Java 基础 - 继承父类属性扩展子类属性
接下来在循环中设置责任人:
//节点责任人(经办人)BidUcUser chargeUser = allTestUsers.stream()
.filter(u -> node.getString("code").equals(u.getBidNodeCode()) && "1".equals(u.getType()))
.findFirst().orElse(null);if (null == chargeUser) {
throw new BusinessException("测试账号节点code未匹配,请检查配置!");}nodeDto.setChargeUserId(chargeUser.getUserId());nodeDto.setChargeUserName(chargeUser.getRealName());nodeDto.setChargeUserCode(chargeUser.getUserName());
设置其他业务逻辑
if (isDeletedNodes) {
//删除不必要节点
purchaseMethodNodes = purchaseMethodNodes.stream().filter(json -> json.getBoolean("ifNecessity")).collect(Collectors.toList());}
03 编写创建采购计划 DTO 逻辑
根据先前的 TODO 任务,我们先易后难,一个一个拆解:
不影响主逻辑的边缘字段,采用随机处理,符合真实业务即可,甚至不传参也可以
//设置 不具备依赖性的字段值planDto.setEstimatedAmount(new BigDecimal(RandomUtil.randomDouble(99999999999999.99,2, RoundingMode.HALF_UP)).setScale(2,RoundingMode.HALF_UP)); //预计签约金额,元planDto.setRecordTime(RandomUtil.randomDay(-1000,0)); //入场时间planDto.setPlanStartTime(RandomUtil.randomDay(-100,0)); //计划开始时间planDto.setPlanFinishTime(RandomUtil.randomDay(500,1000)); //计划完成时间
经办人根据测试需要写死
//设置 招标经办人TestUserEnum chargeUser = TestUserEnum.ATEzbjbr;planDto.setChargeUserId(Long.parseLong(chargeUser.getUserId()));planDto.setChargeUserName(chargeUser.getRealname());planDto.setChargeUserCode(chargeUser.getUsername());
设置采购类别属性 id、name 产品原型页面操作逻辑先选类别、再选类别关联的采购方式,为了编码快捷,采用内部类 - 对象模式,创建采购类别对象、采购方式对象-----PS:测试创建的对象 非后端定义的对象
@Data@AllArgsConstructor@NoArgsConstructor@Builderpublic static class PurchaseCategory{
private String PurchaseCategoryName;
private String PurchaseCategoryId;}@Data@AllArgsConstructor@NoArgsConstructor@Builderpublic static class PurchaseMethod{
private String PurchaseMethodName;
private String PurchaseMethodCode;
private Boolean ifControlPriceFloor;
private Boolean ifEvaluationStaffGteThree;
private Boolean ifEvaluationStaffOdd;
private Boolean ifSignMoneyControl;
private Boolean ifStrategic;
private Boolean ifTn;}
考虑到可以根据需要定义目标采购类别,即计划创建方法buildPlanDto()中增加采购类别入参 >> buildPlanDto(String purchaseCategoryName)
//采购类别//调用API-采购类别列表 查询启用状态的采购类别ListString rs = HttpUtils.doGet(HostLH.TEST_HOST.concat(ApiBid.PURCHASE_CATEGORY_LIST), header, "status=true");List<JSONObject> categoryList = JSON.parseObject(rs).getJSONArray("body").toJavaList(JSONObject.class);PlanTest.PurchaseCategory targetCategory = null;//获取目标采购类别 若指定,则匹配if (ObjectUtil.isNotEmpty(purchaseCategoryName)) {
targetCategory = categoryList.stream().filter(cate -> purchaseCategoryName.equals(cate.getString("name")))
.map(cate -> {
return PurchaseCategory.builder()
.purchaseCategoryName(cate.getString("name"))
.purchaseCategoryId(cate.getString("id"))
.build();
}).findAny().orElse(null);}//若匹配不到或不指定,则写死类别(采购类别-所有采购方式)if (null == targetCategory) {
targetCategory = PurchaseCategory.builder().purchaseCategoryId("1600406567327461378").purchaseCategoryName("采购类别-所有采购方式").build();}planDto.setPurchaseCategoryName(targetCategory.getPurchaseCategoryName());planDto.setPurchaseCategoryId(Long.parseLong(targetCategory.getPurchaseCategoryId()));
设置采购方式属性 与类别相似,接收调用者入参,提供匹配 方法改为buildPlanDto(String purchaseCategoryName,String purchaseMethodName)
//采购方式//调用API-根据ID获取采购方式详情 ID来自类别targetCategoryrs = HttpUtils.doGet(HostLH.TEST_HOST.concat(String.format(ApiBid.PURCHASE_CATEGORY_METHOD, targetCategory.getPurchaseCategoryId())), header);List<JSONObject> methodList = JSON.parseObject(rs).getJSONArray("body").toJavaList(JSONObject.class);PlanTest.PurchaseMethod targetMethod = null;//获取采购类别关联的 采购方式 若指定,则匹配if (ObjectUtil.isNotEmpty(purchaseMethodName)) {
targetMethod = methodList.stream().filter(method -> purchaseMethodName.equals(method.getString("name")))
.map(method -> {
return PlanTest.PurchaseMethod.builder()
.purchaseMethodCode(method.getString("code"))
.purchaseMethodName(method.getString("name"))
.ifControlPriceFloor(method.getBoolean("ifControlPriceFloor"))
.ifEvaluationStaffGteThree(method.getBoolean("ifEvaluationStaffGteThree"))
.ifEvaluationStaffOdd(method.getBoolean("ifEvaluationStaffOdd"))
.ifSignMoneyControl(method.getBoolean("ifSignMoneyControl"))
.ifStrategic(method.getBoolean("ifStrategic"))
.ifTn(method.getBoolean("ifTn"))
.build();
}).findAny().orElse(null);}//若匹配不到或不指定,则写死采购方式(邀请招标)if (null == targetMethod) {
targetMethod = PurchaseMethod.builder()
.purchaseMethodName("邀请招标")
.purchaseMethodCode("inviteBid")
.ifControlPriceFloor(true)
.ifEvaluationStaffGteThree(true)
.ifEvaluationStaffOdd(true)
.ifSignMoneyControl(true)
.ifStrategic(true)
.ifTn(true)
.build();}//浅拷贝 targetMethod所有属性到planDtoBeanUtil.copyProperties(targetMethod,planDto);
设置议标类型
//议标类型//调用API-字典项列表接口获取listrs = HttpUtils.doGet(HostLH.TEST_HOST.concat(ApiBid.BIDDICT_LIST), header, "type=bidType&status=true");List<JSONObject> bidDictList = JSONObject.parseObject(rs).getJSONArray("body").toJavaList(JSONObject.class);//优先取默认的JSONObject bidType = bidDictList.stream().filter(json -> json.getBoolean("ifDefault")).findAny().orElse(null);if (null == bidType) {
//若不存在默认的,则随机取
bidType = bidDictList.get(RandomUtil.randomInt(bidDictList.size()));}planDto.setBidTypeName(bidType.getString("name"));planDto.setBidTypeValue(bidType.getString("value"));
设置项目 id、name
//筛选项目分期String rs = HttpUtils.doGet(HostLH.TEST_HOST.concat(ApiBid.COMMON_GET_USER_TREE), header);reportLog.info("{}", JSONObject.parseObject(rs));
通过获取项目分期树状 JSON 发现,项目组织层级较深,最高达 8 层。目标 JSON 如图所示:
为符合真实场景,如果要取目标值(逻辑要求只取最末级,且只取末级的 type 为PROJECT与STAGE),这最好的办法是编写树的遍历算法,通过递归获取,且取出来后,需提供根据入参匹配的功能。考虑到目标字段较少,只有项目 id、name,取值方式多种多样,且正常业务测试,用不上这么多项目,如果通过递归取,从编码效率上低于连接数据库取值,本文采用最简单的办法,不用手动写死,仍然采用枚举类的方法,将备用测试的项目分期写死在代码中,这样编码更快捷。
项目分期枚举 ProjectEnum:
@user29@user30publicenumProjectEnum{
STAGE_001("1601124116981645313","猪产业-黑吉辽蒙法人公司0森林绿化项目2-无分期"),
STAGE_002("1601128411869249538","猪产业-黑吉辽蒙法人公司1飞机购买项目2一期"),
STAGE_003("1601128413421142017","猪产业-黑吉辽蒙法人公司1飞机购买项目2二期"),
STAGE_004("1601128127541575681","禽产业-山东法人公司0飞机购买项目3一期"),
STAGE_005("1601128296962097153","禽产业-山东法人公司1精装样板项目1一期"),
STAGE_006("1601128298522378242","禽产业-山东法人公司1精装样板项目1二期"),
STAGE_007("1601124250926743554","禽产业-山东法人公司1精装样板项目2-无分期"),
STAGE_008("1601128278775595009","禽产业-豫晋陕甘鄂法人公司0搅拌机项目1一期"),
STAGE_009("1601128283146059777","禽产业-豫晋陕甘鄂法人公司0搅拌机项目1三期"),
STAGE_010("1601128280331681794","禽产业-豫晋陕甘鄂法人公司0搅拌机项目1二期"),
STAGE_011("1601124234438934530","禽产业-黑吉辽蒙法人公司0仓库扩建项目2-无分期"),
STAGE_012("1601128212170047489","禽产业-黑吉辽蒙法人公司0仓库扩建项目3一期"),
STAGE_013("1601124237773406210","禽产业-黑吉辽蒙法人公司1森林绿化项目1-无分期"),
STAGE_014("1601124150825484289","食品-豫晋陕甘鄂法人公司1精装样板项目3-无分期"),
STAGE_015("1601124185164251137","食品产业-京津冀江苏法人公司0搅拌机项目1-无分期"),
STAGE_016("1601128230545293314","食品产业-京津冀江苏法人公司0搅拌机项目2一期"),
STAGE_017("1601128232042659842","食品产业-京津冀江苏法人公司0搅拌机项目2二期"),
STAGE_018("1601128143983247362","食品产业-黑吉辽蒙法人公司0仓库扩建项目1一期"),
STAGE_019("1601128152548016129","食品产业-黑吉辽蒙法人公司0仓库扩建项目2一期"),
STAGE_020("1601128349235707906","食品产业-黑吉辽蒙法人公司0仓库扩建项目3一期"),
STAGE_021("1601124208862068738","饲料产业-云贵川渝法人公司1森林绿化项目1-无分期"),
STAGE_022("1601128163788750849","饲料产业-云贵川渝法人公司1森林绿化项目2一期"),
STAGE_023("1601128166812844034","饲料产业-云贵川渝法人公司1森林绿化项目2三期"),
STAGE_024("1601128165298700289","饲料产业-云贵川渝法人公司1森林绿化项目2二期"),
STAGE_025("1601124215635869698","饲料产业-云贵川渝法人公司2森林绿化项目2-无分期"),
STAGE_026("1601128172168970241","饲料产业-云贵川渝法人公司3仓库扩建项目3一期"),
STAGE_027("1601128173741834241","饲料产业-云贵川渝法人公司3仓库扩建项目3二期"),
;
privateStringprojectId;
privateStringprojectName;}
也为方便测试,添加内部类 - 项目对象
@Data@AllArgsConstructor@NoArgsConstructor@Builderpublic static class Project{
private String projectId;
private String projectName;}
同时设置全局变量供后续使用
//全局变量public List<Project> allProject = Arrays.stream(ProjectEnum.values()).map(o -> {return Project.builder().projectId(o.getProjectId()).projectName(o.getProjectName()).build();}).collect(Collectors.toList());
//项目分期PlanTest.Project targetProject;if (ObjectUtil.isNotEmpty(projectName)) {
//-若指定 则匹配(匹配不到 则随机)
targetProject = allProject.stream().filter(o -> projectName.equals(o.getProjectName())).findAny().orElse(allProject.get(RandomUtil.randomInt(allProject.size())));}else {
//若不指定 则随机
targetProject = allProject.get(RandomUtil.randomInt(allProject.size()));}planDto.setProjectId(Long.parseLong(targetProject.getProjectId()));planDto.setProjectName(targetProject.getProjectName());
然后,设置其他属性
//计划名称if (ObjectUtil.isNotEmpty(planName)) {
//若指定名称,则按需求拼接
planDto.setPlanName(String.format("自动化测试-[采购计划]-%s",planName));}else {
//若不指定名称,则按不重复拼接
String format = String.format("自动化测试-[%s]采购计划", ChineseCharUtils.genFixedLengthChineseChars(4));
planDto.setPlanName(TestDataUtils.getRandomStrNum(format,2000));}
供应商类别,与项目 id 相似的情况,但我们可以交给调用者来指定(测试时,由具体测试人员选择目标类别)
//供应商类别planDto.setProviderCategoryId(providerCategoryId);planDto.setProviderCategoryName(providerCategoryName);
04 完善方法入参及注释
最终我们的 2 个主要核心方法,以及测试用例调用方式就完成了:
创建采购计划(过程)节点 DTO List
List<PlanNodeDto> buildPlanNodeDtoList(String purchaseMethod,Boolean isDeletedNodes)
创建 Plan 采购计划 DTO 对象
PurchasePlanDto buildPlanDto(String planName,String projectName,String purchaseCategoryName,
String purchaseMethodName,String providerCategoryId,
String providerCategoryName,Boolean isDeletedNodes)
测试用例调用 - 新增保存计划
@Testvoid savePlan(@Optional("")String planName,
@Optional("")String projectName,
@Optional("")String purchaseCategoryName,
@Optional("")String purchaseMethodName,
@Optional("1597061939012812802")String providerCategoryId,
@Optional("建筑方案设计")String providerCategoryName,
@Optional("false")Boolean isDeletedNodes)
三、总结分析
应用远景
当数据构造变得简单且高效后,可扩展的功能方向很多,例如:复杂业务数据核对验证(断言)、长链路业务测试创建高可用数据、全链路压测、全自动回归测试(定时任务、调度算法、脚本转换成接口)
数据构造抽离出通用性,提供自定义入参功能,可提供给后续 测试用例调用,能大大提高测试效率
需注意事项
本文示例 DTO 对象较简单,往往业务中接口数据较复杂,不过再复杂的数据对象,分析模式也与本文相同,一个一个去分析,最终,你会对系统更加了解(提高测试效率)。
实际开发中需结合多方因素(长期运用、高可用、容错、编码时效等)来考虑,采用什么方式取值,并非只能通过调用接口、枚举的方式
本文重在分享一种构造数据 DTO 的思路,如果代码有不明白的,建议多学习下 Java-基础,将基础打牢(多敲多练),如果使用 Python,同样可以按这个思路来分析处理,无非是代码技巧不一样
四、完整代码
public class PlanTest extends TestBase {
private static final ReportLog reportLog = new ReportLog(PlanTest.class);
public Map<String,String> header = getBackTokenHeader("*****","*****");
public List<BidUcUser> allTestUsers = Arrays.stream(TestUserEnum.values()).map(u -> {
BidUcUser bidUcUser = new BidUcUser();
bidUcUser.setType(u.getType());
bidUcUser.setBidNodeCode(u.getBidNodeCode());
bidUcUser.setUserId(Long.parseLong(u.getUserId()));
bidUcUser.setRealName(u.getRealname());
bidUcUser.setUserName(u.getUsername());
return bidUcUser;
}).collect(Collectors.toList());
public List<Project> allProject = Arrays.stream(ProjectEnum.values()).map(o -> {return Project.builder().projectId(o.getProjectId()).projectName(o.getProjectName()).build();}).collect(Collectors.toList());
/**
* 测试用例调用-新增保存计划
* @param planName 计划名称 选填 不填则按代码规则随机生成
* @param projectName 项目名称 选填 项目范围在枚举类 ProjectEnum 中配置,若需扩大范围,需自行填加
* @param purchaseCategoryName 采购类别名称 选填 若指定,则匹配 若匹配不到或不指定,则写死类别(采购类别-所有采购方式)
* @param purchaseMethodName 采购方式名称 选填 若指定,则匹配 若匹配不到或不指定,则写死采购方式(邀请招标)
* @param providerCategoryId 供应商类别ID 必填
* @param providerCategoryName 供应商类别名称 必填
* @param isDeletedNodes 是否删除不必要节点
*/
@Test
void savePlan(@Optional("")String planName,
@Optional("")String projectName,
@Optional("")String purchaseCategoryName,
@Optional("")String purchaseMethodName,
@Optional("1597061939012812802")String providerCategoryId,
@Optional("建筑方案设计")String providerCategoryName,
@Optional("false")Boolean isDeletedNodes) {
//创建入参 采购计划DTO对象
PurchasePlanDto planDto = buildPlanDto(planName, projectName, purchaseCategoryName, purchaseMethodName, providerCategoryId, providerCategoryName, isDeletedNodes);
//提交保存计划接口
String rs = HttpUtils.doPost(HostLH.TEST_HOST.concat(ApiBid.PLAN_SAVE), header, JSONObject.toJSONString(planDto));
//输出响应结果日志信息
reportLog.info(" PLAN_SAVE ====> {}",JSONObject.parseObject(rs));
}
/**
* 创建Plan 采购计划DTO对象
* @param planName 计划名称 选填 不填则按代码规则随机生成
* @param projectName 项目名称 选填 项目范围在枚举类 ProjectEnum 中配置,若需扩大范围,需自行填加
* @param purchaseCategoryName 采购类别 选填 若指定,则匹配 若匹配不到或不指定,则写死类别(采购类别-所有采购方式)
* @param purchaseMethodName 采购方式 选填 若指定,则匹配 若匹配不到或不指定,则写死采购方式(邀请招标)
* @param providerCategoryId 供应商类别ID 必填
* @param providerCategoryName 供应商类别名称 必填
* @param isDeletedNodes 是否删除不必要节点
*/
PurchasePlanDto buildPlanDto(String planName,String projectName,String purchaseCategoryName,
String purchaseMethodName,String providerCategoryId,
String providerCategoryName,Boolean isDeletedNodes) {
PurchasePlanDto planDto = new PurchasePlanDto();
//计划名称
if (ObjectUtil.isNotEmpty(planName)) {
//若指定名称,则按需求拼接
planDto.setPlanName(String.format("自动化测试-[采购计划]-%s",planName));
}else {
//若不指定名称,则按不重复拼接
String format = String.format("自动化测试-[%s]采购计划", ChineseCharUtils.genFixedLengthChineseChars(4));
planDto.setPlanName(TestDataUtils.getRandomStrNum(format,2000));
}
//设置 不具备依赖性的字段值
planDto.setEstimatedAmount(new BigDecimal(RandomUtil.randomDouble(99999999999999.99,2, RoundingMode.HALF_UP)).setScale(2,RoundingMode.HALF_UP)); //预计签约金额,元
planDto.setRecordTime(RandomUtil.randomDay(-1000,0)); //入场时间
planDto.setPlanStartTime(RandomUtil.randomDay(-100,0)); //计划开始时间
planDto.setPlanFinishTime(RandomUtil.randomDay(500,1000)); //计划完成时间
//项目分期
PlanTest.Project targetProject;
if (ObjectUtil.isNotEmpty(projectName)) {
//-若指定 则匹配(匹配不到 则随机)
targetProject = allProject.stream().filter(o -> projectName.equals(o.getProjectName())).findAny().orElse(allProject.get(RandomUtil.randomInt(allProject.size())));
}else {
//若不指定 则随机
targetProject = allProject.get(RandomUtil.randomInt(allProject.size()));
}
planDto.setProjectId(Long.parseLong(targetProject.getProjectId()));
planDto.setProjectName(targetProject.getProjectName());
//采购类别
//调用API-采购类别列表 查询启用状态的采购类别List
String rs = HttpUtils.doGet(HostLH.TEST_HOST.concat(ApiBid.PURCHASE_CATEGORY_LIST), header, "status=true");
List<JSONObject> categoryList = JSON.parseObject(rs).getJSONArray("body").toJavaList(JSONObject.class);
PlanTest.PurchaseCategory targetCategory = null;
//获取目标采购类别 若指定,则匹配
if (ObjectUtil.isNotEmpty(purchaseCategoryName)) {
targetCategory = categoryList.stream().filter(cate -> purchaseCategoryName.equals(cate.getString("name")))
.map(cate -> {
return PurchaseCategory.builder()
.purchaseCategoryName(cate.getString("name"))
.purchaseCategoryId(cate.getString("id"))
.build();
}).findAny().orElse(null);
}
//若匹配不到或不指定,则写死类别(采购类别-所有采购方式)
if (null == targetCategory) {
targetCategory = PurchaseCategory.builder().purchaseCategoryId("1600406567327461378").purchaseCategoryName("采购类别-所有采购方式").build();
}
planDto.setPurchaseCategoryName(targetCategory.getPurchaseCategoryName());
planDto.setPurchaseCategoryId(Long.parseLong(targetCategory.getPurchaseCategoryId()));
//采购方式
//调用API-根据ID获取采购方式详情 ID来自类别targetCategory
rs = HttpUtils.doGet(HostLH.TEST_HOST.concat(String.format(ApiBid.PURCHASE_CATEGORY_METHOD, targetCategory.getPurchaseCategoryId())), header);
List<JSONObject> methodList = JSON.parseObject(rs).getJSONArray("body").toJavaList(JSONObject.class);
PlanTest.PurchaseMethod targetMethod = null;
//获取采购类别关联的 采购方式 若指定,则匹配
if (ObjectUtil.isNotEmpty(purchaseMethodName)) {
targetMethod = methodList.stream().filter(method -> purchaseMethodName.equals(method.getString("name")))
.map(method -> {
return PlanTest.PurchaseMethod.builder()
.purchaseMethodCode(method.getString("code"))
.purchaseMethodName(method.getString("name"))
.ifControlPriceFloor(method.getBoolean("ifControlPriceFloor"))
.ifEvaluationStaffGteThree(method.getBoolean("ifEvaluationStaffGteThree"))
.ifEvaluationStaffOdd(method.getBoolean("ifEvaluationStaffOdd"))
.ifSignMoneyControl(method.getBoolean("ifSignMoneyControl"))
.ifStrategic(method.getBoolean("ifStrategic"))
.ifTn(method.getBoolean("ifTn"))
.build();
}).findAny().orElse(null);
}
//若匹配不到或不指定,则写死采购方式(邀请招标)
if (null == targetMethod) {
targetMethod = PurchaseMethod.builder()
.purchaseMethodName("邀请招标")
.purchaseMethodCode("inviteBid")
.ifControlPriceFloor(true)
.ifEvaluationStaffGteThree(true)
.ifEvaluationStaffOdd(true)
.ifSignMoneyControl(true)
.ifStrategic(true)
.ifTn(true)
.build();
}
//浅拷贝 targetMethod所有属性到planDto
BeanUtil.copyProperties(targetMethod,planDto);
//议标类型
rs = HttpUtils.doGet(HostLH.TEST_HOST.concat(ApiBid.BIDDICT_LIST), header, "type=bidType&status=true");
List<JSONObject> bidDictList = JSONObject.parseObject(rs).getJSONArray("body").toJavaList(JSONObject.class);
//优先取默认的
JSONObject bidType = bidDictList.stream().filter(json -> json.getBoolean("ifDefault")).findAny().orElse(null);
if (null == bidType) {
//若不存在默认的,则随机取
bidType = bidDictList.get(RandomUtil.randomInt(bidDictList.size()));
}
planDto.setBidTypeName(bidType.getString("name"));
planDto.setBidTypeValue(bidType.getString("value"));
//供应商类别
planDto.setProviderCategoryId(providerCategoryId);
planDto.setProviderCategoryName(providerCategoryName);
//设置 招标经办人
TestUserEnum chargeUser = TestUserEnum.ATEzbjbr;
planDto.setChargeUserId(Long.parseLong(chargeUser.getUserId()));
planDto.setChargeUserName(chargeUser.getRealname());
planDto.setChargeUserCode(chargeUser.getUsername());
//采购节点List
planDto.setNodes(buildPlanNodeDtoList(planDto.getPurchaseMethodCode(),isDeletedNodes));
return planDto;
}
/**
* 创建采购计划(过程)节点DTO List<PlanNodeDto>
* @param purchaseMethod 采购方法编码
* @param isDeletedNodes 是否删除非必要节点
* @return
*/
List<PlanNodeDto> buildPlanNodeDtoList(String purchaseMethod,Boolean isDeletedNodes) {
//调用接口(API功能-根据采购方式编码获取详情信息-含关联的采购节点信息)
String apiUrl = String.format(HostLH.TEST_HOST.concat(ApiBid.PURCHASE_METHOD_DETAIL), purchaseMethod);
String rs = HttpUtils.doGet(apiUrl, header);
//解析响应json拿到所有的节点List
List<JSONObject> purchaseMethodNodes = JSON.parseObject(rs).getJSONObject("body").getJSONArray("purchaseMethodNodes").toJavaList(JSONObject.class);
DateTime now = DateUtil.date();
AtomicInteger loopInt = new AtomicInteger(1);
List<PlanNodeDto> nodeDtoList = new ArrayList<>(purchaseMethodNodes.size());
purchaseMethodNodes.forEach(node -> {
PlanNodeDto nodeDto = new PlanNodeDto();
nodeDto.setName(node.getString("name")); //节点名称
nodeDto.setPurchaseMethodCode(node.getString("purchaseMethodCode")); //关联采购方式编码
nodeDto.setPurchaseMethodNodeCode(node.getString("code")); //关联采购方式节点编码
//责任部门
nodeDto.setChargeOrg(node.getString("responsibleDept"));
nodeDto.setChargeOrgValue(node.getString("responsibleDeptName"));
nodeDto.setIfNecessity(node.getBoolean("ifNecessity")); //是否必要节点
nodeDto.setSort(node.getInteger("sort")); //排序
nodeDto.setStartTime(DateUtil.offsetDay(now,loopInt.getAndAdd(30))); //节点计划开始时间
nodeDto.setFinishTime(DateUtil.offsetDay(now,loopInt.getAndAdd(30))); //节点计划完成时间
//节点经办人
BidUcUser chargeUser = allTestUsers.stream().filter(u -> node.getString("code").equals(u.getBidNodeCode()) && "1".equals(u.getType())).findFirst().orElse(null);
if (null == chargeUser) {
throw new BusinessException("测试账号节点code未匹配,请检查配置!");
}
nodeDto.setChargeUserId(chargeUser.getUserId());
nodeDto.setChargeUserName(chargeUser.getRealName());
nodeDto.setChargeUserCode(chargeUser.getUserName());
nodeDtoList.add(nodeDto);
});
return nodeDtoList;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class PurchaseCategory{
private String purchaseCategoryName;
private String purchaseCategoryId;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class PurchaseMethod{
private String purchaseMethodName;
private String purchaseMethodCode;
private Boolean ifControlPriceFloor;
private Boolean ifEvaluationStaffGteThree;
private Boolean ifEvaluationStaffOdd;
private Boolean ifSignMoneyControl;
private Boolean ifStrategic;
private Boolean ifTn;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class Project{
private String projectId;
private String projectName;
}}
最后的话:
作者学习java收获最大的不是代码能力,而是对JVM、spring IOC AOP等、以及 springcloud 微服务生态的理解提升,这也帮助我切身地运用到了日常开发测试,近期在研究流量回放相关实现,也受到不少前辈分享的启发,更加明白知识的分享能促进共同进步。
资源分享【这份资料必须领取~】
下方这份完整的软件测试视频学习教程已经上传CSDN官方认证的二维码,朋友们如果需要可以自行免费领取 【保证100%免费】