在使用PowerMock来写单元测试的时候,且单元测试里面下面的@PrepareForTest和@RunWith(PowerMockRunner.class)的时候,单元测试能成功跑出来,但是其生成的单元测试覆盖率为零。比如下面的代码:

package org.powermock.example;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;

import java.util.ArrayList;
import java.util.Properties;

import static org.assertj.core.api.Assertions.assertThat;
import static org.powermock.api.mockito.PowerMockito.doCallRealMethod;
import static org.powermock.api.mockito.PowerMockito.doReturn;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest({Configuration.class, Lamda.class})
public class SomeClassTest {

    @Before
    public void setUp() throws Exception {
        Whitebox.setInternalState(Configuration.class, "enabled", (Object) null);
    }

    @Test
    public void shouldReturnSumIfEnabled() throws Exception {
        mockStatic(Configuration.class);

        Properties properties = new Properties();
        properties.put("enabled", "true");

        doReturn(properties).when(Configuration.class, "readProperties");
        doCallRealMethod().when(Configuration.class, "isEnabled");
        doCallRealMethod().when(Configuration.class, "loadFromProperties");

        assertThat(new SomeClass().add(1, 5)).isEqualTo(6);
    }

    @Test
    public void shouldReturnZeroIfDisabled() throws Exception {
        mockStatic(Configuration.class);

        Properties properties = new Properties();
        properties.put("enabled", "false");

        doReturn(properties).when(Configuration.class, "readProperties");
        doCallRealMethod().when(Configuration.class, "isEnabled");
        doCallRealMethod().when(Configuration.class, "loadFromProperties");

        assertThat(new SomeClass().add(1, 5)).isEqualTo(0);
    }

    @Test(expected = RuntimeException.class)
    public void shouldC() {
        mockStatic(Lamda.class);
        when(Lamda.capitalize(Mockito.anyString())).thenReturn("01234567890");

        ArrayList<String> in = new ArrayList<>();

        in.add("----");
        in.add(null);

        new Lamda().validate(in);
    }
}
package org.powermock.example;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

import lombok.Data;



@Data
public class Configuration {

	private String name;
    private static Boolean enabled;

    public static boolean isEnabled() {
        if (enabled == null){
            loadFromProperties();
        }
        return enabled;
    }

    private static void loadFromProperties() {
        Properties properties = readProperties();
        enabled = "true".equals(properties.getProperty("enabled"));
    }

