(01)Restful风格的增删改查案例及其junit测试详解

  一、相关注解

@GetMapping:等价于@RequestMapping(method=RequestMethod.GET)

@PostMapping:等价于@RequestMapping(method=RequestMethod.POST)

@PutMapping:等价于@RequestMapping(method=RequestMethod.PUT)

@DeleteMapping:等价于@RequestMapping(method=RequestMethod.DELETE)

@RequestBody:映射请求体到java方法的参数

@RequestParam:映射请求参数到java方法的参数

@PageableDefault:指定分页参数默认值

@PathVariable: 映射url片段到java方法的参数

@JsonView:控制json输出内容,可以指定哪些字段显示

@Valid:验证字段是否合法

BindingResult:验证不合法时获取提示信息,@Valid注解和BingResult验证请求参数的合法性并处理校验结果

  二、演示增删查改

User.java

package com.edu.sl.dto;

import java.util.Date;

public class User {

    private String id;
    private String username;
    private String password;
    private Date birthDay;
    
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public Date getBirthDay() {
        return birthDay;
    }
    public void setBirthDay(Date birthDay) {
        this.birthDay = birthDay;
    }
}

UserController.java

package com.edu.sl.controller;

import org.springframework.web.bind.annotation.RestController;

@RestController@RequestMapping("/user")
public class UserController {
    ... ...
}

UserControllerTest.java

package com.edu.sl.controller;

import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {

    @Autowired
    private WebApplicationContext wac;
    
    private MockMvc mockMvc;
    
    @Before
    public void setUp(){
        mockMvc=MockMvcBuilders.webAppContextSetup(wac).build();
    }
   ... ...    
}

测试依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>

1、查询操作

使用@GetMapping注解,测试使用get方法。

1)不传递参数

@GetMapping
public List<User> query(){
  List<User> users = new ArrayList<User>();
  users.add(new User());
  users.add(new User());
  users.add(new User());
  return users;
}
@Test
public void query() throws Exception{
  String content=mockMvc.perform(MockMvcRequestBuilders.get("/user")
                   .contentType(MediaType.APPLICATION_JSON_UTF8))
                   .andExpect(MockMvcResultMatchers.status().isOk())
                   .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3))
                   .andReturn().getResponse().getContentAsString();
    System.out.println(content);
}

2)传递参数

@GetMapping("/a")public List<User> query2(String username,String password){
    System.out.println("username:"+username);
    System.out.println("password:"+password);
    List<User> users = new ArrayList<User>();
    users.add(new User());
    users.add(new User());
    users.add(new User());
    return users;
}
@Test
public void query() throws Exception{
    String content=mockMvc.perform(MockMvcRequestBuilders.get("/user/a")
                          .param("username","sl")
                          .param("password","123456")
                          .contentType(MediaType.APPLICATION_JSON_UTF8))
                          .andExpect(MockMvcResultMatchers.status().isOk())
                          .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3))
                          .andReturn().getResponse().getContentAsString();
    System.out.println(content);
}

3)@RequestParam注解

使用此注解,默认情况下传递的实参中必须含有它指定的参数名,否则报400。此注解含有4个属性可以改变默认情况。

defaultValue:如果没有实参,形参中默认使用的值。

name:起别名,默认实参、形参名字一致,使用name属性时形参可以随意命名,会映射过来。

required:是否必须,默认false必填 ,否则报400。

value:最终得到的值。

@GetMapping("/b")
public List<User> query3(@RequestParam(name="username",required=false,defaultValue="tom") String nikename){
    System.out.println("nikename:"+nikename);
    List<User> users = new ArrayList<User>();
    users.add(new User());
    users.add(new User());
    users.add(new User());
    return users;
}
@Test
public void query3() throws Exception{
    String content=mockMvc.perform(MockMvcRequestBuilders.get("/user/b")
                          .param("username","sl")
                          .contentType(MediaType.APPLICATION_JSON_UTF8))
                          .andExpect(MockMvcResultMatchers.status().isOk())
                          .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3))
                          .andReturn().getResponse().getContentAsString();
    System.out.println(content);
}

