前言 尝试使用Spring MVC来实现一组对User对象操作的RESTful API,配合注释详细说明在Spring MVC中如何映射HTTP请求、如何传参、如何编写单元测试。
一、构建Restful API
1.创建子模块 1.1 创建项目 这里我们创建一个项目
1 2 group = 'com.ray.study' artifact ='spring-boot-02-restful-test'
1.2 引入依赖 在父工程spring-boot-seeds
的 settings.gradle
加入子工程
1 2 3 rootProject.name = 'spring-boot-seeds' include 'spring-boot-01-helloworld' include 'spring-boot-02-restful-test'
这样,子工程spring-boot-02-restful-test
就会自动继承父工程中subprojects
`函数里声明的依赖,主要包含如下依赖:
1 2 3 4 5 implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'
2.RESTful API
2.1 RESTful API
设计
方法名
请求类型
URL
功能说明
list
GET
/users/
查询用户列表
get
GET
/users/{id}
根据id查询用户
insert
POST
/users/
新增用户
update
PUT
/users/
更新用户
delete
DELETE
/users/{id}
根据id删除用户
2.2 RESTful API
实现 2.2.1 User 实体类如下
1 2 3 4 5 6 7 8 9 10 11 @Data @NoArgsConstructor @AllArgsConstructor public class User { private Long id; private String name; private Integer age; }
2.2.2 UserService
(1)UserService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public interface UserService { List<User> list () ; User get (Long id) ; User insert (User user) ; User update (User user) ; boolean delete (Long id) ; }
(2)UserServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Service public class UserServiceImpl implements UserService { private static Map<Long, User> users = Collections.synchronizedMap(new HashMap <>()); @Override public List<User> list () { return new ArrayList <>(users.values()); } @Override public User get (Long id) { return users.get(id); } @Override public User insert (User user) { users.put(user.getId(), user); return user; } @Override public User update (User user) { User u = users.get(user.getId()); u.setName(user.getName()); u.setAge(user.getAge()); users.put(user.getId(), u); return u; } @Override public boolean delete (Long id) { users.remove(id); return true ; } }
2.2.2 UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 @RestController @RequestMapping(value = "/users") public class UserController { @Autowired UserService userService; @GetMapping("/") public List<User> list () { return userService.list(); } @GetMapping("/{id}") public User get (@PathVariable Long id) { return userService.get(id); } @PostMapping("/") public User insert (@RequestBody User user) { return userService.insert(user); } @PutMapping("/") public User update (@RequestBody User user) { return userService.update(user); } @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) public String delete (@PathVariable Long id) { boolean result = userService.delete(id); return "success" ; } }
二、单元测试 关于单元测试,一般做service层测试验证逻辑的正确性即可,如若需要,也可做controller层测试。
1. service
层测试 1.1 Junit进行service层测试 使用Junit进行service层测试
service
层测试主要使用Junit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 import com.ray.study.springboot02.restfultest.model.User;import com.ray.study.springboot02.restfultest.service.UserService;import org.junit.Test;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 java.util.List;import static org.hamcrest.CoreMatchers.notNullValue;import static org.hamcrest.MatcherAssert.assertThat;import static org.hamcrest.Matchers.is;import static org.hamcrest.core.IsEqual.equalTo;import static org.hamcrest.core.IsNull.nullValue;@RunWith(SpringRunner.class) @SpringBootTest public class UserServiceImplTest { @Autowired UserService userService; @Test public void addUser () { User user = new User (); user.setId(1L ); user.setName("张三" ); user.setAge(23 ); user = userService.insert(user); assertThat(user.getId(),is(notNullValue())); } @Test public void testAll () { User user = new User (); user.setId(1L ); user.setName("张三" ); user.setAge(23 ); user = userService.update(user); assertThat(user.getId(),is(notNullValue())); assertThat(user.getName(), equalTo("张三" )); List<User> userList= userService.list(); assertThat(userList.size(),is(1 )); User user1 = userService.get(user.getId()); assertThat(user1.getName(),equalTo("张三" )); user.setName("李四" ); userService.update(user); User user2 = userService.get(user.getId()); assertThat(user2.getName(), equalTo("李四" )); userService.delete(user.getId()); User user3 = userService.get(user.getId()); assertThat(user3, is(nullValue())); } }
1.2 使用Mockito进行Service层测试 参考:
2. controller
层测试 controler
层的测试主要使用MockMvc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 import com.fasterxml.jackson.databind.ObjectMapper;import com.ray.study.springboot02.restfultest.model.User;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.http.MediaType;import org.springframework.test.context.junit4.SpringRunner;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.RequestBuilder;import org.springframework.test.web.servlet.setup.MockMvcBuilders;import org.springframework.web.context.WebApplicationContext;import static org.hamcrest.core.IsEqual.equalTo;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@RunWith(SpringRunner.class) @SpringBootTest public class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mvc; private ObjectMapper objectMapper = new ObjectMapper (); @Before public void setUp () throws Exception { mvc = MockMvcBuilders.webAppContextSetup(wac).build(); } @Test public void getUserList () throws Exception { RequestBuilder request = get("/users/" ); mvc.perform(request) .andExpect(status().isOk()); } @Test public void postUser () throws Exception { User user = new User (1L , "测试大师" , 20 ); String postJson = objectMapper.writeValueAsString(user); RequestBuilder request = post("/users/" ) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(postJson); mvc.perform(request) .andExpect(status().isOk()) .andExpect(jsonPath("$.name" ).value("测试大师" )) .andDo(print()); } @Test public void testAll () throws Exception { RequestBuilder request; request = get("/users/" ); mvc.perform(request) .andExpect(status().isOk()) .andExpect(content().string(equalTo("[]" ))); User user = new User (1L , "测试大师" , 20 ); String postJson = objectMapper.writeValueAsString(user); request = post("/users/" ) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(postJson); mvc.perform(request) .andExpect(status().isOk()) .andExpect(jsonPath("$.name" ).value("测试大师" )) .andDo(print()); request = get("/users/" ); mvc.perform(request) .andExpect(status().isOk()) .andExpect(jsonPath("$.length()" ).value(1 )); User user1 = new User (1L , "测试大师1" , 21 ); String postJson1 = objectMapper.writeValueAsString(user1); request = put("/users/" ) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(postJson1); mvc.perform(request) .andExpect(status().isOk()) .andExpect(jsonPath("$.name" ).value("测试大师1" )) .andDo(print()); request = get("/users/1" ); mvc.perform(request) .andExpect(content().string(equalTo(postJson1))); request = delete("/users/1" ); mvc.perform(request) .andExpect(status().isOk()); request = get("/users/" ); mvc.perform(request) .andExpect(status().isOk()) .andExpect(content().string(equalTo("[]" ))); } }
三、单元测试总结 1.Hamcrest
JUnit 4.4 结合 Hamcrest 提供了一个全新的断言语法——assertThat。程序员可以只使用 assertThat 一个断言语句,结合 Hamcrest 提供的匹配符,就可以表达全部的测试思想.
关于 Hamcrest
可参见:
1.1 常见匹配器 1.1.1 Core
anything
- always matches, useful if you don’t care what the object under test is
describedAs
- decorator to adding custom failure description
is
- decorator to improve readability - see “Sugar”, below
1.1.2 Logical
allOf
- matches if all matchers match, short circuits (like Java &&)
anyOf
- matches if any matchers match, short circuits (like Java || )
not
- matches if the wrapped matcher doesn’t match and vice versa
1.1.3 Object
equalTo
- test object equality using Object.equals
hasToString
- test Object.toString
instanceOf
, isCompatibleType
- test type
notNullValue
, nullValue
- test for null
sameInstance
- test object identity
1.1.4 Beans
hasProperty
- test JavaBeans properties
1.1.5 Collections
array
- test an array’s elements against an array of matchers
hasEntry
, hasKey
, hasValue
- test a map contains an entry, key or value
hasItem
, hasItems
- test a collection contains elements
hasItemInArray
- test an array contains an element
1.1.6 Number
closeTo
- test floating point values are close to a given value
greaterThan
, greaterThanOrEqualTo
, lessThan
, lessThanOrEqualTo
- test ordering
1.1.7 Text
equalToIgnoringCase
- test string equality ignoring case
equalToIgnoringWhiteSpace
- test string equality ignoring differences in runs of whitespace
containsString
, endsWith
, startsWith
- test string matching
1.2 常见示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 package com.ray.study.springboot02.restfultest.service.impl;import com.ray.study.springboot02.restfultest.model.User;import org.junit.Test;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import static org.hamcrest.CoreMatchers.is;import static org.hamcrest.MatcherAssert.assertThat;import static org.hamcrest.Matchers.*;import static org.hamcrest.collection.IsMapContaining.hasEntry;import static org.hamcrest.collection.IsMapContaining.hasKey;import static org.hamcrest.core.IsCollectionContaining.hasItem;import static org.hamcrest.core.IsEqual.equalTo;import static org.hamcrest.core.IsNull.notNullValue;import static org.hamcrest.core.IsNull.nullValue;import static org.hamcrest.core.StringContains.containsString;import static org.hamcrest.number.IsCloseTo.closeTo;import static org.hamcrest.text.IsEqualIgnoringCase.equalToIgnoringCase;public class HamcrestTest { @Test public void testCore () { String name = "tomcat" ; int n = 100 ; double d = 100.12 ; assertThat(name, is("tomcat" )); assertThat(n, is(100 )); assertThat(d, is(100.12 )); assertThat(d, anything(name)); assertThat(d, anything()); } @Test public void testLogical () { double d = 3.35 ; assertThat(d, allOf(greaterThan(3.0 ), lessThan(3.5 ))); assertThat(d, anyOf(greaterThan(3.3 ), lessThan(3.2 ))); assertThat(d, is(not(3.3 ))); } @Test public void testObject () { User user = new User (); user.setId(1L ); assertThat(user.getId(),is(notNullValue())); assertThat(user.getName(), is(nullValue())); } @Test public void testText () { String name = "tomcat" ; assertThat(name, equalTo("tomcat" )); assertThat(name, equalToIgnoringCase("TomCat" )); assertThat(name, containsString("ca" )); assertThat(name, startsWith("tom" )); assertThat(name, endsWith("cat" )); } @Test public void testNumber () { double d = 3.35 ; assertThat(d, closeTo(3.0 , 0.5 )); assertThat(d, greaterThan(3.0 )); assertThat(d, lessThan(3.5 )); assertThat(d, greaterThanOrEqualTo(3.3 )); assertThat(d, lessThanOrEqualTo(3.4 )); } @Test public void testCollection () { User user1 = new User (1L , "tomcat" , 21 ); User user2 = new User (2L , "springboot" , 22 ); User user3 = new User (2L , "springboot" , 22 ); List<User> userList = new ArrayList <>(); userList.add(user1); userList.add(user2); Map<String, User> userMap = new HashMap <>(); userMap.put(user1.getName(), user1); userMap.put(user2.getName(), user2); assertThat(userList, hasItem(user1)); assertThat(userList, hasItem(user3)); assertThat(userList.size(), is(2 )); assertThat(userList,is(not(empty()))); assertThat(userMap, hasEntry(user1.getName(),user1)); assertThat(userMap, hasKey(user1.getName())); assertThat(userMap, hasValue(user1)); } }
2. MockMvc
2.1 入门示例 2.1.1 get 主要内容:
初始化MockMVC
构造get请求
执行请求
响应的期望
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package com.ray.study.springboot02.restfultest.controller;import com.fasterxml.jackson.databind.ObjectMapper;import org.junit.Before;import org.junit.Test;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.RequestBuilder;import org.springframework.test.web.servlet.setup.MockMvcBuilders;import org.springframework.web.context.WebApplicationContext;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;@RunWith(SpringRunner.class) @SpringBootTest public class MockMvcTest { @Autowired private WebApplicationContext wac; private MockMvc mvc; private ObjectMapper objectMapper = new ObjectMapper (); @Before public void setUp () throws Exception { mvc = MockMvcBuilders.webAppContextSetup(wac).build(); } @Test public void getUserList () throws Exception { RequestBuilder request = get("/users/" ); mvc.perform(request) .andExpect(status().isOk()); } }
2.1.2 post 示例1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void postUser () throws Exception { User user = new User (1L , "测试大师" , 20 ); String postJson = objectMapper.writeValueAsString(user); RequestBuilder request = post("/users/" ) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(postJson); mvc.perform(request) .andExpect(status().isOk()) .andExpect(jsonPath("$.name" ).value("测试大师" )) .andDo(print()); }
示例2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test public void postUser2 () throws Exception { User user = new User (1L , "测试大师" , 20 ); String postJson = objectMapper.writeValueAsString(user); RequestBuilder request = post("/users/" ) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(postJson); MvcResult mvcResult = mvc.perform(request) .andExpect(status().isOk()) .andExpect(jsonPath("$.name" ).value("测试大师" )) .andDo(print()) .andReturn(); MockHttpServletResponse response = mvcResult.getResponse(); int status = response.getStatus(); String contentAsString = response.getContentAsString(); }
2.1.3 put 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void putUser () throws Exception { User user1 = new User (1L , "测试大师1" , 21 ); String postJson1 = objectMapper.writeValueAsString(user1); RequestBuilder request = put("/users/" ) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(postJson1); mvc.perform(request) .andExpect(status().isOk()) .andExpect(jsonPath("$.name" ).value("测试大师1" )) .andDo(print()); request = get("/users/1" ); mvc.perform(request) .andExpect(content().string(equalTo(postJson1))); }
2.1.4 delete 1 2 3 4 5 6 7 8 9 10 11 12 @Test public void deleteUser () throws Exception { RequestBuilder request = delete("/users/1" ); mvc.perform(request) .andExpect(status().isOk()); request = get("/users/" ); mvc.perform(request) .andExpect(status().isOk()) .andExpect(content().string(equalTo("[]" ))); }
2.2 MockMvc
抽象 使用 MockMvc
可对 controller
层进行测试
MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。
既然要模拟Http请求,那么必然就会涉及到如下 http
元素:
2.3 处理响应 2.3.1 jsonPath
取值 具体用法可官方文档,很详细
操作符:
Operator
Description
$
根节点:The root element to query. This starts all path expressions.
@
当前节点:The current node being processed by a filter predicate.
*
通配符:Wildcard. Available anywhere a name or numeric are required.
..
Deep scan. Available anywhere a name is required.
.<name>
子节点:Dot-notated child
['<name>' (, '<name>')]
一个或多个子节点:Bracket-notated child or children
[<number> (, <number>)]
一个或多个数组下标:Array index or indexes
[start:end]
数组片段:Array slice operator
[?(<expression>)]
过滤器表达式:Filter expression. Expression must evaluate to a boolean value.
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 { "store" : { "book" : [ { "category" : "reference" , "author" : "Nigel Rees" , "title" : "Sayings of the Century" , "price" : 8.95 } , { "category" : "fiction" , "author" : "Evelyn Waugh" , "title" : "Sword of Honour" , "price" : 12.99 } , { "category" : "fiction" , "author" : "Herman Melville" , "title" : "Moby Dick" , "isbn" : "0-553-21311-3" , "price" : 8.99 } , { "category" : "fiction" , "author" : "J. R. R. Tolkien" , "title" : "The Lord of the Rings" , "isbn" : "0-395-19395-8" , "price" : 22.99 } ] , "bicycle" : { "color" : "red" , "price" : 19.95 } } , "expensive" : 10 }
JsonPath (click link to try)
Result
$.store.book[*].author
The authors of all books
$..author
All authors
$.store.*
All things, both books and bicycles
$.store..price
The price of everything
$..book[2]
The third book
$..book[-2]
The second to last book
$..book[0,1]
The first two books
$..book[:2]
All books from index 0 (inclusive) until index 2 (exclusive)
$..book[1:2]
All books from index 1 (inclusive) until index 2 (exclusive)
$..book[-2:]
Last two books
$..book[2:]
Book number two from tail
$..book[?(@.isbn)]
All books with an ISBN number
$.store.book[?(@.price < 10)]
All books in store cheaper than 10
$..book[?(@.price <= $['expensive'])]
All books in store that are not “expensive”
$..book[?(@.author =~ /.*REES/i)]
All books matching regex (ignore case)
$..*
Give me every thing
$..book.length()
The number of books
2.3.2 打印响应 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void postUser () throws Exception { User user = new User (1L , "测试大师" , 20 ); String postJson = objectMapper.writeValueAsString(user); RequestBuilder request = post("/users/" ) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(postJson); mvc.perform(request) .andExpect(status().isOk()) .andExpect(jsonPath("$.name" ).value("测试大师" )) .andDo(print()); }