Spring MVC 单元测试学习
1.前言
这次来介绍下传统Spring MVC中对单元测试的整合使用,本篇会通过以下3点来介绍,基本满足日常需求:
- Dao层单元测试
- Service层单元测试
- Controller层单元测试
在单元测试中要尽量使用断言,本文所有的测试类都符合几个原则:
- 测试类卸载src/test/java目录下
- 测试类的包结构与被测试类的包结构相同
- 测试类的命名都是被测试类类名后缀加上Test,例如,UserDaoImpl与UserDaoImplTest相对应
- 测试类的方法与被测试类的方法命名相同
2.正文
2.1核心依赖
在Spring MVC 项目中引入单元测试很简单,依赖如下:
<!-- 测试框架 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
2.2 如何创建单元测试类
Spring Boot中单元测试类写在在src/test/java目录下,你可以手动创建具体测试类,如果是IDEA,则可以通过IDEA自动创建测试类,如下图,可以通过快捷键Ctrl+Shift+T(Window)来创建,如下:
2.3. 盲点解释
@Runwith
JUnit用例都是在Runner(运行器)来执行的。通过它,可以为这个测试类指定一个特定的Runner。
JUnit允许用户指定其它的单元测试执行类,只需要我们的测试执行类继承类org.junit.runners.BlockJUnit4ClassRunner就可以了,Spring的执行类SpringJUnit4ClassRunner就是继承了该类。我们平时用Spring也比较多,为了能够更加方便的引用配置文件,我们单元测试就使用了Spring实现的执行类。此时的单元测试执行类将会看起来是这样:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-context.xml")
public class UserServiceImplTest {
@Autowired
private UserService service;
/**
* 测试根据用户名查询用户
*/
@Test
public void getByUsername() {
User user = service.getByUsername("小明");
System.err.println(user);
}
SpringJUnit4ClassRunner
SpringJUnit4ClassRunner是JUnit的BlockJUnit4ClassRunner类的一个常规扩展,提供了一些spring测试环境上下文去规范JUnit测试。
@ContextConfiguration
注解指定了一个测试类运行了Spring 容器环境。
@WebAppConfiguration
测试环境使用,用来表示测试环境使用的ApplicationContext将是WebApplicationContext类型的;value可以指定web应用的根;
Spring MVC测试步骤
直接在测试类上面加上如下2个注解
- @RunWith(SpringJUnit4ClassRunner.class)
- @ContextConfiguration(locations = “classpath:spring-context.xml”)
就能取到spring中的容器的实例,如果配置了@Autowired那么就自动将对象注入。
注意:
如果要对Controller层测试,那么在你的Spring容器中要加入MVC容器的配置。
单元测试回滚
单元个测试的时候如果不想造成垃圾数据,可以开启事物功能,记在方法或者类头部添加@Transactional注解即可,如下:
@Transactional
@Test
public void save() {
User user = new User("测试用户", "123456");
int save = dao.save(user);
Assert.assertEquals(1, save);
}
这样测试完数据就会回滚了,不会造成垃圾数据。
3.核心代码示例
3.1.Spring容器配置
spring-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:annotation-config/>
<context:component-scan base-package="com.littlefxc.examples.service"/>
<context:component-scan base-package="com.littlefxc.examples.dao"/>
<context:property-placeholder location="classpath:druid.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="${spring.datasource.druid.url}"/>
<property name="username" value="${spring.datasource.druid.username}"/>
<property name="password" value="${spring.datasource.druid.password}"/>
<property name="driverClassName" value="${spring.datasource.druid.driver-class-name}"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 注解式事务配置 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="txManager"/>
<import resource="spring-mvc.xml" />
</beans>
spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven />
<context:component-scan base-package="com.littlefxc.examples.controller"/>
</beans>
3.2.Dao层单元测试
基本上所有的WEB程序都会涉及到数据库,本次示例就以最简化的模式:
持久层框架就用spring-jdbc。
Dao层的测试涉及到基本的CRUD操作。
UserDaoImpl
@Repository
public class UserDaoImpl implements UserDao {
@Autowired
private JdbcTemplate template;
private static final BeanPropertyRowMapper<User> MAPPER = new BeanPropertyRowMapper<>(User.class);
/**
* 新建用户
*
* @param user
* @return
*/
@Override
public int save(User user) {
return template.update(
"insert into user(username, password) values (?, ?)",
user.getUsername(), user.getPassword());
}
/**
* 修改密码
*
* @return
*/
@Override
public int update(User user) {
return template.update(
"update user set password = ? where username = ?", user.getPassword(), user.getUsername());
}
/**
* 根据ID删除用户
*
* @return
*/
@Override
public int delete(Long id) {
return template.update("delete from user where id = ?", id);
}
/**
* 获取所有用户
*
* @return
*/
@Override
public List<User> list() {
return template.query("select id, username, password from user", MAPPER);
}
@Override
public User getByUsername(String username) {
try {
return template.queryForObject(
"select id, username, password from user where username = ? limit 1", MAPPER, username);
} catch (EmptyResultDataAccessException e) {
return null;
}
}
}
UserDaoImplTest
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-context.xml")
public class UserDaoImplTest {
@Autowired
private UserDao dao;
/**
* 测试根据用户名查询用户
*/
@Test
public void getByUsername() {
User user = dao.getByUsername("小明");
System.err.println(user);
}
/**
* 测试新建用户
*/
@Transactional
@Test
public void save() {
User user = new User("测试用户", "123456");
int save = dao.save(user);
Assert.assertEquals(1, save);
}
/**
* 测试修改密码
*/
@Transactional
@Test
public void update() {
User user = new User("测试用户", "123456");
dao.save(user);
/* 密码修改前 */
User before = dao.getByUsername("测试用户");
user.setPassword("654321");
/* 密码修改后 */
dao.update(user);
User after = dao.getByUsername("测试用户");
/* 断言判断修改密码前后的两个类不同 */
Assert.assertNotEquals(before, after);
}
/**
* 测试删除用户
*/
@Transactional
@Test
public void delete() {
int save = dao.save(new User("测试用户", "123456"));
Assert.assertEquals(1, save);
User user = dao.getByUsername("测试用户");
int delete = dao.delete(user.getId());
Assert.assertEquals(1, delete);
}
/**
* 测试用户列表
*/
@Test
public void list() {
List<User> list = dao.list();
System.err.println(list);
}
}
3.3.Service层单元测试
UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao dao;
/**
* 根据用户名查询用户
*
* @param username
* @return
*/
@Override
public User getByUsername(String username) {
return dao.getByUsername(username);
}
/**
* 列表查询用户
*
* @return
*/
@Override
public List<User> list() {
return dao.list();
}
/**
* 修改密码
* @return
*/
@Transactional
@Override
public int updatePassword(User user) {
return dao.update(user);
}
/**
* 根据ID删除用户
* @return
*/
@Transactional
@Override
public int deleteById(Long id) {
return dao.delete(id);
}
/**
* 添加用户
* @param user
* @return
*/
@Transactional
@Override
public int save(User user) {
return dao.save(user);
}
}
UserServiceImplTest
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-context.xml")
public class UserServiceImplTest {
@Autowired
private UserService service;
/**
* 测试根据用户名查询用户
*/
@Test
public void getByUsername() {
User user = service.getByUsername("小明");
System.err.println(user);
}
/**
* 测试列表查询用户
*/
@Test
public void list() {
List<User> list = service.list();
System.err.println(list);
}
/**
* 测试修改密码
*/
@Transactional
@Test
public void updatePassword() {
User user = new User("测试用户", "123456");
service.save(user);
/* 密码修改前 */
User before = service.getByUsername("测试用户");
user.setPassword("654321");
/* 密码修改后 */
service.updatePassword(user);
User after = service.getByUsername("测试用户");
/* 断言判断修改密码前后的两个类不同 */
Assert.assertNotEquals(before, after);
}
/**
* 测试根据ID删除用户
*/
@Transactional
@Test
public void deleteById() {
int save = service.save(new User("测试用户", "123456"));
Assert.assertEquals(1, save);
User user = service.getByUsername("测试用户");
int delete = service.deleteById(user.getId());
Assert.assertEquals(1, delete);
}
/**
* 测试添加用户
*/
@Transactional
@Test
public void save() {
List<User> list = service.list();
System.err.println(list);
}
}
3.4.Controller层单元测试
上面只是针对Service和Dao层做测试,但是有时候需要对Controller层(API)做测试,这时候就得用到MockMvc了,你可以不必启动工程就能测试这些接口。
MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。
UserController
@RestController
public class UserController {
@Autowired
private UserService service;
/**
* 用户列表
* @return
*/
@RequestMapping("/user/list")
public List<User> list() {
return service.list();
}
/**
* 新建用户
* @param username
* @param password
* @return
*/
@RequestMapping("/user/save")
public int save(@RequestParam String username, @RequestParam String password) {
return service.save(new User(username, password));
}
/**
* 根据ID删除用户
* @param id
* @return
*/
@RequestMapping("/user/delete")
public int delete(@RequestParam Long id) {
return service.deleteById(id);
}
/**
* 根据用户名查询用户
* @param username
* @return
*/
@RequestMapping("/user/getByUsername")
public User getByUsername(@RequestParam String username) {
return service.getByUsername(username);
}
/**
* 根据用户名修改密码
* @param username
* @param newPassword
* @return
*/
@RequestMapping("/user/updatepassword")
public int updatepassword(@RequestParam String username, @RequestParam String newPassword) {
return service.updatePassword(new User(username, newPassword));
}
}
UserControllerTest
/**
* 1.@WebAppConfiguration:测试环境使用,用来表示测试环境使用的ApplicationContext将是WebApplicationContext类型的;
* 2.@ContextConfiguration: 指定测试类的容器环境
*/
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = {"classpath:spring-context.xml"})
public class UserControllerTest {
/**
* 模拟浏览器
*/
MockMvc mvc;
/**
* 加载WEB的上下文
*/
@Autowired
WebApplicationContext context;
@Before
public void before() {
// mvc = MockMvcBuilders.standaloneSetup(new TestController()).build();
mvc = MockMvcBuilders.webAppContextSetup(context).build();//建议使用这种
}
/**
* 测试用户列表
*
* @throws Exception
*/
@Test
public void list() throws Exception {
mvc.perform(
MockMvcRequestBuilders.get("/user/list"))
.andExpect(status().isOk()) // 期待返回状态码200
.andDo(print()); // 打印返回的 http response 信息
}
/**
* 测试添加用户
* 期待:Body = 1
*
* @throws Exception
*/
@Transactional
@Test
public void save() throws Exception {
mvc.perform(
MockMvcRequestBuilders.post("/user/save")
.param("username", "测试用户1")
.param("password", "123456"))
.andExpect(status().isOk())
.andDo(print());
}
/**
* 测试删除用户
* 期待:Body = 1
*/
@Transactional
@Test
public void delete() throws Exception {
mvc.perform(MockMvcRequestBuilders.post("/user/delete")
.param("id", "1"))
.andExpect(status().isOk())
.andDo(print());
}
/**
* 测试根据用户名查询用户
*/
@Test
public void getByUsername() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user/getByUsername")
.param("username", "小明"))
.andExpect(status().isOk())
.andDo(print());
}
/**
* 测试修改密码
* 期待:Body = 1
*/
@Transactional
@Test
public void updatepassword() throws Exception {
mvc.perform(MockMvcRequestBuilders.post("/user/updatepassword")
.param("username", "小明")
.param("newPassword", "654321"))
.andExpect(status().isOk())
.andDo(print());
}
}