如果实参中有username这个属性,就取它的值赋给nikename,没有这个属性,nikename就取tom这个值

4)@PageableDefault注解

使用此注解指定分页参数默认值,配合Pageable使用,需要引入以下依赖

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-commons</artifactId>
</dependency>
@GetMapping("/c")
public List<User> query4(User user,@PageableDefault(page=10,size=20,sort="age,asc") Pageable pageable){
    System.out.println("page:"+pageable.getPageNumber());
    System.out.println("size:"+pageable.getPageSize());
    System.out.println("sort:"+pageable.getSort());
    List<User> users = new ArrayList<User>();
    users.add(new User());
    users.add(new User());
    users.add(new User());
    return users;
}
@Test
public void query4() throws Exception{
    String content=mockMvc.perform(MockMvcRequestBuilders.get("/user/c")
                          .param("username","sl")
                          .param("size","10")
                          .param("page","2")
                          .param("sort","username,desc")
                          .contentType(MediaType.APPLICATION_JSON_UTF8))
                          .andExpect(MockMvcResultMatchers.status().isOk())
                          .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3))
                          .andReturn().getResponse().getContentAsString();
    System.out.println(content);
}

如果实参中传递了size、page、sort参数,则实际使用传递的,如果实参中没有传递,则使用形参中默认的值,不会报错。

实参中的属性会自动匹配到形参的类的对象的属性上面,如username会自动匹配到实参中user中的username中。

5)@PathVariable注解

此注解把声明的url(value="/{id}")中的片段id的值作为参数传到java方法的id上

@GetMapping(value="/{id3}")
public User getUserInfo(@PathVariable("id3") String id){   System.out.println("id:"+id);//可以获取到
    User user=new User();
    user.setUsername("tom");
    return user;
}
@Test
public void getUserInfo() throws Exception{
    String content=mockMvc.perform(MockMvcRequestBuilders.get("/user/1")
    .contentType(MediaType.APPLICATION_JSON_UTF8))
    .andExpect(MockMvcResultMatchers.status().isOk())
    .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("tom"))
    .andReturn().getResponse().getContentAsString();
    System.out.println(content);
}

@PathVariable里面有name,value属性,作用一样,用来指定变量的名字,例如上例中id3会映射到id上。两个id3要保持一致。

大括号中的变量可以使用正则表达式,例如只希望id是整数,可以如下:

@GetMapping(value="/a/{id3:\\d+}")
public User getUserInfo2(@PathVariable("id3") String id){
    System.out.println("id:"+id);
    User user=new User();
    user.setUsername("tom");
    return user;
}
@Test
public void getUserInfo2() throws Exception{
    String content=mockMvc.perform(MockMvcRequestBuilders.get("/user/a/13")
    .contentType(MediaType.APPLICATION_JSON_UTF8))
    .andExpect(MockMvcResultMatchers.status().isOk())
    .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("tom"))
    .andReturn().getResponse().getContentAsString();
    System.out.println(content);
}
@Test
public void getUserInfo2_fail() throws Exception{
    mockMvc.perform(MockMvcRequestBuilders.get("/user/a/1.3")
    .contentType(MediaType.APPLICATION_JSON_UTF8))
    .andExpect(MockMvcResultMatchers.status().is4xxClientError());
}

如果将参数值13改为字母或者小数,报404。

6)@JsonView注解

分三步:用接口定义视图名称,在get方法上指定视图,在Controller方法上指定视图。

修改User类,做如下修改:

public interface UserSimpleView{};
public interface UserDetailView extends UserSimpleView{};

@JsonView(User.UserSimpleView.class)
public String getUsername() {
    return username;
}

@JsonView(User.UserDetailView.class)
public String getPassword() {
    return password;
}
@GetMapping("/d")
@JsonView(User.UserSimpleView.class)
public List<User> query5(){
    List<User> list=new ArrayList<User>();
    list.add(new User());
    list.add(new User());
    list.add(new User());
    return list;
}

