最近在研究单元测试,先是搭建了Spring-test + mockito + junit的环境,但是由于发现项目中需要有mock静态方法的地方,于是研究了下Spring-test + powermock+ junit后者我研究了三天踩过无数坑,最终还是放弃了。mockito团队也是建议不要对静态方法进行mock,当然他们也没有实现静态方法的mock。
Spring-test + powermock+ junit虽然是可以mock静态方法,但是在spring-test环境下powermock无法与mockito同时使用。我说的是:使用powermock后无法使用mockito的@mock注解,大概原因是因为powermock使用的xstream无法解析@mock生成的对象(下文简称mock对象),错误信息大概是这样的:com.thoughtworks.xstream.converters.ConversionException。具体原因是因为:mockito采用的mock技术是采用cglib代理一个对象,其mock的对象是一个通过字节码技术生成的对象。而powermock采用的技术是实现一个自定义的类加载器。而powermock的类加载器加载mock对象解析不了cglib生成的对象。我尝试过通过powermock的@PowerMockIgnore注解将mock对象排除在powermock的类加载器中,即不在powermock类加载器中加载mock对象,此时test程序跑起来是没有问题的。但是又有一个问题来了,因为你的test代码以及所有除了@PowerMockIgnore注解标注的包下的代码都是在powermock的类加载器里加载的,现在你mock对象却不是在这个类加载器中(mock对象在sun.misc.Launch$AppClassLoader这个类加载器里),而且powermock的类加载器并没有按照双亲委派模型去实现。所以...就会造成mock对象在使用时有些功能比如when()方法失效,永远获取不了正确的结果。因为你再test里面期待的返回结果类和mock对象返回结果类都不是在同一个加载器里面。如果你还是想研究下spring-test+powermock,我将会在文章后面给出一个spring-test+powermock的例子。
好了说了这么多坑还是来看看如何搭建一个Spring-test + mockito + junit环境以及单元测试覆盖率报告。
1:在你的pom.xml文件引入依赖以及插件
<!-- junit + mock start -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>your-spring-version</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>2.0.2-beta</version>
<scope>test</scope>
</dependency>
<!-- junit + mock end -->
<!-- junit test plugin start -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19.1</version>
<configuration>
<!--设置包含的测试类surefire默认 包含**/*Test.java, **/Test*.java, **/*TestCase.java
默认排除 **/Abstract*Test.java, **/Abstract*TestCase.java -->
<includes>
<!-- <include>*Test</include> -->
</includes>
<!-- 设置不进行测试类 -->
<excludes>
<!-- <exclude>Test*</exclude> -->
</excludes>
<!-- 跳过测试阶段,測試類写的有问题也会出错,一般不推荐 -->
<!--<skip>true</skip> -->
</configuration>
</plugin>
<!-- 构建项目站点报告插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.0-beta-3</version>
<configuration>
<!-- 配置站点国际化 -->
<locales>zh_CN</locales>
<!-- 输出编码 -->
<outputEncoding>GBK</outputEncoding>
</configuration>
</plugin>
<!-- 项目API doc报告 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.7</version>
</plugin>
<!-- 单元测试报告html -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>2.12.2</version>
<configuration>
<showSuccess>false</showSuccess>
</configuration>
</plugin>
<!-- 测试覆盖率的报告 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.7</version>
<dependencies>
<!-- 处理cobertura2.7无法解析java8特殊表达式,自定义其内部asm依赖 -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>5.0.3</version>
</dependency>
</dependencies>
<configuration>
<!-- 解决JavaNCSS got an error while parsing the java file(无法解析java8部分语法的警告) -->
<quiet>true</quiet>
<formats>
<format>html</format>
<format>xml</format>
</formats>
</configuration>
<executions>
<execution>
<id>cobertura-report</id>
<goals>
<goal>cobertura</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
</plugin>
<!-- junit test plugin end -->
2:先按照途中构建项目目录

