自动化测试中,经常需要构造请求参数,例如 JSON 格式的参数,简单的好说,可以手工修改或是用 Postman、Jmeter 等工具结合简单的代码进行处理, 但当数据传输对象(DTO)很复杂,部分字段依赖性很强、强校验时,就不能单靠手工修改来满足自动化测试需求了,这时候就需要利用代码来解决了,本文给大家分享一种构造测试数据的方法 ====> 通过枚举、1:1 模仿前端请求逻辑接口以及程序解析数据来构造复杂 JSON。
- 文章作者:gitee@ 随性自然 fqc, 转载时,请注明来源,注明作者,这是对文章作者的尊重,也是对知识的尊重。
本次示例接口为业务开发中常见的新增保存接口,参数相对比较简单,为方便理解,我这里画个草图讲下大概的业务逻辑:
总体,业务逻辑 为创建采购计划选择不同的采购方式 并发起采购过程,采购过程由节点 1线性流程一直操作到节点 N。其中需特殊说明的:
开发语言采用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、分析参数结构,嵌套层级关系
2、找出哪些字段不具备依赖性,该部分字段可以通过 随机、写死、自定义方式设置 value,该部分字段处理较简单
3、找出各强校验、具备逻辑关联关系的字段,并梳理清楚它们的取值源、取值逻辑、影响后续业务逻辑,我们的编程主要是针对该部分字段进行设置 value,该部分字段优先采用调用接口形式取值,如果处理起来麻烦,可采用枚举形式,在限定范围内取值
或者直接复制后端代码文件,这属于Java 基础 - 面向对象,本文就不细介绍,直接贴代码:
PurchasePlanDto 采购计划对象:
@Data
public 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 采购过程节点对象:
@Data
public 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;
}
}
怎么开始呢? ==> 这里作者 建议从里层向外层,先简单后复杂进行编写 即优先编写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
@AllArgsConstructor
public 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<user>
对象,并定义成全局变量,提供给后续使用:
//全局变量
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());
}
根据先前的 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
@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;
}
考虑到可以根据需要定义目标采购类别,即计划创建方法buildPlanDto()
中增加采购类别入参 >> buildPlanDto(String purchaseCategoryName)
//采购类别
//调用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()));
设置采购方式属性 与类别相似,接收调用者入参,提供匹配 方法改为buildPlanDto(String purchaseCategoryName,String purchaseMethodName)
//采购方式
//调用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);
设置议标类型
//议标类型
//调用API-字典项列表接口获取list
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"));
设置项目 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
@user30
public enum ProjectEnum {
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二期"),
;
private String projectId;
private String projectName;
}
也为方便测试,添加内部类 - 项目对象
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public 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);
最终我们的 2 个主要核心方法,以及测试用例调用方式就完成了:
创建采购计划(过程)节点 DTO List<PlanNodeDto>
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)
测试用例调用 - 新增保存计划
@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)
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;
}
}