内容指引

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

最新文章

  1. Vertica集群扩容实验过程记录
  2. python基础(八)面向对象的基本概念
  3. 显示图片的(自定义)吐司Toast
  4. centos7使用传统网卡名
  5. hdu4951 Multiplication table (乘法表的奥秘)
  6. 分布式并行数据库将在OLTP 领域促进去“Oracle”
  7. undefined和void
  8. MySQL 5.7 Zip 安装(win7)
  9. light oj 1297 Largest Box
  10. iOS开发技巧(系列十八:扩展UIColor,支持十六进制颜色设置)
  11. windows下apache如何完整卸载?
  12. 实现一个自己的promise
  13. 一个web应用的诞生(10)--关注好友
  14. 【MyBatis源码分析】insert方法、update方法、delete方法处理流程(下篇)
  15. k60引脚图
  16. asyncio异步IO--协程(Coroutine)与任务(Task)详解
  17. gogs 安装
  18. ffmpeg中av_log的实现分析
  19. Java中产生随机数的两个方法
  20. Getting started with C# and GDAL

热门文章

  1. 图像处理------Canny边缘检测
  2. Stanford Word Segmenter使用
  3. freemarker.template.TemplateException:Error executing macro:mainSelect
  4. iOS - Quartz 2D 画板绘制
  5. Log4j各级别日志重复打印
  6. canvas实现水波纹效果
  7. 【BZOJ3944】Sum(杜教筛)
  8. 【BZOJ4071】八邻旁之桥(线段树)
  9. Java数组的操作方法
  10. A brief introduction to weakly supervised learning(简要介绍弱监督学习)