@GetMapping("/b/{id:\\d+}")
@JsonView(User.UserDetailView.class)
public User getUserInfo3(@PathVariable("id") String id){
    User user=new User();
    user.setUsername("tom");
    return user;
@Test
public void query5() throws Exception{
    String content=mockMvc.perform(MockMvcRequestBuilders.get("/user/d")
                          .contentType(MediaType.APPLICATION_JSON_UTF8))
                          .andExpect(MockMvcResultMatchers.status().isOk())
                          .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3))
                          .andReturn().getResponse().getContentAsString();
    System.out.println(content);
}

@Test
public void getUserInfo3() throws Exception{
    String content=mockMvc.perform(MockMvcRequestBuilders.get("/user/b/13")
                          .contentType(MediaType.APPLICATION_JSON_UTF8))
                          .andExpect(MockMvcResultMatchers.status().isOk())
                          .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("tom"))
                          .andReturn().getResponse().getContentAsString();
    System.out.println(content);
}

从System输出的结果中可以看到,query5返回json只有一个username属性,getUserInfo3含有username和password属性。

  2、新增操作

使用@PostMapping注解,测试使用post方法。

1)不校验参数值

@PostMapping
public User create(@RequestBody User user){
    System.out.println(user.getUsername());
    System.out.println(user.getPassword());
    System.out.println(user.getId());
    System.out.println(user.getBirthDay());
    user.setId("1");
    return user;
}
@Test
public void whenCreateSuccess() throws Exception{
    String content="{\"username\":\"tom\",\"password\":123456}";
    String result=mockMvc.perform(MockMvcRequestBuilders.post("/user")
                         .contentType(MediaType.APPLICATION_JSON_UTF8)
                         .content(content))
                         .andExpect(MockMvcResultMatchers.status().isOk())
                         .andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))
                         .andReturn().getResponse().getContentAsString();
    System.out.println(result);
}

注意:必须加@RequestBody注解,否则接收不到参数值,输出的属性值都是null。

2)校验参数值

后端使用hibernate-validator.jar校验,Controller中方法添加@Valid注解,实体类添加需要验证的注解。

修改User类,做如下修改:

@NotBlank(message="密码不能为空")
private String password;
@Past(message="生日必须是过去的时间")
private Date birthDay;
@PostMapping("/a")
public User create2(@Valid @RequestBody User user){
    System.out.println(user.getUsername());
    System.out.println(user.getPassword());
    System.out.println(user.getId());
    System.out.println(user.getBirthDay());
    user.setId("1");
    return user;
}
@Test
public void create2() throws Exception{  //当前时间加上一年
    Date birthDay=new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    String content="{\"id\":\"1\",\"username\":\"tom\",\"password\":null,\"birthDay\":"+birthDay.getTime()+"}";
    String result=mockMvc.perform(MockMvcRequestBuilders.post("/user/a")
                         .contentType(MediaType.APPLICATION_JSON_UTF8)
                         .content(content))
                         .andExpect(MockMvcResultMatchers.status().isOk())
                         .andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))
                         .andReturn().getResponse().getContentAsString();
    System.out.println(result);
}

此时运行测试用例报400。不会进入方法体。

说明:字段上的验证注解与方法中@Valid注解同时使用才有效。

3)BindingResult类

在Controller中的方法里加上这个类,可以获取到验证不通过的提示信息。

