测试驱动开发实践3————testSave之新增用户
内容指引
1.确定新增用户的业务规则
2.根据业务规则设计测试用例
3.为测试用例赋值并驱动开发
一、确定新增用户的规则
1.注册用户允许通过“用户名+密码”、“手机号+密码”或“Email+密码”来注册:
2.对于通过“用户名+密码”注册的用户,手机号和Email字段可以为空;
3.对于通过“手机号+密码”注册的用户,用户名和Email字段可以为空;
4.对于通过“Email+密码”注册的用户,用户名和手机号字段可以为空;
5.但是,用户名、手机号和Email不可同时为空,必须至少有其中一个值不为空,并且:
6.如果用户名字段有值,那么该字段必须由3-64位大小写字母和数字组成,且值不可与其它用户名重复;
7.如果手机号字段有值,那么该字段必须符合手机号格式,且值不可与其它手机号重复;
8.如果Email字段有值,那么该字段必须符合Email格式,且值不可与其它Email重复;
9.密码允许6-16位大小写字母、数字及下划线组成;
二、根据业务规则设计测试用例
设计测试用例的技巧
先列出可能新增数据的方式,以此分组。如可以根据“用户名+密码”、“手机号+密码”和“Email+密码”分为三组,然后对此三组分别进一步细化设计;
先合法,再非法。先设计输入合法值时的用例,再设计非法值时的用例;
设计非法值用例时针对每个可能的参数各个击破。如“用户名+密码”的非法值就分为“用户名不合法”和“密码不合法”;
对于每个非法值的设计,先考虑输入校验,再考虑逻辑校验。如,对于用户名不合法,从输入校验上可以分为“用户名为空”、“用户名长度不合法(3-64位)”和“用户名违反大小写字母及数字组成的规则”,从逻辑校验上需要设计“用户名已存在(违反唯一性)”的用例。密码不合法的用例可同样参考这个顺序设计。
可借助Xmind思维导图工具协助用例设计:

最终我们设计出如下测试用例:
1.用户名+密码(注册方式一)
1.1 用户名、密码均合法;
1.2 用户名不合法:
1.2.1 用户名为空;
1.2.2 用户名长度不合法(3-64位);
1.2.3 用户名违反大小写字母及数字组成的规则;
1.2.4 用户名已存在(违反唯一性);
1.3 密码不合法
1.3.1 长度不符合6-16位;
1.3.2 违反大小写字母、数字及下划线组成的规则;
2.手机号+密码(注册方式二)
2.1 手机号、密码均合法;
2.2 手机号不合法:
2.1.1 手机号为空;
2.1.2 不符合手机号格式;
2.1.3 手机号已存在;
2.3 密码不合法:同1.3
3.Email+密码(注册方式三)
3.1 Email、密码均合法;
3.2 Email不合法
3.2.1 Email为空;
3.2.2 不符合Email格式;
3.2.3 Email已存在;
3.3 密码不合法:同1.3
三、为测试用例赋值并驱动开发
首先打开测试方法testSave,这个方法中会依次测试新增用户和修改用户的逻辑,定位在“测试新增用户”处:

