1. Junit 的介绍
JUnit 的使用非常容易上手,测试类放在 src/main/test;下,然后类名一般用 Test 结尾
;
通常来说,一个测试类对应一个系统类,类中的每个方法,用于测试一个方法,方法一般以 test 开头,每个方法用 @Test 标注;
如果有测试开始前要执行的操作,用 @Before 标注一个方法来执行;如果有测试结束之后要执行的操作,用 @After 标注一个方法来执行,一般会将多个测试方法中的初始化和资源清理代码放在这些方法中;
每次运行一个方法,都会创建一个类的实例,将各个测试方法互相之间隔离开来,避免互相影响,导致测试失败;
在每个测试方法中,最后都会用 Assert 类来进行断言,判断测试是否通过,就是看一下测试得到的结果,是不是我们期望的,如果不是会报错,表示测试不通过;
JUnit 还有一个 Suite 的概念,一次性运行多个测试用例,算是一个测试套件,比如下面这样子:
@RunWith(Suite.class)
@SuiteClasses({TestSuite1.class, TestSuite2.class})
public class TestSuitMain {
}
我们一般都是,写好一些代码组件,然后就针对这些代码组件,去立即写对应的单元测试,单元测试是一个类一个类的执行
到了最后,你都要集成测试,mvn test,将所有的单元测试全部跑一遍;
2. spring-boot-starter-test
spring boot 对单元测试提供了很好的支持,只要依赖 spring-boot-starter-test 即可,这个依赖会自动导入单元测试需要的所有依赖,包括了 JUnit、AssertJ、Hamcrest 以及其他的一些包,而且这个 spring-boot-starter-test 一般是设置为 test scope 即可;
具体来说,spring-boot-starter-test 导入之后,会包含下面这些东西:
- JUnit:最经典的单元测试框架;
- Spring Test、Spring Boot Test:是 spring 和 spring boot 环境下,对测试的一个支持;
- AssertJ:是用来进行断言的;
- Hamcrest:是用来进行复杂断言,复杂的表达式;
- Mockito:测试替身的模拟;
- JSONassert:都是对 json 数据进行操作的;
-
3. 测试 spring boot 应用程序
3.1 springboot 单元测试脚手架
```java // @RunWith的意思,是不要使用默认方式进行单元测试,而是使用指定的类来提供单元测试 // 所有的spring测试都是找SpringRunner.class @RunWith(SpringRunner.class) // 这个是 spring boot 提供的,会一直找到一个 Application 类, // 只要包含了@SpringBootApplication 的就算,然后会先启动这个类,来给单元测试提供环境 @SpringBootTest public class EmployeeServiceTest {
// 这里就可以从启动的 spring 上下文中,将EmployeeService注入到这里来,供我们进行测试 @Autowired private EmployeeService employeeService;
@Test public void testFindById() {
}
}
<a name="EC4L2"></a>
### 3.2 测试 service 组件
要考虑的地方有3点:
- 测试之前自己构造好数据,测试结束之后自动回滚数据构造;
- 将 service 依赖的 dao 进行模拟打桩进来;
- 可能需要在数据库中构造好数据;
spring boot 会默认在单元测试结束之后,进行数据库事务回滚;<br />spring boot 会集成 mockito 框架来模拟依赖 dao 进行打桩;
```java
/*
* 这个注解是说,在执行单元测试的时候,不是直接去执行里面的单元测试的方法
* 因为那些方法执行之前,是需要做一些准备工作的,它是需要先初始化一个spring容器的
* 所以得找这个SpringRunner这个类,来先准备好spring容器,再执行各个测试方法
*/
@RunWith(SpringRunner.class)
/*
* 这个是说,会从最顶层的包结构开始招,com.zhss.springboot
* 找到一个标注了@SpringBootApplication注解的一个类,算是启动类
* 然后会执行这个启动类的main方法,就可以创建spring容器,给后面的单元测试提供完整的这个环境
*/
@SpringBootTest
public class UserServiceImplTest {
/**
* 用户管理模块的service组件
*/
@Autowired
private UserService userService;
/**
* 这里加了@MockBean的注解
* 就代表了说,这个UserDAO就不会用我们定义的那个userDAO了
* 这里会由spring boot整合mockito框架,然后创建一个实现了UserDAO接口的匿名实现类
* 然后将这个模拟出来实现了UserDAO接口的类的实例bean,放入spring容器中
* 替代我们自己的那个UserDAO
*/
@MockBean
private UserDAO userDAO;
/**
* 测试用例:查询所有用户信息
*/
@Test
public void testListUsers() {
// 准备好mock userDAO的返回数据
List<User> users = new ArrayList<User>();
User user = new User();
user.setId(1L);
user.setName("测试用户");
user.setAge(20);
users.add(user);
// 对userDAO进行mock逻辑设置
when(userDAO.listUsers()).thenReturn(users);
// 测试UserSerivce的listUsers()方法
List<User> resultUsers = userService.listUsers();
// 对测试结果进行断言
assertEquals(users, resultUsers);
}
/**
* 测试用例:根据ID查询一个用户
*/
@Test
public void testGetUserById() {
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setName("测试用户");
user.setAge(20);
when(userDAO.getUserById(userId)).thenReturn(user);
User resultUser = userService.getUserById(userId);
assertEquals(user, resultUser);
}
/**
* 测试用例:新增用户
*/
@Test
public void testSaveUser() {
Long userId = 1L;
User user = new User();
user.setName("测试用户");
user.setAge(20);
when(userDAO.saveUser(user)).thenReturn(userId);
Long resultUserId = userService.saveUser(user);
assertEquals(userId, resultUserId);
}
/**
* 测试用例:修改用户
*/
@Test
public void testUpdateUser() {
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setName("测试用户");
user.setAge(20);
when(userDAO.updateUser(user)).thenReturn(true);
Boolean updateResult = userService.updateUser(user);
assertTrue(updateResult);
}
/**
* 测试用例:删除用户
*/
@Test
public void testRemoveUser() {
Long userId = 1L;
when(userDAO.removeUser(userId)).thenReturn(true);
Boolean removeResult = userService.removeUser(userId);
assertTrue(removeResult);
}
}
3.3 测试 dao 组件
/**
* 用户管理模块的DAO组件的单元测试类
*
* 单元测试尽量不要依赖外部,但是直到最后一层的时候,DAO层的时候,跟redis,rabbitmq打交道
* 还是要依靠开发环境里的基础设施,来进行单元测试
*
* @author zhonghuashishan
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
/*
* @Transactional注解加了之后,就可以让每个方法都是放在一个事务里面
* 接着是这样子,比如我们要执行一些数据库相关的操作,比如说测试mapper,是需要实际操作数据库的
* 那么在测试查询操作的时候,我们需要通过程序预先灌入一些数据,再测试能否查询到这些数据
* 接着测试完之后,其实应该让这个事务要回滚,就可以自动取消我们插入的那些数据了
* 让单元测试方法执行的这些增删改的操作,都是一次性的
*/
@Transactional
@Rollback(true)
public class UserDAOImplTest {
/**
* 用户管理模块的DAO组件
*/
@Autowired
private UserDAO userDAO;
/**
* 测试用例:查询所有用户信息
*/
@Test
public void testListUsers() {
// 准备好mock userMapper的返回数据
User user = new User();
user.setName("测试用户");
user.setAge(20);
userDAO.saveUser(user);
List<User> users = new ArrayList<User>();
users.add(user);
// 测试UserSerivce的listUsers()方法
List<User> resultUsers = userDAO.listUsers();
// 对测试结果进行断言
assertEquals(users.size(), resultUsers.size());
}
/**
* 测试用例:根据ID查询一个用户
*/
@Test
public void testGetUserById() {
User user = new User();
user.setName("测试用户");
user.setAge(20);
userDAO.saveUser(user);
Long userId = user.getId();
User resultUser = userDAO.getUserById(userId);
assertEquals(user.toString(), resultUser.toString());
}
/**
* 测试用例:新增用户
*/
@Test
public void testSaveUser() {
User user = new User();
user.setName("测试用户");
user.setAge(20);
Long resultUserId = userDAO.saveUser(user);
assertThat(resultUserId, is(greaterThan(0L)));
}
/**
* 测试用例:修改用户
*/
@Test
public void testUpdateUser() {
Integer oldAge = 20;
Integer newAge = 21;
User user = new User();
user.setName("测试用户");
user.setAge(oldAge);
userDAO.saveUser(user);
user.setAge(newAge);
Boolean updateResult = userDAO.updateUser(user);
assertTrue(updateResult);
User updatedUser = userDAO.getUserById(user.getId());
assertEquals(newAge, updatedUser.getAge());
}
/**
* 测试用例:删除用户
*/
@Test
public void testRemoveUser() {
User user = new User();
user.setName("测试用户");
user.setAge(20);
userDAO.saveUser(user);
Boolean removeResult = userDAO.removeUser(user.getId());
assertTrue(removeResult);
}
}
spring boot 有一个 @Sql 注解,可以在测试开始前执行 SQL 语句初始化数据;
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
@Rollback(true)
public class EmployeeMapperTest {
@Autowired
private EmployeeMapper employeeMapper;
@Test
// 这个@Sql注解,就会在我们执行测试之前,先执行sql语句,初始化数据
@Sql({"employee.sql"})
public void testAddEmployee() {
Employee employee = new Employee();
employee.setName("李四");
employee.setAge(30);
Long employeeId = employeeService.add(employee);
employee.setId(employeeId);
assertTrue(employeeId > 0);
// 接着需要从数据库中查询数据来比较
Employee resultEmployee = employeeMapper.findById(1);
assertEquals(employee, resultEmployee);
}
}
3.4 测试 Controller 组件
/**
* 用户管理模块的控制器组件的单元测试类
* @author zhonghuashishan
*
*/
@RunWith(SpringRunner.class)
/*
* 通过这个注解表明,你要测试的controller是谁
*/
@WebMvcTest(UserController.class)
public class UserControllerTest {
/**
* 注入一个MockMvc,模拟对controller发起http请求
*/
@Autowired
private MockMvc mockMvc;
/**
* 模拟userService组件
*/
@MockBean
private UserService userService;
/**
* 测试用例:查询所有用户信息
*/
@Test
public void testListUsers() {
try {
List<User> users = new ArrayList<User>();
User user = new User();
user.setId(1L);
user.setName("测试用户");
user.setAge(20);
users.add(user);
when(userService.listUsers()).thenReturn(users);
mockMvc.perform(get("/api/v1.0/user/"))
.andExpect(content().json(JSONArray.toJSONString(users)));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 测试用例:根据ID查询一个用户
*/
@Test
public void testGetUserById() {
try {
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setName("测试用户");
user.setAge(20);
when(userService.getUserById(userId)).thenReturn(user);
mockMvc.perform(get("/api/v1.0/user/{id}", userId))
.andExpect(content().json(JSONObject.toJSONString(user)));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 测试用例:新增用户
*/
@Test
public void testSaveUser() {
Long userId = 1L;
User user = new User();
user.setName("测试用户");
user.setAge(20);
when(userService.saveUser(user)).thenReturn(userId);
try {
mockMvc.perform(post("/api/v1.0/user/").contentType("application/json").content(JSONObject.toJSONString(user)))
.andExpect(content().json("{'status': 'success', 'message': '新增用户ID为" + user.getId() + "'}"));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 测试用例:修改用户
*/
@Test
public void testUpdateUser() {
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setName("测试用户");
user.setAge(20);
when(userService.updateUser(user)).thenReturn(true);
try {
mockMvc.perform(put("/api/v1.0/user/{id}", userId).contentType("application/json").content(JSONObject.toJSONString(user)))
.andExpect(content().string("success"));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 测试用例:删除用户
*/
@Test
public void testRemoveUser() {
Long userId = 1L;
when(userService.removeUser(userId)).thenReturn(true);
try {
mockMvc.perform(delete("/api/v1.0/user/{id}", userId))
.andExpect(content().string("success"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.5 mvc 请求模拟
模拟 GET 请求:mvc.perform(get(“/employee/{id}”, 1))
模拟 POST 请求:mvc.perform(post(“/employee/{id}”, 1))
模拟文件上传:mvc.perform(multipart(“/upload”).file(“file”, “文件内容”.getBytes(“UTF-8”)))
模拟表单请求:mvc.perform(post(“/employee”).param(“name”, “张三”).param(“age”, 20))
模拟 session:mvc.perform(get(“/employee”).sessionAttr(name, value))
模拟 cookiei:mvc.perform(get(“/employee”).cookie(new Cookie(name ,value)))
模拟 HTTP body 内容,比如 json:mvc.perform(get(“/employee”).content(json))
模拟设置 HTTP header:
mvc.perform(get(“/employee/{id}”), employeeId)
.contentType(“application/x-www-form-urlencoded”)
.accept(“application/json”)
.header(header, value)
3.6 比较 mvc 请求返回结果
mvc.perform(get("/employee"))
.andExpect(status().isOk()) // 响应是否为200状态码
.andExpect(content().contentType(MediaType.APPLICATION_JSON)) // 返回的content type是不是application/json
.andExpect(jsonPath("$.name").value("张三")) // 返回内容本身检查
mvc.perform(post("/employee"))
.andExpect(model().attribute("name", "张三"))
.andExpect(model().attributeExists("name"))
4. Mockito 使用
假设我们有一个接口,需要来模拟这个接口的实现类对象
public interface EmployeeService {
public Employee findById(Long id);
public boolean add(Employee employee);
}
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class EmployeeServiceTest {
@Test
public void test() {
// 模拟出来一个实现了EmployeeService接口的对象
EmployeeService employeeService = mock(EmployeeService.class);
// 这个对象的findById()方法无论传入什么参数,都是返回预定义的一个对象
when(employeeService.findById(anyLong())).thenReturn(employee);
// 尝试调用
Employee resultEmployee = employeeServicie.findById(1);
// 比较结果
assertEquals(employee, resultEmployee);
}
}
还可以检查一个接口被调用的次数:
when(employeeService.findById(eq(1))).thenReturn(employee);
Employee resultEmployee = employeeService.findById(1);
assertEquals(employee, resultEmployee);
// 这里就是检查employeeService的findById(1)调用是否为2次
verify(employeeService ,times(2)).findById(eq(1));
此外,还可以检查多个接口被调用的顺序:
EmployeeService employeeService = mock(EmployeeService.class);
when(employeeService.findById(eq(1))).thenReturn(employee);
when(employeeService.add(eq(employee))).thenResult(true);
boolean addResult = employeeService.add(employee);
Employee resultEmployee = employeeService.findById(1);
// 这里就是在验证,是否按照下面的顺序完成的调用
InOrder inOrder = inOrder(employeeService);
inOrder.verify(employeeService).add(employee);
inOrder.verify(employeeService).findById(1);
还可以模拟抛出异常:
EmployeeService employeeService = mock(EmployeeService.class);
when(employeeService.findById(lt(0))).thenThrow(new IllegalArgumentException("employeeId不能小于0"))
Employee reseultEmployee = employeeService.findById(-1);