然后新建一个BaseUnit.java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:META-INF/spring/*.xml")
public class BaseUnit {
@Before
public void setup(){
MockitoAnnotations.initMocks(this);
}
}
3:新建一个test类,建议以Test.java 结尾(可以看上面第一个插件里写的注释)
public class LoginFacadeTest extends BaseUnit {
@InjectMocks
@Resource
private LoginFacade loginFacade;
@Mock
private UserManager userManager;
@Test
public void getUserMessage() {
assertEquals("0", "0");
}
}
4:新建spring-test. xml 当然你自己的xml可能不一样,主要是定义spring加载的环境,下面的代码主要是展示如何隔离数据库,毕竟单元测试要求不要依赖任何外部环境就可以测试程序。
<?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:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-4.3.xsd">
<!-- Scans for application @Components to deploy -->
<context:component-scan base-package="*">
</context:component-scan>
<context:property-placeholder location="classpath*:/*.properties" />
<bean id="mysqlMapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage"
value="com.bank.user.dao.mysql,com.bank.user.dao.oracle" />
<property name="sqlSessionFactoryBeanName" value="mysqlSampleSqlSessionFactory" />
</bean>
<bean id="mysqlSampleSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="mysqlSampleDataSource" />
<!-- <property name="configLocation" value="classpath*:/mybatis-mysql-config-mapper.xml"
/> -->
</bean>
<bean id="mysqlSampleDataSource" class="org.mockito.Mockito"
factory-method="mock">
<constructor-arg value="com.alibaba.druid.pool.DruidDataSource" />
</bean>
</beans>
5:由于我这个项目是一个dubbo项目,所以可能会有些外部服务依赖。一般处理是将其mock掉
新建dubbo-test.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<!-- 操作日志记录接口配置 -->
<bean id="operationLogFacade" class="org.mockito.Mockito"
factory-method="mock">
<constructor-arg
value="com.bank.sysmng.api.OperationLogFacade" />
</bean>
</beans>
6:现在单元测试环境就搭建好了。可以测试了。
下面再讲下如果在spring项目中用到了redis,可能要特殊处理下。
因为redis有一种用法比如:redisTemplate.opsForValue().get();如果我们仅仅是对redisTemplate进行mock的话,那你就不知道如何对redisTemplate进行when()操作了。所以我们需要把redisTemplate.opsForValue()惊喜mock.处理方法如下:
6.1:新增一个RedisTemplateSpy.java (注意保证RedisTemplateSpy能被注册为bean)
这个类主要就是把redisTemplate的opsForValue()等方法也进行了mock。我们就可以使用
when(redisTemplate.opsForValue().get(userLoginFailedNumDay)).thenReturn("6"); 这种语法了。需要说明的是,在你的test代码中就不要用 @Mock private RedisTemplate redisTemplate; 引入redisTemplate,直接使用 @Autowired private RedisTemplate redisTemplate; 引入就行了,因为redisTemplate已经被mock了。
@Component
public class RedisTemplateSpy implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if ("redisTemplate".equals(beanName)) {
RedisTemplate bean2 =Mockito.mock(RedisTemplate.class);
if (bean instanceof RedisTemplate) {
Mockito.when(bean2.opsForValue()).thenReturn(Mockito.mock(ValueOperations.class));
Mockito.when(bean2.opsForList()).thenReturn(Mockito.mock(ListOperations.class));
Mockito.when(bean2.opsForSet()).thenReturn(Mockito.mock(SetOperations.class));
Mockito.when(bean2.opsForZSet()).thenReturn(Mockito.mock(ZSetOperations.class));
}
return bean2;
}
return bean;
}
}
如果你还是想研究下spring-test + powerMock 看看下面的例子。
搭建一个spring-test + powerMock的例子,网上的例子并不多,并且很多坑,我研究了几天才找到一个可以用的例子。
1:pom.xml
需要说明的是:spring-test 的版本必须为3.0.5.RELEASE,否则会出错。这是我试出来的。其他版本的spring-test都不行你可以试一下。当然如果你的项目spring版本高也无所谓,你只需要单独指定spring-test为3.0.5.RELEASE就行了,spring-context,spring-core这些版本还是可以不用变用可以高版本。
建议参考上面的网址,我下面记录下来主要是防止上面网址不可用。
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2010 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.powermock.examples</groupId>
<artifactId>powermock-examples</artifactId>
<version>1.6.4</version>
</parent>
<artifactId>powermock-examples-spring-mockito</artifactId>
<name>${project.artifactId}</name>
<properties>
<spring.version>3.0.5.RELEASE</spring.version>
</properties>
<description>
Example showing how to use the PowerMock in a Spring Integration Test
</description>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<scope>test</scope>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4-rule</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-classloading-xstream</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2:测试类代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:/example-context.xml")
@PrepareForTest(IdGenerator.class)
public class SpringExampleTest {
@Rule
public PowerMockRule rule = new PowerMockRule();
@Autowired
private MyBean myBean;
@Test
public void mockStaticMethod() throws Exception {
// Given
final long expectedId = 2L;
mockStatic(IdGenerator.class);
when(IdGenerator.generateNewId()).thenReturn(expectedId);
// When
final Message message = myBean.generateMessage();
// Then
assertEquals(expectedId, message.getId());
assertEquals("My bean message", message.getContent());
}
@Test
public void mockStaticMethodAndVerify() throws Exception {
// Given
final long expectedId = 2L;
mockStatic(IdGenerator.class);
when(IdGenerator.generateNewId()).thenReturn(expectedId);
// When
final Message message = myBean.generateMessage();
// Then
assertEquals(expectedId, message.getId());
assertEquals("My bean message", message.getContent());
verifyStatic(); IdGenerator.generateNewId();
}
@Test
public void stubStaticMethod() throws Exception {
// Given
final long expectedId = 2L;
stub(method(IdGenerator.class, "generateNewId")).toReturn(expectedId);
// When
final Message message = myBean.generateMessage();
// Then
assertEquals(expectedId, message.getId());
assertEquals("My bean message", message.getContent());
}
@Test
public void suppressStaticMethod() throws Exception {
// Given
suppress(method(IdGenerator.class, "generateNewId"));
// When
final Message message = myBean.generateMessage();
// Then
assertEquals(0L, message.getId());
assertEquals("My bean message", message.getContent());
}
}
3:example-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"
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-2.5.xsd">
<context:component-scan base-package="powermock.examples.spring" />
</beans>
4:几个用到的类
@Component
public class MyBean {
public Message generateMessage() {
final long id = IdGenerator.generateNewId();
return new Message(id, "My bean message");
}
}
public class Message {
private final long id;
private final String content;
public Message(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
public class IdGenerator {
/**
* @return A new ID based on the current time.
*/
public static long generateNewId() {
return System.currentTimeMillis();
}
}
Spring-test与Mockito单元测试
本文详细介绍了如何搭建Spring-test+mockito+junit环境进行单元测试,包括配置依赖、创建测试类、处理静态方法mock及数据库隔离。并探讨了Spring-test与powermock的兼容性问题,提供了一个可行的spring-test+powermock示例。
1618

被折叠的 条评论
为什么被折叠?