首先,我们完成第一个任务“//TODO 列出新增用户测试用例清单”。将“测试用例清单文档”写入多行注释中,作为测试清单。以后还有可能往这个清单中增加新的测试用例。让测试用例代码成为有价值的开发文档;
/**
* 测试新增用户
*/
/**
* 列出新增用户测试用例清单
*
1.用户名+密码(注册方式一)
1.1 用户名、密码均合法;
1.2 用户名不合法:
1.2.1 用户名为空;
1.2.2 用户名长度不合法(3-64位);
1.2.3 用户名违反大小写字母及数字组成的规则;
1.2.4 用户名已存在(违反唯一性);
1.3 密码不合法
1.3.1 长度不符合6-16位;
1.3.2 违反大小写字母、数字及下划线组成的规则;
2.手机号+密码(注册方式二)
2.1 手机号、密码均合法;
2.2 手机号不合法:
2.1.1 手机号为空;
2.1.2 不符合手机号格式;
2.1.3 手机号已存在;
2.3 密码不合法:同1.3
3.Email+密码(注册方式三)
3.1 Email、密码均合法;
3.2 Email不合法
3.2.1 Email为空;
3.2.2 不符合Email格式;
3.2.3 Email已存在;
3.3 密码不合法:同1.3
*/
“云开发”平台生成的初始化代码中已经为我们设计了一个”测试新增用户“的测试模版,由“测试用例赋值”、“模拟请求”及“测试断言”组成。代码如下:
测试用例赋值
/**---------------------测试用例赋值开始---------------------**/
//TODO 将下面的null值换为测试参数
User user = new User();
user.setUuid(null);
user.setUsername(null);
user.setPassword(null);
user.setNickname(null);
user.setPhoto(null);
user.setMobile(null);
user.setEmail(null);
user.setQq(null);
user.setWeChatNo(null);
user.setSex(null);
user.setMemo(null);
user.setScore(null);
user.setLastLogin(null);
user.setLoginIP(null);
user.setLoginCount(null);
user.setLock(null);
user.setLastLockTime(null);
user.setLockTimes(null);
Long operator = null;
Long id = 4L;
/**---------------------测试用例赋值结束---------------------**/
模拟请求
this.mockMvc.perform(
MockMvcRequestBuilders.post("/user/create")
.param("uuid",user.getUuid())
.param("username",user.getUsername())
.param("password",user.getPassword())
.param("nickname",user.getNickname())
.param("photo",user.getPhoto())
.param("mobile",user.getMobile())
.param("email",user.getEmail())
.param("qq",user.getQq())
.param("weChatNo",user.getWeChatNo())
.param("sex",user.getSex().toString())
.param("memo",user.getMemo())
.param("score",user.getScore().toString())
.param("lastLogin",user.getLastLogin().toString())
.param("loginIP",user.getLoginIP())
.param("loginCount",user.getLoginCount().toString())
.param("lock",user.getLock().toString())
.param("lastLockTime",user.getLastLockTime().toString())
.param("lockTimes",user.getLockTimes().toString())
.param("operator",operator.toString())
)
测试断言
// 打印结果
.andDo(print())
// 检查状态码为200
.andExpect(status().isOk())
// 检查内容有"user"
.andExpect(content().string(containsString("user")))
// 检查返回的数据节点
.andExpect(jsonPath("$.user.userId").value(id))
.andExpect(jsonPath("$.user.uuid").value(user.getUuid()))
.andExpect(jsonPath("$.user.username").value(user.getUsername()))
.andExpect(jsonPath("$.user.password").value(user.getPassword()))
.andExpect(jsonPath("$.user.nickname").value(user.getNickname()))
.andExpect(jsonPath("$.user.photo").value(user.getPhoto()))
.andExpect(jsonPath("$.user.mobile").value(user.getMobile()))
.andExpect(jsonPath("$.user.email").value(user.getEmail()))
.andExpect(jsonPath("$.user.qq").value(user.getQq()))
.andExpect(jsonPath("$.user.weChatNo").value(user.getWeChatNo()))
.andExpect(jsonPath("$.user.sex").value(user.getSex()))
.andExpect(jsonPath("$.user.memo").value(user.getMemo()))
.andExpect(jsonPath("$.user.score").value(user.getScore()))
.andExpect(jsonPath("$.user.lastLogin").value(user.getLastLogin()))
.andExpect(jsonPath("$.user.loginIP").value(user.getLoginIP()))
.andExpect(jsonPath("$.user.loginCount").value(user.getLoginCount()))
.andExpect(jsonPath("$.user.lock").value(user.getLock()))
.andExpect(jsonPath("$.user.lastLockTime").value(user.getLastLockTime()))
.andExpect(jsonPath("$.user.lockTimes").value(user.getLockTimes()))
.andExpect(jsonPath("$.user.creationTime").isNotEmpty())
.andExpect(jsonPath("$.user.creatorUserId").value(operator))
.andExpect(jsonPath("$.user.lastModificationTime").isEmpty())
.andExpect(jsonPath("$.user.lastModifierUserId").value(0))
.andExpect(jsonPath("$.user.isDeleted").value(false))
.andExpect(jsonPath("$.user.deletionTime").isEmpty())
.andExpect(jsonPath("$.user.deleterUserId").value(0))
.andReturn();
每一个测试用例的测试代码均由“测试用例赋值+模拟请求+测试断言组成”,测试用例赋值不同,模拟请求的参数和测试断言就应相应调整。
1.用户名、密码均合法:
第一个新增用户的测试用例代码,就在原测试模版的基础上修改即可。修改后代码:
/**---------------------测试用例赋值开始---------------------**/
// 用户名、密码均合法
User user = new User();
user.setUsername("Manon");
user.setPassword("123456");
Long operator = 1L;
Long id = 7L;
/**---------------------测试用例赋值结束---------------------**/
this.mockMvc.perform(
MockMvcRequestBuilders.post("/user/create")
.param("username",user.getUsername())
.param("password",user.getPassword())
)
// 打印结果
.andDo(print())
// 检查状态码为200
.andExpect(status().isOk())
// 检查内容有"user"
.andExpect(content().string(containsString("user")))
// 检查返回的数据节点
.andExpect(jsonPath("$.user.userId").value(id))
.andExpect(jsonPath("$.user.uuid").isNotEmpty())
.andExpect(jsonPath("$.user.username").value(user.getUsername()))
.andExpect(jsonPath("$.user.password").value(user.getPassword()))
.andExpect(jsonPath("$.user.nickname").isEmpty())
.andExpect(jsonPath("$.user.photo").isEmpty())
.andExpect(jsonPath("$.user.mobile").isEmpty())
.andExpect(jsonPath("$.user.email").isEmpty())
.andExpect(jsonPath("$.user.qq").isEmpty())
.andExpect(jsonPath("$.user.weChatNo").isEmpty())
.andExpect(jsonPath("$.user.sex").value(0))
.andExpect(jsonPath("$.user.memo").isEmpty())
.andExpect(jsonPath("$.user.score").value(0.0))
.andExpect(jsonPath("$.user.lastLogin").isEmpty())
.andExpect(jsonPath("$.user.loginIP").isEmpty())
.andExpect(jsonPath("$.user.loginCount").value(0))
.andExpect(jsonPath("$.user.lock").value(false))
.andExpect(jsonPath("$.user.lastLockTime").isEmpty())
.andExpect(jsonPath("$.user.lockTimes").value(0))
.andExpect(jsonPath("$.user.creationTime").isNotEmpty())
.andExpect(jsonPath("$.user.creatorUserId").value(0))
.andExpect(jsonPath("$.user.lastModificationTime").isEmpty())
.andExpect(jsonPath("$.user.lastModifierUserId").value(0))
.andExpect(jsonPath("$.user.isDeleted").value(false))
.andExpect(jsonPath("$.user.deletionTime").isEmpty())
.andExpect(jsonPath("$.user.deleterUserId").value(0))
.andReturn();
代码解说
/**---------------------测试用例赋值开始---------------------**/
// 用户名、密码均合法
User user = new User();
user.setUsername("Manon");
user.setPassword("123456");
Long operator = 1L;
Long id = 7L;
/**---------------------测试用例赋值结束---------------------**/
在测试用例赋值部分,我们输入合法的用户名“Manon”,合法的密码“123456”。
给operator赋值为1,这个变量在修改用户的时候会用上,因为是Long型,所以写为“1L”。
为id赋值为“7L”。为什么输入为“7”?这里需要解释一下:
在UserControllerTest类运行时,先会执行testList方法,接着执行testSave方法。在执行testList方法前执行了setUp方法,其中添加了一条数据,id为“1”。接着,在testList方法中添加了4条数据,所以testList方法执行完时,User的数据库表主键id变为“5”了,虽然执行完testList方法后这5条数据都因为事务回滚清空了,但是id值“1-5”已被占用了。接着准备执行testSave方法前又执行了一次setUp方法,再次添加了一条数据,id变为“6”。所以,在testSave中添加的第一条数据的主键id值应为“7”,因为是Long型字段,所以赋值为“7L”。如果在setUp或testList中插入了更多数据,那么这个值也应相应调整,原理已说明。
this.mockMvc.perform(
MockMvcRequestBuilders.post("/user/create")
.param("username",user.getUsername())
.param("password",user.getPassword())
)
这段代码是利用mockMvc模拟post访问"/user/save"这个微服务Rest控制器接口,模拟表单提交了两个参数“username”和“password”,值已经在上面的测试用例赋值中。最后一个是设定本地环境为中文,所以报错信息会用中文提示,如果是英文环境,会通过英文提示。
// 打印结果
.andDo(print())
// 检查状态码为200
.andExpect(status().isOk())
// 检查内容有"user"
.andExpect(content().string(containsString("user")))
// 检查返回的数据节点
.andExpect(jsonPath("$.user.userId").value(id))
.andExpect(jsonPath("$.user.uuid").isNotEmpty())
.andExpect(jsonPath("$.user.username").value(user.getUsername()))
.andExpect(jsonPath("$.user.password").value(user.getPassword()))
.andExpect(jsonPath("$.user.nickname").isEmpty())
.andExpect(jsonPath("$.user.photo").isEmpty())
.andExpect(jsonPath("$.user.mobile").isEmpty())
.andExpect(jsonPath("$.user.email").isEmpty())
.andExpect(jsonPath("$.user.qq").isEmpty())
.andExpect(jsonPath("$.user.weChatNo").isEmpty())
.andExpect(jsonPath("$.user.sex").value(0))
.andExpect(jsonPath("$.user.memo").isEmpty())
.andExpect(jsonPath("$.user.score").value(0.0))
.andExpect(jsonPath("$.user.lastLogin").isEmpty())
.andExpect(jsonPath("$.user.loginIP").isEmpty())
.andExpect(jsonPath("$.user.loginCount").value(0))
.andExpect(jsonPath("$.user.lock").value(false))
.andExpect(jsonPath("$.user.lastLockTime").isEmpty())
.andExpect(jsonPath("$.user.lockTimes").value(0))
.andExpect(jsonPath("$.user.creationTime").isNotEmpty())
.andExpect(jsonPath("$.user.creatorUserId").value(0))
.andExpect(jsonPath("$.user.lastModificationTime").isEmpty())
.andExpect(jsonPath("$.user.lastModifierUserId").value(0))
.andExpect(jsonPath("$.user.isDeleted").value(false))
.andExpect(jsonPath("$.user.deletionTime").isEmpty())
.andExpect(jsonPath("$.user.deleterUserId").value(0))
.andReturn();
其中:
.andDo(print())
这个是用来将请求及返回结果打印到控制台中,方便测试人员查看及分析。
// 检查状态码为200
.andExpect(status().isOk())
这个是基本的检查,正确的请求返回的状态码应为“200”,如果是“404”或其它值,就代表有问题。
// 检查内容有"user"
.andExpect(content().string(containsString("user")))
如果新增数据成功,那么应返回user的实例json数据,其中含有user(和领域类名称相同)这个节点。如果表单验证通不过,则返回“formErrors”节点,如果发生异常,则返回“errorMessage”节点。
// 检查返回的数据节点
.andExpect(jsonPath("$.user.userId").value(id))
.andExpect(jsonPath("$.user.uuid").isNotEmpty())
.andExpect(jsonPath("$.user.username").value(user.getUsername()))
.andExpect(jsonPath("$.user.password").value(user.getPassword()))
.andExpect(jsonPath("$.user.nickname").isEmpty())
.andExpect(jsonPath("$.user.photo").isEmpty())
.andExpect(jsonPath("$.user.mobile").isEmpty())
.andExpect(jsonPath("$.user.email").isEmpty())
.andExpect(jsonPath("$.user.qq").isEmpty())
.andExpect(jsonPath("$.user.weChatNo").isEmpty())
.andExpect(jsonPath("$.user.sex").value(0))
.andExpect(jsonPath("$.user.memo").isEmpty())
.andExpect(jsonPath("$.user.score").value(0.0))
.andExpect(jsonPath("$.user.lastLogin").isEmpty())
.andExpect(jsonPath("$.user.loginIP").isEmpty())
.andExpect(jsonPath("$.user.loginCount").value(0))
.andExpect(jsonPath("$.user.lock").value(false))
.andExpect(jsonPath("$.user.lastLockTime").isEmpty())
.andExpect(jsonPath("$.user.lockTimes").value(0))
.andExpect(jsonPath("$.user.creationTime").isNotEmpty())
.andExpect(jsonPath("$.user.creatorUserId").value(0))
.andExpect(jsonPath("$.user.lastModificationTime").isEmpty())
.andExpect(jsonPath("$.user.lastModifierUserId").value(0))
.andExpect(jsonPath("$.user.isDeleted").value(false))
.andExpect(jsonPath("$.user.deletionTime").isEmpty())
.andExpect(jsonPath("$.user.deleterUserId").value(0))
.andReturn();
这是对返回的json数据的进一步判断,其中:
userId的值应该等于前面定义的id值"7";
uuid的值由领域类默认赋值,所以返回值中应该已有生成的UUID值,所以这里断言该字段不为空;
username和password:返回值应该等于前面赋的参数值;
nickname、photo、mobile、email、qq、weChatNo、memo:这些未传参赋值的String字段,应该保存为空字符串,所以断言为“.isEmpty()”;
sex:性别,未赋值的情况下应保存为“0”(性别保密);
score:评分,未赋值情况下应默认保存为“0.0”;
lastLogin:最后一次登陆时间,注册时未登陆,所以应保存为null,测试断言也应是“isEmpty()”;
loginIP:登陆IP,应返回空字符串;
loginCount:登陆次数应为0;
lock:是否锁定,默认注册时应该是不锁定,所以应返回值“false”;
lastLockTime:最近锁定时间,默认注册应保存为null,所以可以用“isEmpty()断言”;
lockTimes:锁定次数,默认应保存为0,所以返回值应为“0”;
creationTime:创建时间应该有值,所以可以用“.isNotEmpty()”来断言;
creatorUserId:创建者ID,本业务特殊,刚注册时无法确定创建者ID,只有注册后才产生主键ID,所以应返回该字段Long型的默认值“0”;
lastModificationTime:最近修改时间,注册时未修改,所以应保存为null,因此返回值可用“isEmpty()”断言;
lastModifierUserId:最近修改者,应返回默认值“0”;
isDeleted:新增的数据应该是未删除的,所以该字段应返回“false”;
deletionTime:删除时间应保存为null,所以返回值可用“isEmpty()”断言;
deleterUserId:删除者,应返回默认值“0”;
执行测试
写完测试代码后,我们运行下单元测试,结果如下:

