我们可以在不启动服务的情况下,进行单元测试,以便提交出高质量的代码。本文以一个小例子,说明在Spring中如何进行单元测试。
一:测试Controller
1:在pom.xml文件中引入相关依赖
<properties>
<!-- 设置项目编码编码 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- spring版本号 -->
<spring.version>4.3.5.RELEASE</spring.version>
<!-- mybatis版本号 -->
<mybatis.version>3.4.1</mybatis.version>
</properties>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12-beta-3</version>
</dependency>
<!--引入文件上传-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
<exclusions>
<exclusion>
<artifactId>commons-io</artifactId>
<groupId>commons-io</groupId>
</exclusion>
</exclusions>
</dependency>
2:编写被测试类--UserController
import com.qiqi.juint.test.model.vo.UserVO;
import com.qiqi.juint.test.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "getUser",method = RequestMethod.GET)
@ResponseBody
public List<UserVO> getUserInfo(@RequestParam Integer age){
return userService.getUserInfo(age);
}
}
3:测试类--UserControllerTest
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
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.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration //声明一个ApplicationContext集成测试加载WebApplicationContext,作用是模拟ServletContext
@ContextConfiguration(locations={"classpath:spring/spring-application.xml"})
public class UserControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
@Test
public void testGetUserInfo() throws Exception {
MvcResult mvResult = mockMvc.perform(MockMvcRequestBuilders.get("/user/getUser").param("age","12"))
.andDo(print())
.andReturn();//最后返回相应的MvcResult
}
}
测试结果:
MockHttpServletRequest:
HTTP Method = GET
Request URI = /user/getUser
Parameters = {age=[12]}
Headers = {}
Handler:
Type = com.qiqi.juint.test.controller.UserController
Method = public java.util.List<com.qiqi.juint.test.model.vo.UserVO> com.qiqi.juint.test.controller.UserController.getUserInfo(java.lang.Integer)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = {Content-Type=[application/json;charset=UTF-8]}
Content type = application/json;charset=UTF-8
Body = [{"id":2,"age":12,"name":"小花"},{"id":3,"age":12,"name":"小兰"},{"id":7,"age":12,"name":"大师兄"}]
Forwarded URL = null
Redirected URL = null
Cookies = []
二:测试Service
引入的依赖和测试Controller相同。测试时,在Service类上添加相应的注解就可以进行测试,不用单独写测试类。
import com.qiqi.juint.test.dao.UserMapper;
import com.qiqi.juint.test.model.vo.UserVO;
import com.qiqi.juint.test.service.UserService;
import org.apache.log4j.Logger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.transaction.Transactional;
import java.util.List;
/**
* Created by ZhaoQiqi on 2018/11/8.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:spring/spring-application.xml"})
@Transactional
@Service
public class UserServiceImpl implements UserService {
private static final Logger logger = Logger.getLogger(UserServiceImpl.class);
@Autowired
private UserMapper userMapper;
public List<UserVO> getUserInfo(Integer age) {
logger.info("调用方法getUserInfo(Integer age)");
return userMapper.getUserInfo(age);
}
@Test
public void test(){
List<UserVO> list = userMapper.getUserInfo(12);
System.out.println(list);
}
}
测试结果:
13:20:09.492 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
13:20:09.496 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@50305a]
13:20:09.503 [main] DEBUG o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@6b9267b [wrapping: com.mysql.jdbc.JDBC4Connection@408b35bf]] will be managed by Spring
13:20:09.506 [main] DEBUG c.q.j.t.dao.UserMapper.getUserInfo - ==> Preparing: SELECT `id` as id, `age` as age, `name` as name FROM user WHERE age = ?;
13:20:09.525 [main] DEBUG c.q.j.t.dao.UserMapper.getUserInfo - ==> Parameters: 12(Integer)
13:20:09.541 [main] DEBUG c.q.j.t.dao.UserMapper.getUserInfo - <== Total: 3
13:20:09.546 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@50305a]
[id:2 age:12 name:小花, id:3 age:12 name:小兰, id:7 age:12 name:大师兄]
13:20:09.546 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@50305a]
13:20:09.546 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@50305a]
三:在测试类中添加回滚注解,避免测试数据污染数据库
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration //声明一个ApplicationContext集成测试加载WebApplicationContext,作用是模拟ServletContext
@ContextConfiguration(locations={"classpath:spring/spring-application.xml"})
//配置事务的回滚,对数据库的增删改都会回滚,便于测试用例的循环利用
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
@Transactional
public class UserControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
@Test
public void testGetUserInfo() throws Exception {
MvcResult mvResult = mockMvc.perform(MockMvcRequestBuilders.get("/user/getUser")
.param("age","12"))
.andDo(print())
.andReturn();//最后返回相应的MvcResult
}
@Test
public void testInsertUser() throws Exception {
User user = new User(15,"哈哈");
String requestJson = JSONObject.toJSONString(user);
System.out.println(requestJson);
MvcResult mvResult = mockMvc.perform(
MockMvcRequestBuilders.post("/user/insertUser")
.contentType(MediaType.APPLICATION_JSON).content(requestJson))
.andDo(print())
.andReturn();//最后返回相应的MvcResult
}
}
这样在测试时,对数据库的增删改都会回滚,数据库数据不会改变。在非测试时,即正常调用时,若被测试类没有进行回滚注解,数据库数据自然会改变。
四、相关代码的解释:
@webappconfiguration是一级注释,用于声明一个ApplicationContext集成测试加载WebApplicationContext。作用是模拟ServletContext。
@ContextConfiguration:因为controller,component等都是使用注解,需要注解指定spring的配置文件,扫描相应的配置,将类初始化等。
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
@Transactional
上面两句的作用是,让我们对数据库的操作会事务回滚,如对数据库的添加操作,在方法结束之后,会撤销我们对数据库的操作。
为什么要事务回滚?
测试过程对数据库的操作,会产生脏数据,影响我们数据的正确性
不方便循环测试,即假如这次我们将一个记录删除了,下次就无法再进行这个Junit测试了,因为该记录已经删除,将会报错。
如果不使用事务回滚,我们需要在代码中显式的对我们的增删改数据库操作进行恢复,将多很多和测试无关的代码
方法解析:
perform:执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理;
get:声明发送一个get请求的方法。MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables):根据uri模板和uri变量值得到一个GET请求方式的。另外提供了其他的请求的方法,如:post、put、delete等。
param:添加request的参数,如上面发送请求的时候带上了了pcode = root的参数。假如使用需要发送json数据格式的时将不能使用这种方式,可见后面被@ResponseBody注解参数的解决方法
andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确(对返回的数据进行的判断);
andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台(对返回的数据进行的判断);
andReturn:最后返回相应的MvcResult;然后进行自定义验证/进行下一步的异步处理(对返回的数据进行的判断);