(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); }
此时运行测试用例,会进入方法体。打印结果如下:
说明: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,输出如下:
四、其他
1、返回验证码说明:
400:请求格式错误,例如要求实参中必须有username,但实际没有。或者验证不通过
405:后台不支持method,例如get请求post的方法
2、JsonPath使用说明:
在测试中使用jsonpath很方便,推荐使用。github中搜索 jsonpath,结果如下https://github.com/json-path/JsonPath
3、Hibernate Validateor使用说明:
Hibernate Validateor网上也有很多api,哪位网友知道比较全的地址可以告诉一下,下面截图奉上部分。