自从用了springMVC后,对于service层和dao层的测试,已经很熟悉了,但是对于controller层,要知道controller层的逻辑是否正确,就必须启动服务,但是这样稍微有点改动,就必须启动服务,十分麻烦。还好,spring提供了mockMvc模块,可以模拟web请求来对controller层进行单元测试
首先,要准备一个简单工程,我这里用的ssm来搭建的工程框架,并且使用了mybaits的通用mapper和分页插件pagehelper。这里不进行赘述,以后会单独写一篇来详细描述搭建ssm框架过程,或者读者可以直接百度,百度上还是有很多资源的。下面直接上源码。
代码结构:
注意,我这里把entity层和service和一些基础类,提取到另一个工程中了,和放在同一个工程里是一样的。
配置文件
spring-mybatis.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p"
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-3.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<context:component-scan base-package="com.charlotte.blog" />
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:spring/app.properties" />
bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
<property name="driverClassName" value="${driverClassName}" />
<property name="filters" value="${filters}" />
<property name="maxActive" value="${maxActive}" />
<property name="initialSize" value="${initialSize}" />
<property name="maxWait" value="${maxWait}" />
<property name="minIdle" value="${minIdle}" />
<property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}" />
<property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}" />
<property name="validationQuery" value="${validationQuery}" />
<property name="testWhileIdle" value="${testWhileIdle}" />
<property name="testOnBorrow" value="${testOnBorrow}" />
<property name="testOnReturn" value="${testOnReturn}" />
<property name="maxOpenPreparedStatements" value="${maxOpenPreparedStatements}" />
<property name="removeAbandoned" value="${removeAbandoned}" />
<property name="removeAbandonedTimeout" value="${removeAbandonedTimeout}" />
<property name="logAbandoned" value="${logAbandoned}" />
bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:com/charlotte/blog/mapper/*.xml" />
<property name="configLocation" value="classpath:mybatis-config.xml" />
bean>
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.charlotte.blog.mapper" />
<property name="markerInterface" value="tk.mybatis.mapper.common.Mapper" />
<property name="properties">
<value>
IDENTITY=MYSQL
ORDER=AFTER
notEmpty=FLASE
value>
property>
bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
bean>
<tx:annotation-driven transaction-manager="transactionManager"
proxy-target-class="true" />
beans>
spring-mvc.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
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-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<context:component-scan base-package="com.charlotte.blog.controller" />
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="com.charlotte.sdk.common.beans.MyFastJsonHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8value>
<value>application/jsonvalue>
list>
property>
<property name="features">
<array value-type="com.alibaba.fastjson.serializer.SerializerFeature">
<value>DisableCircularReferenceDetectvalue>
<value>WriteMapNullValuevalue>
<value>WriteNullListAsEmptyvalue>
<value>WriteNullStringAsEmptyvalue>
<value>WriteNullBooleanAsFalsevalue>
array>
property>
bean>
mvc:message-converters>
mvc:annotation-driven>
beans>
spring-db.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd"
default-lazy-init="true">
<context:property-placeholder location="classpath:spring/app.properties" />
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
<property name="driverClassName" value="${driverClassName}" />
<property name="filters" value="${filters}" />
<property name="maxActive" value="${maxActive}" />
<property name="initialSize" value="${initialSize}" />
<property name="maxWait" value="${maxWait}" />
<property name="minIdle" value="${minIdle}" />
<property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}" />
<property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}" />
<property name="validationQuery" value="${validationQuery}" />
<property name="testWhileIdle" value="${testWhileIdle}" />
<property name="testOnBorrow" value="${testOnBorrow}" />
<property name="testOnReturn" value="${testOnReturn}" />
<property name="maxOpenPreparedStatements" value="${maxOpenPreparedStatements}" />
<property name="removeAbandoned" value="${removeAbandoned}" />
<property name="removeAbandonedTimeout" value="${removeAbandonedTimeout}" />
<property name="logAbandoned" value="${logAbandoned}" />
bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:com/charlotte/blog/mapper/*.xml" />
<property name="configLocation" value="mybatis-config.xml" />
bean>
beans>
属性文件app.properties
# 数据库连接配置
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/blog
username=root
password=123456
filters: stat
maxActive: 20
initialSize: 1
maxWait: 60000
minIdle: 10
maxIdle: 15
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
maxOpenPreparedStatements: 20
removeAbandoned: true
removeAbandonedTimeout: 1800
logAbandoned: true
#redis 配置
redis.host=127.0.0.1
redis.port=6379
redis.pass=
redis.dbIndex=0
redis.expiration=3000
redis.maxIdle=300
redis.maxActive=600
redis.maxWait=1000
redis.testOnBorrow=true
controller层,以AccountInfoController为例
/**
*
*/
package com.charlotte.blog.controller.account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.charlotte.blog.controller.BaseController;
import com.charlotte.sdk.common.beans.BaseResponse;
import com.charlotte.sdk.common.utils.page.PageHandler;
import com.charlotte.sdk.main.entity.BlogAccountInfo;
import com.charlotte.sdk.main.form.account.AccountInfoSaveForm;
import com.charlotte.sdk.main.form.account.AccountInfoSearchForm;
import com.charlotte.sdk.main.service.account.AccountInfoService;
import com.github.pagehelper.PageInfo;
/**
* @author dingjunjie
*
*/
@Controller
@RequestMapping(value = "/account")
public class AccountInfoController extends BaseController{
@Autowired
private AccountInfoService accountInfoService;
@ResponseBody
@RequestMapping(value = "/test", method = RequestMethod.GET)
public BaseResponse test() {
BaseResponse response = new BaseResponse();
AccountInfoSearchForm searchForm = new AccountInfoSearchForm();
PageInfo result = accountInfoService.search(searchForm, new PageHandler());
response.setData(result);
return response;
}
@ResponseBody
@RequestMapping(value = "/add", method = RequestMethod.GET)
public BaseResponse add(@Validated AccountInfoSaveForm saveForm) {
saveForm.setUserName("admin");
saveForm.setPasswordSalt("test");
saveForm.setUserPassword("123456");
BaseResponse response = new BaseResponse();
accountInfoService.saveByForm(saveForm, null);
return response;
}
}
首先看下测试包的代码结构
这里最主要的就是BaseTest.java,代码如下
/**
*
*/
package com.charlotte.blog.base;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ImportResource;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
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.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
/**
* 单元测试基础类
*
* @author dingjunjie
* @date 2018-01-28 17:02:30
*/
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration(value = "src/main/webapp")
@ContextConfiguration(locations={"classpath:spring/spring-mybatis.xml","classpath:spring/spring-mvc.xml"})
@ComponentScan(basePackages={"com.charlotte.blog.controller", "com.charlotte.sdk.main.service"})
//当然 你可以声明一个事务管理 每个单元测试都进行事务回滚 无论成功与否
//@TransactionConfiguration( transactionManager = "transactionManager",defaultRollback = true)
//@Transactional
public class BaseTest {
public Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private WebApplicationContext webApplicationContext;
protected MockMvc mockMvc;
protected MockHttpServletRequest request;
protected MockHttpServletResponse response;
/**
* 初始化SpringmvcController类测试环境
*/
@Before
public void setup(){
//加载web容器上下文
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
}
我把这个类写成了基类,以后要新增单元测试,只要继承该类,就行了。
例如测试accountInfoController的方法,可以如下这样写
/**
*
*/
package com.charlotte.blog.controller.account;
import org.junit.Test;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import com.charlotte.blog.base.BaseTest;
/**
* @author dingjunjie
*
*/
public class TestAccountController extends BaseTest{
@Test
public void testTest(){
try {
ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.get("/account/test"));
MvcResult mvcResult = resultActions.andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
String result = mvcResult.getResponse().getContentAsString();
System.out.println("==========结果为:==========\n" + result + "\n");
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void testAdd(){
try {
ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.get("/account/add"));
MvcResult mvcResult = resultActions.andReturn();
String result = mvcResult.getResponse().getContentAsString();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里,其实还能够优化,可以把
ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.get("/account/test"));
MvcResult mvcResult = resultActions.andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
String result = mvcResult.getResponse().getContentAsString();
这部分代码封装成一个方法,放到BaseTest.java中去,访问url和参数paramMap作为参数,这样,继承类中,只要直接调用该方法,就能获得数据,更加方便。后续如果优化了,会直接更新到该文中,读者也可自行编写,还是比较容易的。