    private static Properties readProperties() {
        Properties properties = new Properties();
        try {
            properties.load(new FileInputStream("some.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return properties;
    }
}

package org.powermock.example;

import java.util.List;
import java.util.function.Consumer;

public class Lamda {

    public static String capitalize(String in) {
        String result = "";
        String[] a = in.split(",");
        for (String s : a) {
            result += s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
        }
        return result;
    }

    public void validate(List<String> in) {
        doWithList(in, s -> {
            if (s != null && s.length() > 10) {
                throw new RuntimeException("");
            }
        });
    }

    private void doWithList(List<String> in, Consumer<String> consumer) {
        in.stream().map(Lamda::capitalize).forEach(consumer);
    }

}

package org.powermock.example;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

/**
 *
 */
@Data

public class SomeClass {
    public int add(int x, int y) {
        if (Configuration.isEnabled()) {
            return x + y;
        }
        return 0;
    }
}

那么应该如何做呢?Pom.xml的配置应该注意哪些点呢?

#1. 如果pom.xml还继承了parent pom,确保的你的parent pom里面没有plugin的配置
否则会报下面的类似的错误。

aused by: java.lang.IllegalStateException: Class xxxx.class is already instrumented.
        at org.jacoco.agent.rt.internal_290345e.core.internal.instr.InstrSupport.assertNotInstrumented(InstrSupport.java:176)
        at org.jacoco.agent.rt.internal_290345e.core.internal.instr.ClassInstrumenter.visitField(ClassInstrumenter.java:55)
        at org.jacoco.agent.rt.internal_290345e.asm.ClassVisitor.visitField(ClassVisitor.java:294)
        at org.jacoco.agent.rt.internal_290345e.asm.ClassReader.readField(ClassReader.java:883)
        at org.jacoco.agent.rt.internal_290345e.asm.ClassReader.accept(ClassReader.java:694)
        at org.jacoco.agent.rt.internal_290345e.asm.ClassReader.accept(ClassReader.java:500)
        at org.jacoco.agent.rt.internal_290345e.core.instr.Instrumenter.instrument(Instrumenter.java:90)
        at org.jacoco.agent.rt.internal_290345e.core.instr.Instrumenter.instrument(Instrumenter.java:108)

#2 确保你的dependency中有下面的依赖

 <dependency>
     <groupId>org.jacoco</groupId>
     <artifactId>org.jacoco.agent</artifactId>
     <version>${jacoco.version}</version>
     <classifier>runtime</classifier>
</dependency>

否则会报下面的错误:

com.siact.product.jwp.module.user.service.UserTest  Time elapsed: 0.073 sec  <<< ERROR!
java.lang.NoClassDefFoundError: org/jacoco/agent/rt/internal_290345e/Offline
Caused by: java.lang.ClassNotFoundException: org.jacoco.agent.rt.internal_290345e.Offline

#3 确保你的maven-surefire-plugin的生成的acoco-agent.destfile值和jacoco-maven-plugin中的dataFile一致
在这里插入图片描述
#4 确保你的PowerMock的版本和Mockito的版本能匹配上,符合下面的列表
在这里插入图片描述
#5 确保你的是用了org.jacoco offline的模式。

   <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>${jacoco.version}</version>
                <executions>
                    <execution>
                        <id>default-instrument</id>
                        <goals>
                            <goal>instrument</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>default-restore-instrumented-classes</id>
                        <goals>
                            <goal>restore-instrumented-classes</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>${project.build.directory}/coverage.exec</dataFile>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

下面上整个pom.xml的文件:

<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-examples</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>jacoco-offline</artifactId>
    <name>JaCoCo Offline with PowerMock</name>
    <description>
        Example how to get code coverage with PowerMock
    </description>

    <properties>
        <jacoco.version>0.7.7.201606060606</jacoco.version>

        <!-- Used to locate the profile specific configuration file. -->
        <build.profile.id>dev</build.profile.id>

        <jacoco.it.execution.data.file>${project.build.directory}/coverage-reports/jacoco-it.exec</jacoco.it.execution.data.file>
        <jacoco.ut.execution.data.file>${project.build.directory}/coverage-reports/jacoco-ut.exec</jacoco.ut.execution.data.file>

        <jdk.version>1.8</jdk.version>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <!-- Only unit tests are run by default. -->
        <skip.unit.tests>false</skip.unit.tests>

    </properties>

    <dependencies>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-api-mockito</artifactId>
        </dependency>
        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-module-junit4</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>org.jacoco.agent</artifactId>
            <version>${jacoco.version}</version>
            <classifier>runtime</classifier>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>${jdk.version}</source>
                    <target>${jdk.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>${jacoco.version}</version>
                <executions>
                    <execution>
                        <id>default-instrument</id>
                        <goals>
                            <goal>instrument</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>default-restore-instrumented-classes</id>
                        <goals>
                            <goal>restore-instrumented-classes</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>${project.build.directory}/coverage.exec</dataFile>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.19.1</version>
                <configuration>
                    <systemPropertyVariables>
                        <jacoco-agent.destfile>${project.build.directory}/coverage.exec</jacoco-agent.destfile>
                    </systemPropertyVariables>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

上面是一种PowerMock和Mockito的搭配方式,除此之外还有下面的搭配方式。

	<!-- JUnit5 and PowerMock Begin -->

		<dependency>
			<groupId>org.powermock</groupId>
			<artifactId>powermock-module-junit4</artifactId>
			<scope>test</scope>

		</dependency>
		<dependency>
			<groupId>org.powermock</groupId>
			<artifactId>powermock-api-mockito2</artifactId>
			<version>1.7.0RC2</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.mockito</groupId>
			<artifactId>mockito-core</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-test</artifactId>
			<scope>compile</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<scope>compile</scope>
		</dependency>
		<dependency>
			<groupId>org.powermock</groupId>
			<artifactId>powermock-api-easymock</artifactId>
			<scope>test</scope>
		</dependency>


		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter-engine</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.junit.vintage</groupId>
			<artifactId>junit-vintage-engine</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter-api</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter-params</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.junit.platform</groupId>
			<artifactId>junit-platform-suite-api</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.jacoco</groupId>
			<artifactId>org.jacoco.agent</artifactId>
			<classifier>runtime</classifier>
			<scope>test</scope>
			<version>${jacoco.version}</version>
		</dependency>
		<!-- JUnit5 and PowerMock End -->

其版本信息如下:

<jacoco.version>0.8.0</jacoco.version>
<powermock.version>1.6.3</powermock.version>
<mockito.version>3.9.0</mockito.version>
<powermock.api.mockito2.version>1.7.0RC2</powermock.api.mockito2.version>

还有其他的组合

  <!-- tests -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>com.fasterxml.uuid</groupId>
      <artifactId>java-uuid-generator</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.assertj</groupId>
      <artifactId>assertj-core</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-client</artifactId>
      <version>1.17</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-core</artifactId>
      <version>3.5.15</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-inline</artifactId>
      <version>3.5.15</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-module-junit4</artifactId>
      <version>2.0.2</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-module-junit4-rule</artifactId>
      <version>2.0.2</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-api-mockito2</artifactId>
      <version>2.0.2</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-classloading-xstream</artifactId>
      <version>2.0.2</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>${version.spring.framework}</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${version.spring.framework}</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.camunda.bpm</groupId>
      <artifactId>camunda-engine-rest-core</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
      <classifier>tests</classifier>
    </dependency>

    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-webapp</artifactId>
      <version>${version.jetty}</version>
      <scope>test</scope>
    </dependency>

最后扔一个彩蛋,如果在项目中使用了lombok,在DTO或者Bean的包下,添加一个名字为
lombok.config的文件,并在里面输入下面的配置:

config.stopBubbling = true
lombok.addLombokGeneratedAnnotation = true

这样,以lombok的@Data注解生成的类,其会被lombo的字节码编译环境自动嵌入@lombok.generated 注解,在0.8.0版本以上的Jacoco中,其响应的DTO和Entity就不会记录到Jacoco的覆盖率当中,从而大大提高了代码覆盖率!
其中config.stopBubbling = true的作用是这种自动化添加@lombok.generated 注解的功能不会蔓延和扩展到父包里面~

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