Spring-test + mockito + junit +cobertura 以及Spring-test + powermock+ junit遇到的坑

本文详细介绍了如何搭建Spring-test+mockito+junit环境进行单元测试,包括配置依赖、创建测试类、处理静态方法mock及数据库隔离。并探讨了Spring-test与powermock的兼容性问题,提供了一个可行的spring-test+powermock示例。

最近在研究单元测试,先是搭建了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这些版本还是可以不用变用可以高版本。

这个例子来源:http://www.tachilab.com/p/github.com/jayway/powermock/powermock-1.6.4/examples/spring-mockito/index.html

建议参考上面的网址,我下面记录下来主要是防止上面网址不可用。

<?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();
	}
}

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值