@PostMapping("/b")
public User create3(@Valid @RequestBody User user,BindingResult errors){
    if(errors.hasErrors()){
        List<ObjectError> list=errors.getAllErrors();
        for(ObjectError error:list){
            System.out.println(((FieldError)error).getField()+" "+ error.getDefaultMessage());
        }
    }
    System.out.println(user.getUsername());
    System.out.println(user.getPassword());
    System.out.println(user.getId());
    System.out.println(user.getBirthDay());
    user.setId("1");
    return user;
}
@Test
public void create3() throws Exception{
    Date birthDay=new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    String content="{\"id\":\"1\",\"username\":\"tom\",\"password\":null,\"birthDay\":"+birthDay.getTime()+"}";
    String result=mockMvc.perform(MockMvcRequestBuilders.post("/user/b")
                         .contentType(MediaType.APPLICATION_JSON_UTF8)
                         .content(content))
                         .andExpect(MockMvcResultMatchers.status().isOk())
                         .andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))
                         .andReturn().getResponse().getContentAsString();
    System.out.println(result);
}

此时运行测试用例,会进入方法体。打印结果如下:

(01)Restful风格的增删改查案例及其junit测试详解

说明:BindingResult与@Valid注解同时使用才有效。

处理日期一般传递时间戳 new date().getTime(),因为js、app等都可以处理时间戳和日期的转换。

  3、修改操作

使用@PutMapping注解,测试使用put方法。

@Test
public void update() throws Exception{
    Date birthDay=new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    String content="{\"id\":\"1\",\"username\":\"tom\",\"password\":null,\"birthDay\":"+birthDay.getTime()+"}";
    String result=mockMvc.perform(MockMvcRequestBuilders.put("/user/1")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(content))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))
            .andReturn().getResponse().getContentAsString();
    System.out.println(result);
}
@Test
public void update() throws Exception{
    Date birthDay=new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    String content="{\"id\":\"1\",\"username\":\"tom\",\"password\":null,\"birthDay\":"+birthDay.getTime()+"}";
    String result=mockMvc.perform(MockMvcRequestBuilders.put("/user/1")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(content))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))
            .andReturn().getResponse().getContentAsString();
    System.out.println(result);
}

  4、删除操作

使用@DeleteMapping注解,测试使用delete方法。

@DeleteMapping("/{id:\\d+}")
public void delete(@PathVariable String id){
    System.out.println("id :"+id);
}
@Test
public void delete() throws Exception{
    mockMvc.perform(MockMvcRequestBuilders.delete("/user/1").contentType(MediaType.APPLICATION_JSON_UTF8))
    .andExpect(MockMvcResultMatchers.status().isOk());
}

三、自定义验证注解

虽然hibernate-validator提供了大量的校验注解,但有时仍不能满足我们的需求,这时就要自定义注解。

package com.edu.sl.validator;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)//指定注解使用的类
public @interface MyConstraint {
    String message();
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}
package com.edu.sl.validator;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.springframework.beans.factory.annotation.Autowired;

public class MyConstraintValidator implements ConstraintValidator<MyConstraint,Object> {
    
    @Override
    public void initialize(MyConstraint arg0) {
        System.out.println("校验器初始化方法"); 
    }

    @Override
    public boolean isValid(Object arg0, ConstraintValidatorContext arg1) {
        System.out.println("arg0 :"+arg0);
     //写校验逻辑
        return false;//校验失败
    }
}

自定义了一个注解和一个类,注解MyConstraint中三个属性是固定的。类MyConstraintValidator中的第二个泛型Object是可用类型,假如定义为String,只有在String类型的字段上可以使用该注解。自定义类中不需要加Component即可被Spring管理,可以注入其他bean用于校验逻辑。

直接运行测试用例update,输出如下:

(01)Restful风格的增删改查案例及其junit测试详解

四、其他

1、返回验证码说明:

400:请求格式错误,例如要求实参中必须有username,但实际没有。或者验证不通过
  405:后台不支持method,例如get请求post的方法

  2、JsonPath使用说明:

  在测试中使用jsonpath很方便,推荐使用。github中搜索 jsonpath,结果如下https://github.com/json-path/JsonPath

  3、Hibernate Validateor使用说明:

Hibernate Validateor网上也有很多api,哪位网友知道比较全的地址可以告诉一下,下面截图奉上部分。

(01)Restful风格的增删改查案例及其junit测试详解

(01)Restful风格的增删改查案例及其junit测试详解

相关推荐