异常定位在刚刚写的测试代码中,我们查看控制台提示的出错信息:

我们发现请求返回了"formErrors",说明表单输入的校验未通过,其中第一行提示是“"codes" : [ "NotNull.user.lastLogin", "NotNull.lastLogin", "NotNull.java.util.Date", "NotNull" ],”
打开领域类“User.java”,看看lastLogin这个字段:
/**
* 最后一次登录时间
*/
@NotNull(groups={CheckCreate.class, CheckModify.class})
//@Future/@Past(groups={CheckCreate.class, CheckModify.class})
@Column(name = "last_login")
private Date lastLogin;
我们看到默认是有一个“@NotNull”注解的,要求必须传参,值不能为null,实际上我们通过“用户名+密码”传参时是没有给lastLogin传参的,也就是lastLogin参数值允许为null,所以这里未通过校验。这里特别说明下“groups={CheckCreate.class, CheckModify.class}”,这是分组校验的设置。刚才我们测试代码中请求的网址是“/user/create”:
this.mockMvc.perform(
MockMvcRequestBuilders.post("/user/create")
打开被请求的rest控制器,看看这个方法的代码:

我们看到控制器的添加用户方法中启用了CheckCreate这个分组校验,所以在领域类中的字段校验注解中使用了这个分组属性的会生效,我们改下代码:
/**
* 最后一次登录时间
*/
@NotNull(groups={CheckModify.class})
@Column(name = "last_login")
private Date lastLogin;
将@NotNull注解中的"CheckCreate.class"删除掉。同理,将领域类字段中用户名(username)和密码(password)之外所有字段的校验注解中的分组校验“CheckCreate.class”都去掉。修改后代码如下:
private static final long serialVersionUID = 1L;
public interface CheckCreate{};
public interface CheckModify{};
/**
* 用户ID
*/
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long userId;
/**
* 用户UUID编号
*/
@Column(nullable = false, name = "uuid", unique = true, length = 36)
private String uuid = UUID.randomUUID().toString();
/**
* 用户名
*/
@NotBlank(groups={CheckCreate.class, CheckModify.class})
@Length(min = 3, max = 64, groups={CheckCreate.class, CheckModify.class})
@Pattern(regexp = "^[A-Za-z0-9]+$", groups={CheckCreate.class, CheckModify.class})
@Column(nullable = false, name = "username", length = 64)
private String username = "";
/**
* 密码
*/
@NotBlank(groups={CheckCreate.class, CheckModify.class})
@Length(min = 6, max = 16, groups={CheckCreate.class, CheckModify.class})
@Pattern(regexp = "^\\w+$", groups={CheckCreate.class, CheckModify.class})
@Column(nullable = false, name = "password", length = 256)
private String password;
/**
* 昵称
*/
@NotBlank(groups={CheckModify.class})
@Length(min = 1, max = 50, groups={CheckModify.class})
//@Pattern(regexp = "", groups={CheckCreate.class, CheckModify.class})
@Column(nullable = false, name = "nickname", length = 50)
private String nickname = "";
/**
* 照片
*/
@NotBlank(groups={CheckModify.class})
@Length(min = 1, max = 50, groups={CheckModify.class})
//@Pattern(regexp = "", groups={CheckCreate.class, CheckModify.class})
@Column(nullable = false, name = "photo", length = 50)
private String photo = "";
/**
* 手机
*/
@NotBlank(groups={CheckModify.class})
@Pattern(regexp = "((\\d{11})|^((\\d{7,8})|(\\d{4}|\\d{3})-(\\d{7,8})|(\\d{4}|\\d{3})-(\\d{7,8})-(\\d{4}|\\d{3}|\\d{2}|\\d{1})|(\\d{7,8})-(\\d{4}|\\d{3}|\\d{2}|\\d{1}))$)", groups={CheckModify.class})
@Column(nullable = false, name = "mobile", length = 15)
private String mobile = "";
/**
* 邮箱地址
*/
@NotBlank(groups={CheckModify.class})
@Email(groups={CheckModify.class})
@Column(nullable = false, name = "email", length = 255)
private String email = "";
/**
* QQ
*/
@NotBlank(groups={ CheckModify.class})
@Length(min = 1, max = 50, groups={CheckModify.class})
//@Pattern(regexp = "", groups={CheckCreate.class, CheckModify.class})
@Column(nullable = false, name = "qq", length = 50)
private String qq = "";
/**
* 微信号
*/
@NotBlank(groups={ CheckModify.class})
@Length(min = 1, max = 50, groups={CheckModify.class})
//@Pattern(regexp = "", groups={CheckCreate.class, CheckModify.class})
@Column(nullable = false, name = "we_chat_no", length = 50)
private String weChatNo = "";
/**
* 性别
*/
//@Range(min=value,max=value, groups={CheckCreate.class, CheckModify.class})
@Column(nullable = false, name = "sex")
private Short sex = 0;
/**
* 描述
*/
@NotBlank(groups={CheckModify.class})
@Length(min = 1, max = 50, groups={CheckModify.class})
//@Pattern(regexp = "", groups={CheckCreate.class, CheckModify.class})
@Column(nullable = false, name = "memo", length = 50)
private String memo = "";
/**
* 评分
*/
//@Range(min=value,max=value, groups={CheckCreate.class, CheckModify.class})
//@Digits(integer, fraction, groups={CheckCreate.class, CheckModify.class})
@Column(nullable = false, name = "score")
private Double score = 0.0;
/**
* 最后一次登录时间
*/
@NotNull(groups={CheckModify.class})
@Column(name = "last_login")
private Date lastLogin;
/**
* 最后一次登录IP
*/
@NotBlank(groups={CheckModify.class})
@Pattern(regexp = "^(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])$", groups={CheckModify.class})
@Column(nullable = false, name = "login_i_p", length = 23)
private String loginIP = "";
/**
* 登录次数
*/
//@Range(min=value,max=value, groups={CheckCreate.class, CheckModify.class})
@Column(nullable = false, name = "login_count")
private Integer loginCount = 0;
/**
* 是否被锁定
*/
@NotNull(groups={CheckModify.class})
//@AssertTrue/@AssertFalse(groups={CheckCreate.class, CheckModify.class})
@Column(nullable = false, name = "lock")
private Boolean lock = false;
/**
* 锁定时间
*/
@NotNull(groups={CheckModify.class})
@Column(name = "last_lock_time")
private Date lastLockTime;
/**
* 锁定计数器次数
*/
//@Range(min=value,max=value, groups={CheckCreate.class, CheckModify.class})
@Column(nullable = false, name = "lock_times")
private Integer lockTimes = 0;
/**
* 创建时间
*/
@Column(nullable = false, name = "creation_time", updatable=false)
private Date creationTime = new Date();
/**
* 创建者
*/
@Column(nullable = false, name = "creator_user_id", updatable=false)
private long creatorUserId;
/**
* 最近修改时间
*/
@Column(name = "last_modification_time")
private Date lastModificationTime;
/**
* 最近修改者
*/
@Column(name = "last_modifier_user_id")
private long lastModifierUserId;
/**
* 已删除
*/
@Column(nullable = false, name = "is_deleted")
private Boolean isDeleted = false;
/**
* 删除时间
*/
@Column(name = "deletion_time")
private Date deletionTime;
/**
* 删除者
*/
@Column(name = "deleter_user_id")
private long deleterUserId;
再次运行单元测试:

现在返回了errorMessage,说明现在领域类的校验没有问题,但是程序运行出现了异常,现在我们通过debug的方式运行单元测试,并且在rest控制器(UserController.java的create方法)中打断点:

进入到代码的下一层方法

经过断点跟踪,我们发现服务实现层的代码中有一个将operator参数转Long型的方法,这里导致了异常:

实际上,用户注册时是无法提供operator这个操作者参数的,也就是参数为null,null转Long型导致了异常,删除该代码即可。
补充一句,如果是其他类的添加数据,operator参数是需要提供的,它会赋值给领域类的创建者这个字段,以记录操作者是谁。
再此运行单元测试,现在代码出错定位在修改用户处,代表第一个测试用例已运行通过:

现在为新增用户写第二个测试用例代码:
/**---------------------测试用例赋值开始---------------------**/
// 用户名为空
User user2 = new User();
user2.setUsername("");
user2.setPassword("123456");
operator = 1L;
id = 8L;
/**---------------------测试用例赋值结束---------------------**/
this.mockMvc.perform(
MockMvcRequestBuilders.post("/user/create")
.param("username",user2.getUsername())
.param("password",user2.getPassword())
)
// 打印结果
.andDo(print())
// 检查状态码为200
.andExpect(status().isOk())
// 检查内容有"formErrors"
.andExpect(content().string(containsString("formErrors")))
// 检查返回的数据节点
.andExpect(content().string(containsString("\"code\" : \"NotBlank\"")))
.andReturn();
id赋值在上一次成功添加数据后加一,变为“8L”。
通过“用户名+密码”注册时,用户名是不可以为空的,所以,正确情况下该测试应该会引发输入校验错误,返回“formErrors”,且错误信息中应含有"code : NotBlank"(用户名不能为空)的错误:

由于我们在领域类有如下注解,所以已经能控制输入的用户名不能为空(否则触发formErrors的校验反馈):

运行单元测试,错误代码定位在测试修改的代码部分,说明第二个测试用例已通过测试。现在我们写第三个单元测试代码:
/**---------------------测试用例赋值开始---------------------**/
// 用户名长度不合法(3-64位)
User user3 = new User();
user3.setUsername("Ma");
user3.setPassword("123456");
operator = 1L;
id = 8L;
/**---------------------测试用例赋值结束---------------------**/
this.mockMvc.perform(
MockMvcRequestBuilders.post("/user/create")
.param("username",user3.getUsername())
.param("password",user3.getPassword())
)
// 打印结果
.andDo(print())
// 检查状态码为200
.andExpect(status().isOk())
// 检查内容有"formErrors"
.andExpect(content().string(containsString("formErrors")))
// 检查返回的数据节点
.andExpect(content().string(containsString("\"code\" : \"Length\"")))
.andReturn();
/**---------------------测试用例赋值开始---------------------**/
// 用户名长度不合法(3-64位)
User user4 = new User();
user4.setUsername("ManonManonManonManonManonManonManonManonManonManonManonManonManon");//长度为65
user4.setPassword("123456");
operator = 1L;
id = 8L;
/**---------------------测试用例赋值结束---------------------**/
this.mockMvc.perform(
MockMvcRequestBuilders.post("/user/create")
.param("username",user4.getUsername())
.param("password",user4.getPassword())
)
// 打印结果
.andDo(print())
// 检查状态码为200
.andExpect(status().isOk())
// 检查内容有"formErrors"
.andExpect(content().string(containsString("formErrors")))
// 检查返回的数据节点
.andExpect(content().string(containsString("\"code\" : \"Length\"")))
.andReturn();
由于上一个用例会引发“formErrors”的错误,所以不会向数据库插入数据,所以id不会增长,这里仍然给id赋值“8L”,而不是“9L”。
我们写了两个测试代码,故意分别将用户名长度设为2位和65位,不符合“3-64”位的长度要求,因为“云开发”初始化的代码中默认已将用户名的长度启用了这个长度限制的注解,所以测试应能通过(返回formErrors错误,并指明code为Length):

运行单元测试代码,果然返回了这个formErrors,说明测试已通过(换句话说,代码已能对客户端不符合长度要求的用户名进行输入校验):

现在接着写单元测试代码:
/**---------------------测试用例赋值开始---------------------**/
// 用户名违反大小写字母及数字组成的规则,故意加入不合法的"!"
User user5 = new User();
user5.setUsername("Manon!");//长度为65
user5.setPassword("123456");
operator = 1L;
id = 8L;
/**---------------------测试用例赋值结束---------------------**/
this.mockMvc.perform(
MockMvcRequestBuilders.post("/user/create")
.param("username",user5.getUsername())
.param("password",user5.getPassword())
)
// 打印结果
.andDo(print())
// 检查状态码为200
.andExpect(status().isOk())
// 检查内容有"formErrors"
.andExpect(content().string(containsString("formErrors")))
// 检查返回的数据节点
.andExpect(content().string(containsString("\"code\" : \"Pattern\"")))
.andExpect(content().string(containsString("\"codes\" : [ \"^[A-Za-z0-9]+$\" ],")))
.andReturn();
用户名中故意加入不合法的“!”,预期触发formErrors,并且提示不符合正则表达式“[A-Za-z0-9]+$”的校验规则,因为领域类中username已有该校验注解,所以测试通过了。

现在继续写单元测试用例代码,模拟“用户名已存在(违反唯一性)”的情况,前面我们添加用户名为“Manon”的用户且添加成功。现在数据库中已存在用户名为“Manon”的数据,我们故意再添加一条这样的数据,期望能引起报错。由于这个数据并没有违反用户名输入的基本校验(非空、3-64位、符合正则规则),而是违反唯一性(并且是对于非空值而言的唯一性),错误提示为“用户名已存在!”,这个需要通过逻辑校验,所以应返回异常:errorMessage。我们给这个异常一个编码:10001(这个编码可以根据自己的规则去编写,但是不同异常的错误编码不能相同),所以我们期望返回的结果中包含""errorMessage" : "[10001]",测试用例代码如下:
/**---------------------测试用例赋值开始---------------------**/
// 用户名已存在(违反唯一性)
User user6 = new User();
user6.setUsername("Manon");
user6.setPassword("123456");
operator = 1L;
id = 8L;
/**---------------------测试用例赋值结束---------------------**/
this.mockMvc.perform(
MockMvcRequestBuilders.post("/user/create")
.param("username",user6.getUsername())
.param("password",user6.getPassword())
)
// 打印结果
.andDo(print())
// 检查状态码为200
.andExpect(status().isOk())
// 检查内容有"formErrors"
.andExpect(content().string(containsString("\"errorMessage\" : \"[10001]")))
// 检查返回的数据节点
.andReturn();
对于逻辑校验而言,代码应写在服务实现层:

原来新增的用户的代码为:
if(user.getUserId()==null){
return userRepository.save(user);
修改为:
if(user.getUserId()==null){
// 以"手机号+密码"或"Email+密码"注册时,用户名可以保存为空字符串(即"用户名未设置"),但是如果用户名不为空,则不能与已存在的其它用户名重复
if(!user.getUsername().equals("")){
List<User> list = userRepository.findByUsernameIgnoringCase(user.getUsername());
if(list.size() > 0){
throw new BusinessException(ErrorCode.User_Username_Exists);
}
}
return userRepository.save(user);
我们根据用户名查询数据库,返回一个列表,如果列表元素数量大于0,就代表数据库中已存在用户名为该值的数据了,那么我们就抛出一个“用户名已存在”的异常。这里我们查询数据是通过dao层的接口方法“userRepository.findByUsernameIgnoringCase”实现的。当前还没有该接口方法,所以我们需要在dao层写一个接口:
List<User> findByUsernameIgnoringCase(String username);
上面服务实现层代码中使用BusinessException抛出异常,这是“云开发”平台封装的异常处理类,这里简单介绍下:

上图中的三个工具方法都跟异常处理相关,代码如下:
BusinessException
package top.cloudev.team.common;
/**
* 自定义业务异常类
* Created by Mac.Manon on 2018/03/13
*/
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 1L;
public BusinessException(Object Obj) {
super(Obj.toString());
}
}
ErrorCode
package top.cloudev.team.common;
/**
* 错误码枚举类
* Created by Mac.Manon on 2018/03/13
*/
public enum ErrorCode {
//TODO 在这里定义错误码,并将key加入国际化语言包。key组成规则:"ErrorCode."+ code
//User_Username_Exists("10001");
private String code;
ErrorCode(String code) {
this.code = code;
}
@Override
public String toString() {
return "[" + this.code + "] : " + I18nUtils.getMessage("ErrorCode." + this.code);
}
}
根据代码中的提示,我们定义三个错误码:用户名已存在,手机号已存在,Email已存在。其中后面两个错误码在后面的测试用例中用得上,修改后代码如下:
package top.cloudev.team.common;
/**
* 错误码枚举类
* Created by Mac.Manon on 2018/03/13
*/
public enum ErrorCode {
ser_Username_Exists("10001"),//用户名已存在
User_Mobile_Exists("10002"),//手机号已存在
User_Email_Exists("10003");//Email已存在
private String code;
ErrorCode(String code) {
this.code = code;
}
@Override
public String toString() {
return "[" + this.code + "] : " + I18nUtils.getMessage("ErrorCode." + this.code);
}
}
根据提示,我们需要在i18n的messages配置中配置好相应的错误提示多国语言包(实现异常提示信息的本地化),语言包配置位置为资源文件夹下的"i18n/messages":

其中,中文做了64位转码,如果直接使用中文,有可能客户端出现乱码。这里介绍一个转码工具:CodeText,这是Mac系统下的一个转码工具,Windows系统下也可以找找类似工具。

英文语言包配置如下:

中文语言包和默认语言包设置一致:

I18nUtils
package top.cloudev.team.common;
import javax.servlet.http.HttpServletRequest;
import java.util.Locale;
import java.util.ResourceBundle;
/**
* 国际化工具
* Created by Mac.Manon on 2018/03/13
*/
public class I18nUtils {
/**
* 根据key获得基于客户端语言的本地化信息
* @param key
* @return
*/
public static String getMessage(String key){
return getMessage(key,Locale.getDefault());
}
/**
* 根据key获得基于request指定的Locale的本地化信息
* @param key
* @param request
* @return
*/
public static String getMessage(String key, HttpServletRequest request){
if(request.getLocale() != null)
return getMessage(key,request.getLocale());
else
return getMessage(key);
}
/**
* 根据key和指定的locale获得本地化信息
* @param key
* @param locale
* @return
*/
public static String getMessage(String key, Locale locale){
ResourceBundle resourceBundle = ResourceBundle.getBundle("i18n/messages",locale);
return resourceBundle.getString(key);
}
}
现在回到服务实现类抛异常的代码:
throw new BusinessException(ErrorCode.User_Username_Exists);
当数据库中已存在该用户名时抛出如上异常。
现在运行单元测试,确实返回了我们期望的异常信息,测试通过了:

现在我们将操作系统的语言设置为英文,然后运行单元测试,则抛出英文提示的异常。以Mac系统为例:



我们看到异常的详细描述变成英文了。
接下来我们需要继续写“手机号+密码”,“Email+密码”的相关测试用例代码了,方法类似上面,就不一一讲解了,有兴趣的同学请Github上获取最新代码:
Github代码获取:https://github.com/MacManon/top_cloudev_team
测试驱动开发实践3————testSave之新增用户的更多相关文章
- 测试驱动开发实践4————testSave之新增文档分类
[内容指引] 1.确定"新增文档分类"的流程及所需的参数 2.根据业务规则设计测试用例 3.为测试用例赋值并驱动开发 一.确定"新增文档分类"的流程及所需的参数 ...
- 测试驱动开发实践5————testSave之修改文档分类
[内容指引] 1.确定"修改文档分类"的微服务接口及所需的参数 2.设计测试用例及测试用例合并 3.为测试用例赋值并驱动开发 上一篇我们通过17个测试用例完成了"新增文档 ...
- 测试驱动开发实践2————从testList开始
内容指引 1.测试之前 2.改造dto层代码 3.改造dao层代码 4.改造service.impl层的list方法 5.通过testList驱动domain领域类的修改 一.测试之前 在" ...
- 测试驱动开发实践 - Test-Driven Development(转)
一.前言 不知道大家有没听过“测试先行的开发”这一说法,作为一种开发实践,在过去进行开发时,一般是先开发用户界面或者是类,然后再在此基础上编写测试. 但在TDD中,首先是进行测试用例的编写,然后再进行 ...
- 测试驱动开发实践 - Test-Driven Development
一.前言 不知道大家有没听过“测试先行的开发”这一说法,作为一种开发实践,在过去进行开发时,一般是先开发用户界面或者是类,然后再在此基础上编写测试. 但在TDD中,首先是进行测试用例的编写,然后再进行 ...
- 测试驱动开发实践3————从testList开始
[内容指引] 运行单元测试: 装配一条数据: 模拟更多数据测试列表: 测试无搜索列表: 测试标准查询: 测试高级查询. 一.运行单元测试 我们以文档分类(Category)这个领域类为例,示范如何通过 ...
- 测试驱动开发实践—从testList开始
[内容指引]运行单元测试:装配一条数据:模拟更多数据测试列表:测试无搜索列表:测试标准查询:测试高级查询. 一.运行单元测试 我们以文档分类(Category)这个领域类为例,示范如何通过编写测试用例 ...
- TDD(测试驱动开发)培训录
2014年我一直从事在敏捷实践咨询项目,这也是我颇有收获的一年,特别是咨询项目的每一点改变,不管是代码质量的提高,还是自组织团队的建设,都能让我们感到欣慰.涉及人的问题都是复杂问题,改变人,改变一个组 ...
- TDD(测试驱动开发)培训录(转)
本文转载自:http://www.cnblogs.com/whitewolf/p/4205761.html 最近也在了解TDD,发现这篇文章不错,特此转载一下. TDD(测试驱动开发)培训录 2015 ...
随机推荐
- Exception in thread "main" java.lang.IllegalArgumentException
1.错误描述 Exception in thread "main" java.lang.IllegalArgumentException: Cannot format given ...
- Centos运行Mysql因为内存不足进程被杀
今天刚刚申请了一个新的域名,在申请完域名刚准备绑定给小伙伴分享注册新域名的喜悦时,刚把网站发到我们小伙伴们的讨论群里,却发现访问不了了,提示,数据库连接失败! 真的时一个尴尬..... 所有人都 ...
- ThreadPoolExecutor线程池参数设置技巧
一.ThreadPoolExecutor的重要参数 corePoolSize:核心线程数 核心线程会一直存活,及时没有任务需要执行 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线 ...
- SVN的安装以及和eclipse的结合使用
SVN概述 l 通常软件开发由多人协作开发,如果对代码文件.配置文件.文档等没有进行版本控制,将会出现很多问题: l 备份多个版本,占用磁盘空间大 l 解决代码冲突困难 l 容易引发BUG l 难于追 ...
- Vue-自带vue-resource插件实现http请求
安装 本地环境安装路由插件vue-resource: cnpm install vue-resource --save-dev *没有安装淘宝镜像的可以将 cnpm 替换成 npm 想要安装的可 ...
- java.lang.OutOfMemoryError: PermGen space有效解决方法
PermGen space的全称是Permanent Generation space,是指内存的永久保存区域OutOfMemoryError: PermGen space从表面上看就是内存益出,解决 ...
- 关系型数据库工作原理-查询优化器(翻译自Coding-Geek文章)
本文翻译自Coding-Geek文章:< How does a relational database work>.原文链接:http://coding-geek.com/how-data ...
- 2018第一波iOS经典笔试题(现场实拍)
序言 作为一个开发者,眼里不仅仅只存在于那一行又一行的代码,更还有那诗和远方. 注明:面试是对自我审视的一种过程,面试题和iOS程序员本身技术水平没任何关联,无论你能否全部答出,都不要对自己产生任何正 ...
- UWP:使用Composition实现类似安卓的水波纹Ripple效果
先放效果图: 首先,建立一个RippleHelper.cs文件,然后建立以下附加属性: IsFillEnable:是否扩大到整个控件 RippleDuration:持续时间 RippleRadius: ...
- border-image的拉伸和平铺
<!doctype html> <html> <head> <meta charset="utf-8"> <title> ...