springboot使用单元测试
springboot引入单元测试很简单,引入以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
一般创建项目的时候会自动引入,如果没有就手动引入。
一、Service层单元测试
Spring Boot中单元测试类写在在src/test/java目录下,你可以手动创建具体测试类。
package com.test.zmw;
import com.test.zmw.bean.User;
import com.test.zmw.service.UserService;
import org.junit.Assert;
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 static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ZmwApplicationTests {
@Autowired
private UserService userService;
@Test
public void simple(){
User user = userService.findById(37);
Assert.assertThat(user.getName(),is("mine"));
}
}
上面就是最简单的单元测试写法,顶部只要@RunWith(SpringRunner.class)和SpringBootTest即可,这里使用了assertThat断言,下面会介绍,也推荐大家使用该断言。
二、Controller层单元测试
上面只是针对Service层做测试,但是有时候需要对Controller层(API)做测试,这时候就得用到MockMvc了,你可以不必启动工程就能测试这些接口。
MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。
Controller类:
package com.test.zmw.controller;
import com.test.zmw.bean.PageVo;
import com.test.zmw.bean.User;
import com.test.zmw.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@RestController
@Api(value = "用户管理")
public class UserController {
@Autowired
private HttpServletRequest request;
@Autowired
UserService userService;
@ApiOperation(value = "添加用户",notes = "根据user对象添加用户")
@PostMapping(value = "/addUser")
public User addUser(@RequestBody User user){
return userService.saveUser(user);
}
@ApiOperation(value = "获取用户信息分页显示",notes = "根据分页实体对象获取用户信息及分页显示")
@PostMapping(value = "/PageLikeUser")
public Page<User> getPageUser(@RequestBody PageVo pageVo){
return userService.getPageUser(pageVo);
}
@ApiOperation(value = "获取用户信息",notes = "根据url的id获取用户信息")
@GetMapping(value = "/getOne/{id}")
public User getOneUser(@PathVariable int id){
return userService.findById(id);
}
@ApiOperation(value = "更新用户信息",notes = "根据user对象更新用户信息")
@PutMapping(value = "/updatUser")
public User updateOneUser(@RequestBody User user){
return userService.updateUser(user);
}
@ApiOperation(value = "删除用户",notes = "根据url的id删除用户")
@DeleteMapping(value = "/deleteUser/{id}")
public Integer deleteUser(@PathVariable int id){
return userService.deleteUserById(id);
}
}
测试类:
package com.test.zmw;
import com.alibaba.fastjson.JSONObject;
import com.test.zmw.bean.User;
import com.test.zmw.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpMethod;
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.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class ZmwApplicationTests {
@Autowired
private UserService userService;
@Autowired
private MockMvc mockMvc;
@Test
public void controllerTest1() throws Exception {
User user = new User();
user.setName("叶枫");
user.setDept(3);
user.setGroup(2);
user.setMobile("12345678911");
user.setBirthday(new Date());
String json = JSONObject.toJSON(user).toString();
mockMvc.perform(MockMvcRequestBuilders.post("/addUser")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.content(json))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
@Test
public void controllerTest2() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/getOne/72")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("小兔"))
.andExpect(MockMvcResultMatchers.jsonPath("$.mobile").value("18888888888"))
.andDo(MockMvcResultHandlers.print());
}
}
@AutoConfigureMockMvc:该注解表示启动测试的时候自动注入 MockMvc,而这个MockMvc有以下几个基本的方法:
mockMvc.perform
执行一个请求MockMvcRequestBuilders.get(“/getOne/72”)
构造一个请求,Post请求就用.post
方法contentType(MediaType.APPLICATION_JSON_UTF8)
代表发送端发送的数据格式是application/json;charset=UTF-8accept(MediaType.APPLICATION_JSON_UTF8)
代表客户端希望接受的数据类型为application/json;charset=UTF-8ResultActions.andExpect
添加执行完成后的断言ResultActions.andExpect(MockMvcResultMatchers.status().isOk())
方法看请求的状态响应码是否为200,如果不是则抛异常,测试不通过ResultActions.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("小兔"))
这里jsonPath用来获取name字段比对是否为小兔,不是就测试不通过ResultActions.andDo
添加一个结果处理器,表示要对结果做点什么事情,比如此处用MockMvcResultHandlers.print()
输出整个响应结果信息
测试结果1:
测试结果2:
三、异常单元测试
在JUnit4后支持下面的写法 :
在@Test注解内提供了expected属性,你可以用它来指定一个Throwble类型,如果方法调用中抛出了这个异常,那么这条测试用例就相当于通过了
@Test(expected = AssertionError.class)//抛出指定异常则测试通过
public void exceptionTest() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/getOne/72")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("张三"))
.andExpect(MockMvcResultMatchers.jsonPath("$.mobile").value("18888888888"))
.andDo(MockMvcResultHandlers.print());
}
但是,异常会在方法的某个位置被抛出,但不一定在特定的某行。
四、打包测试
如果将所有的测试方法写在同一个测试类中,不仅类代码太多,找某个功能点的测试方法也不太方便,这样一来,要跑完所有的测试方法就必须一个个测试类来跑测试。
Junit提供了一个打包运行所有测试类的方法,类里面不需要写代码。比如:
@RunWith(Suite.class)
@SpringBootTest
@Suite.SuiteClasses({
ExampleErrorTest.class,
ExampleControllerTest.class
})
public class MainExampleTest {
}
五、单元测试回滚
单元个测试的时候如果不想造成垃圾数据,可以开启事物功能,记在方法或者类头部添加@Transactional
注解即可,如下:
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@Transactional//开启事务动能
public class ZmwApplicationTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserService userService;
@Test
@Rollback
public void test1() throws Exception {
User user = new User();
user.setName("叶枫");
user.setDept(3);
user.setGroup(2);
user.setMobile("12345678911");
user.setBirthday(new Date());
String json = JSONObject.toJSON(user).toString();
mockMvc.perform(MockMvcRequestBuilders.post("/addUser")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.content(json))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
}
这样测试完数据就会回滚了,不会造成垃圾数据。如果你想关闭回滚,只要加上@Rollback(false)
注解即可。@Rollback
表示事务执行完回滚,支持传入一个参数value,默认true即回滚,false不回滚。
注:如果你使用的数据库是Mysql,有时候会发现加了注解@Transactional
也不会回滚,那么你就要查看一下你的默认引擎是不是InnoDB,如果不是就要改成InnoDB。
六、新断言assertThat
JUnit 4.4 结合 Hamcrest 提供了一个全新的断言语法——assertThat。程序员可以只使用 assertThat 一个断言语句,结合 Hamcrest 提供的匹配符,就可以表达全部的测试思想。
1、assertThat的基本语法
assertThat( [value], [matcher statement] );
- value 是接下来想要测试的变量值;
matcher statement
是使用Hamcrest
匹配符来表达的对前面变量所期望的值的声明,如果value
值与matcher statement
所表达的期望值相符,则测试成功,否则测试失败。
2、assertThat的优点
- 优点1:以前JUnit提供了很多的assertion语句,如:
assertEquals,assertNotSame,assertFalse,assertTrue,assertNotNull,assertNull
等,现在有了JUnit4.4,一条assertThat
即可替代所有的assertion
语句,这样可以在所有的单元测试中只使用一个断言方法,使得编写单元测试用例变得简单,代码风格变的统一,测试代码也更容易维护。 - 优点2:
assertThat
使用了Hamcrest
的Matcher
匹配符,用户可以使用匹配符规定的匹配标准精确的指定一些想设定满足的条件,具有很强的易读性,而且使用起来更加灵活。如:
使用匹配符Matcher和不使用之间的比较:
// 想判断某个字符串 s 是否含有子字符串 "developer" 或 "Works" 中间的一个
// JUnit 4.4 以前的版本:assertTrue(s.indexOf("developer")>-1||s.indexOf("Works")>-1 );
// JUnit 4.4:
assertThat(s, anyOf(containsString("developer"), containsString("Works")));
// 匹配符 anyOf 表示任何一个条件满足则成立,类似于逻辑或 "||", 匹配符 containsString 表示是否含有参数子
// 字符串,文章接下来会对匹配符进行具体介绍
- 优点3:
assertThat
不再像assertEquals
那样,使用比较难懂的“谓主宾”的语法模式(如:assertEquals(3,x);
),相反,assertThat
使用了类似于“主谓宾”的易读语法模式(如:assertThat(x,is(3);
),使得代码更加直观、易读。 - 优点4:可以将这些
Matcher
匹配符联合起来灵活使用,达到更多的目的。如:
// 联合匹配符not和equalTo表示“不等于”
assertThat( something, not( equalTo( "developer" ) ) );
// 联合匹配符not和containsString表示“不包含子字符串”
assertThat( something, not( containsString( "Works" ) ) );
// 联合匹配符anyOf和containsString表示“包含任何一个子字符串”
assertThat(something, anyOf(containsString("developer"), containsString("Works")));
- 优点5:错误信息更加易懂、可读且具有描述性(descriptive)
JUnit 4.4 以前的版本默认出错后不会抛出额外提示信息,如:
assertTrue( s.indexOf("developer") > -1 || s.indexOf("Works") > -1 );
如果该断言出错,只会抛出无用的错误信息,如:junit.framework.AssertionFailedError:null
。
如果想在出错时想打印出一些有用的提示信息,必须得程序员另外手动写,如:
assertTrue( "Expected a string containing 'developer' or 'Works'",
s.indexOf("developer") > -1 || s.indexOf("Works") > -1 );
非常的不方便,而且需要额外代码。
JUnit 4.4 会默认自动提供一些可读的描述信息,如:
String s = "hello world!";
assertThat( s, anyOf( containsString("developer"), containsString("Works") ) );
// 如果出错后,系统会自动抛出以下提示信息:
java.lang.AssertionError:
Expected: (a string containing "developer" or a string containing "Works")
got: "hello world!"
3、如何使用assertThat
JUnit 4.4 自带了一些 Hamcrest
的匹配符 Matcher
,但是只有有限的几个,在类 org.hamcrest.CoreMatchers
中定义,要想使用他们,必须导入包 org.hamcrest.CoreMatchers.*
。
assertThat
大部分使用例子:
@Test
public void numberTest(){
/**数值匹配**/
//测试变量是否大于指定值
assertThat(50,greaterThan(49));
//测试变量值是否小于指定值
assertThat(50,lessThan(100));
//测试变量是否大于等于指定值
assertThat(50,greaterThanOrEqualTo(50));
//测试变量是否小于等于指定值
assertThat(50,lessThanOrEqualTo(100));
//测试所有条件必须成立
assertThat(50,allOf(greaterThan(20),lessThan(51)));
//测试只要有一个条件成立
assertThat(50,anyOf(greaterThan(20),lessThan(50)));
//测试变量是否在指定范围之内
assertThat(26.9,closeTo(20,30));
//测试无论什么条件都成立
assertThat("",anything());
//测试变量等于指定值
assertThat(100,is(100));
//测试变量不等于指定值
assertThat(100,not(4564654));
}
@Test
public void stringTest(){
/**字符串匹配**/
String url = "http://www.taobao.com";
//测试变量是否包含指定字符串
assertThat(url,containsString("taobao"));
//测试变量是否以指定字符串开头
assertThat(url,startsWith("http://"));
//测试变量是否以指定字符串结尾
assertThat(url,endsWith("com"));
//测试变量是否等于指定字符串
assertThat(url,equalTo("http://www.taobao.com"));
//测试变量在忽略大小写的情况下是是否等于指定字符串
assertThat(url,equalToIgnoringCase("HTTP://WWW.taoBAO.com"));
//测试变量在忽略头尾任意空格的情况下是否等于指定字符串
assertThat(url,equalToIgnoringWhiteSpace(" HTTP://WWW.taoBAO.com "));
//测试变量的值为null
assertThat(null,nullValue());
//测试变量的值不为null
assertThat(url,notNullValue());
//is匹配符简写应用之一,is(equalTo(x))的简写
assertThat(url,is("http://www.taobao.com"));
User user = new User();
//is匹配符简写应用之二,is(instanceOf(SomeClass.class))的简写
//测试user是否为User的实例
assertThat(user,is(User.class) );
assertThat(user,isA(User.class) );
}
@Test
public void collectionTest(){
/**集合匹配**/
User user1 = new User("张三",new Date(),"16666666666",3,2);
User user2 = new User("秦羽",new Date(),"12345678911",2,3);
List<User> userList = new ArrayList<>();
userList.add(user1);
userList.add(user2);
//测试集合中是否含有指定元素
assertThat(userList,hasItem(user1));
assertThat(userList,hasItem(user2));
Map<String,User> userMap = new HashMap<>();
userMap.put(user1.getName(),user1);
userMap.put(user2.getName(),user2);
//测试map中是否含有指定键值对
assertThat(userMap,hasEntry("张三",user1));
//测试map中是否含有指定键
assertThat(userMap,hasKey("秦羽"));
//测试map中是否含有指定值
assertThat(userMap,hasValue(user2));
}
七、单元测试与重构的关系
- 什么是重构?
重构的目的是为了保证现有模块功能不改变的前提下,使代码更加清晰,简洁,更好扩展。 - 为什么重构?
我们在开发过程中很难一直保证遵守设计原则,有很多原因:赶进度,我们不可避免的减少了对设计的思考,而为了更快(短期看起来快,破坏了长期利益)的完成开发任务,我们抛弃了设计原则。又或者我们很难在设计的初期预见到未来的变化,这时我们就需要重构这一法宝。 - 强大的护盾–单元测试。
- 为了能够确保,重构没有破坏现有的代码功能,我们需要单元测试这个护盾来确保重构没有造成破坏。
- 单元测试,最美妙的事情不是在所有的测试实例通过后的那份心情,而是它给了你大刀阔斧进行重构的自由。
- 重构完之后,想看看提高了多少性能,单元测试运行时间就可以告诉你。
- 没有单元测试,何谈重构!