内容指引

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之新增用户的更多相关文章

  1. 测试驱动开发实践4————testSave之新增文档分类

    [内容指引] 1.确定"新增文档分类"的流程及所需的参数 2.根据业务规则设计测试用例 3.为测试用例赋值并驱动开发 一.确定"新增文档分类"的流程及所需的参数 ...

  2. 测试驱动开发实践5————testSave之修改文档分类

    [内容指引] 1.确定"修改文档分类"的微服务接口及所需的参数 2.设计测试用例及测试用例合并 3.为测试用例赋值并驱动开发 上一篇我们通过17个测试用例完成了"新增文档 ...

  3. 测试驱动开发实践2————从testList开始

    内容指引 1.测试之前 2.改造dto层代码 3.改造dao层代码 4.改造service.impl层的list方法 5.通过testList驱动domain领域类的修改 一.测试之前 在" ...

  4. 测试驱动开发实践 - Test-Driven Development(转)

    一.前言 不知道大家有没听过“测试先行的开发”这一说法,作为一种开发实践,在过去进行开发时,一般是先开发用户界面或者是类,然后再在此基础上编写测试. 但在TDD中,首先是进行测试用例的编写,然后再进行 ...

  5. 测试驱动开发实践 - Test-Driven Development

    一.前言 不知道大家有没听过“测试先行的开发”这一说法,作为一种开发实践,在过去进行开发时,一般是先开发用户界面或者是类,然后再在此基础上编写测试. 但在TDD中,首先是进行测试用例的编写,然后再进行 ...

  6. 测试驱动开发实践3————从testList开始

    [内容指引] 运行单元测试: 装配一条数据: 模拟更多数据测试列表: 测试无搜索列表: 测试标准查询: 测试高级查询. 一.运行单元测试 我们以文档分类(Category)这个领域类为例,示范如何通过 ...

  7. 测试驱动开发实践—从testList开始

    [内容指引]运行单元测试:装配一条数据:模拟更多数据测试列表:测试无搜索列表:测试标准查询:测试高级查询. 一.运行单元测试 我们以文档分类(Category)这个领域类为例,示范如何通过编写测试用例 ...

  8. TDD(测试驱动开发)培训录

    2014年我一直从事在敏捷实践咨询项目,这也是我颇有收获的一年,特别是咨询项目的每一点改变,不管是代码质量的提高,还是自组织团队的建设,都能让我们感到欣慰.涉及人的问题都是复杂问题,改变人,改变一个组 ...

  9. TDD(测试驱动开发)培训录(转)

    本文转载自:http://www.cnblogs.com/whitewolf/p/4205761.html 最近也在了解TDD,发现这篇文章不错,特此转载一下. TDD(测试驱动开发)培训录 2015 ...

随机推荐

  1. 【mysql】连接查询

  2. Oracle 存储过程中的 =>

    oracle实参与形参有两种对应方式1.一种是位置方式,和面向对象语言参数传递类似;2.另外一种是=> 作为形参对应,因为位置对应方法有缺限,比如一个函数有3个参数,但第2个是可以不传(有默认值 ...

  3. freemarker处理哈希表的内建函数

    freemarker处理哈希表的内建函数 1.简易说明 (1)map取值 (2)key取值 2.实现示例 <html> <head> <meta http-equiv=& ...

  4. STM32f4 ARM Bootloader

    参考资料: 基于ARM 的嵌入式系统Bootloader 启动流程分析 Bootloader 详解 ( 代码环境 | ARM 启动流程 | uboot 工作流程 | 架构设计) Android系统启动 ...

  5. Linux之网络管理

    一.网络基础 1)ISO/OSI七层模型简介 ISO:国际标准化组织 OSI:开放系统互联模型 IOS:苹果操作系统(在计算机网络中,IOS是互联网操作系统,是思科公司为其网络设备开发的操作维护系统) ...

  6. Spring的Bean有哪些作用域?

    Spring的Bean有以下五种作用域: 1.singleton:SpringIOC容器只会创建该Bean的唯一实例: 2.prototype:每次请求都创建一个实例: 3.requset:每次HTT ...

  7. AC自动机模板2(【CJOJ1435】)

    题面 Description 对,这就是裸的AC自动机. 要求:在规定时间内统计出模版字符串在文本中出现的次数. Input 第一行:模版字符串的个数N. 第2->N+1行:N个字符串.(每个模 ...

  8. 【BZOJ1207】【HNOI2004】打鼹鼠(动态规划)

    [BZOJ1207][HNOI2004]打鼹鼠 题面 BZOJ题面 题解 考虑到m的范围只有10000 O(m^2)的复杂度是可以接受的 所以直接暴力DP 每次枚举前面出现的鼹鼠 检查是否能够转移过来 ...

  9. [BZOJ1878] [SDOI2009] HH的项链 (树状数组)

    Description HH有一串由各种漂亮的贝壳组成的项链.HH相信不同的贝壳会带来好运,所以每次散步 完后,他都会随意取出一段贝壳,思考它们所表达的含义.HH不断地收集新的贝壳,因此, 他的项链变 ...

  10. [BZOJ1207] [HNOI2004] 打鼹鼠 (dp)

    Description 鼹鼠是一种很喜欢挖洞的动物,但每过一定的时间,它还是喜欢把头探出到地面上来透透气的.根据这个特点阿Q编写了一个打鼹鼠的游戏:在一个n*n的网格中,在某些时刻鼹鼠会在某一个网格探 ...