Spring + jacoco + powerMock 单元测试
一 什么是单元测试
什么是单元测试呢?单元测试就是针对最小的功能单元编写测试代码。Java程序最小的功能单元是方法,因此,对Java程序进行单元测试就是针对单个Java方法的测试。
其实在日常的工作中,大部分开发都很容易对项目的单测忽略,甚至因为工期紧张就没有写过单元测试,作者上一家公司就没有写单测的习惯,换了工作之后,这边对单测的要求还是比较高的,新项目行覆盖率需要达到80%,旧的项目行覆盖率需要达到60%,在经过几次磨练之后,接下来就重点的讲一讲如何写单测(其实我也写的不是很好),以及如何知道自己的项目的一个整体的单测覆盖情况,这里面有一些概念啊比如说行覆盖率、方法覆盖率等大家事先先了解一下
二 Spring + Jacoco + powerMock 单测
其实写单测很简单,我们最熟悉的就是用大家都很熟悉Junit去编写单测,但是这对一般的方法逻辑是可行的,但是遇到有外部依赖等复杂条件,用Junit去写就不是很方便了。
一般处理Spring 单测我们一般的做法就是如下启动Spring容器:
image.png
其实这样的做法有几个不好的地方哈:
1.试想,这每个测试方法都需要进行Spring 容器的初始化,这样整一个CI流程跑下来的话,时间就会很长
2.如果遇到有第三方调用的情况(没有进行mock的情况),本机环境和三方环境不通的情况,很容易就导致了单测的失败
那么有人就说了,那你第三方的调用数据进行mock不就行了嘛,其实是可以的哈,其实单测的话 从controller层 到 service层 到 dao、RPC层 都应该进行单测编写,但是实际开发时间都不会允许有这么长的时间去整,所以一般的关注点就是在service层,并且建议controller层不要写太多的业务代码,尽量都放在service层里面。那么service层的代码将那些重要的操作进行mock掉之后,就可以在不启动容器的情况下快速的进行跑单测,但这个不是绝对的哈,根据自己项目的风格来,该启容器还是得起的。
好嘛,讲了这么多,其实大家很多人都应该知道这些个理论啊,那么接下来就手把手教大家怎么集成并且手机
三 实战
首先父pom文件需要引入一些依赖和jacoco的pluin工具
父pom文件:
<lombok-version>1.18.16</lombok-version>
<org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
<jacoco.version>0.8.0</jacoco.version>
<powermock-api-version>1.7.4</powermock-api-version>
<powermock-module-junit4.version>2.0.2</powermock-module-junit4.version>
<mockito-all.version>${powermock-module-junit4.version}-beta</mockito-all.version>
<dependencies>
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.agent</artifactId>
<version>${jacoco.version}</version>
<classifier>runtime</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M1</version>
<type>maven-plugin</type>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<!--单测相关-->
<dependency>
<artifactId>spring-boot-starter-test</artifactId>
<groupId>org.springframework.boot</groupId>
<version>1.5.22.RELEASE</version>
</dependency>
<dependency>
<artifactId>junit</artifactId>
<groupId>junit</groupId>
<scope>test</scope>
<version>4.12</version>
</dependency>
<dependency>
<artifactId>spring-test</artifactId>
<groupId>org.springframework</groupId>
<version>4.3.18.RELEASE</version>
</dependency>
<!--power mock-->
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<scope>test</scope>
<version>${powermock-module-junit4.version}</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<scope>test</scope>
<version>${powermock-api-version}</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-core</artifactId>
<version>${powermock-api-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>${mockito-all.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<!-- <plugin>-->
<!-- <!– The gmavenplus plugin is used to compile Groovy code. To learn more about this plugin,-->
<!-- visit https://github.com/groovy/GMavenPlus/wiki –>-->
<!-- <groupId>org.codehaus.gmavenplus</groupId>-->
<!-- <artifactId>gmavenplus-plugin</artifactId>-->
<!-- <version>1.6</version>-->
<!-- <executions>-->
<!-- <execution>-->
<!-- <goals>-->
<!-- <goal>compile</goal>-->
<!-- <goal>compileTests</goal>-->
<!-- </goals>-->
<!-- </execution>-->
<!-- </executions>-->
<!-- </plugin>-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M1</version>
<configuration>
<reportsDirectory>../surefire-reports</reportsDirectory>
<systemPropertyVariables>
<jacoco-agent.destfile>jacoco.exec</jacoco-agent.destfile>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version> <!-- or newer version -->
<configuration>
<source>1.8</source> <!-- depending on your project -->
<target>1.8</target> <!-- depending on your project -->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok-version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</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>
</executions>
</plugin>
</plugins>
</build>
哪个模块需要打桩,就把dependencyManagement 里面的jar包引入到模块里面就可以了
接下来就是划重点的地方了,项目里面需要重新起一个模块用于进行单测结果的收集和聚合,如果想对service层的代码就行聚合的话,添加这个模块的依赖就行了,初次以外,如果有一些文件你觉得不需要进行单测手机,以免影响到你的整个项目的测试覆盖率,那么你可以自定义的去排除这些文件夹或者文件 话不多说 上代码
<?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">
<parent>
<artifactId>dietetic</artifactId>
<groupId>com.dynelm</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>dynelm-dietetic-test</artifactId>
<dependencies>
<dependency>
<artifactId>dynelm-dietetic-service</artifactId>
<groupId>com.dynelm</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.0</version>
<configuration>
<fileSets>
<fileSet>
<directory>..</directory>
<include>**/*.exec</include>
</fileSet>
</fileSets>
<!-- <includes>-->
<!-- <include>com/bytedance/cg/gcrm/contact/**</include>-->
<!-- </includes>-->
<!-- <excludes>-->
<!-- <!–thrift生成的文件过滤掉–>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/remote/**</exclude>-->
<!-- <!–mybatis生成的文件过滤掉–>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/repository/mapper/**</exclude>-->
<!-- <!– mq不支持 –>-->
<!-- <exclude>com/bytedance/cg/locallife/base/mq/**</exclude>-->
<!-- <!–登录过滤器过滤掉–>-->
<!-- <exclude>com/bytedance/cg/locallife/web/interceptor/**</exclude>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/listener/**</exclude>-->
<!-- <!–common定义去掉,不是业务逻辑–>-->
<!-- <exclude>com/bytedance/cg/locallife/web/common/**</exclude>-->
<!-- <!–启动器不单测–>-->
<!-- <exclude>com/bytedance/cg/locallife/web/LocalLifeHttpServiceApplication**</exclude>-->
<!-- <!–demo–>-->
<!-- <exclude>com/bytedance/cg/locallife/web/controller/DemoController.class</exclude>-->
<!-- <exclude>com/bytedance/cg/locallife/web/controller/TestController.class</exclude>-->
<!-- <exclude>com/bytedance/cg/locallife/base/service/impl/TestService.class</exclude>-->
<!-- <exclude>com/bytedance/cg/locallife/base/filter/TccLogbackFilter.class</exclude>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/es/**</exclude>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/adapter/**</exclude>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/rmq/consumer/**</exclude>-->
<!-- <!–这三个都带有导入性质的,写单测成本太高,保证controller和service可用即可–>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/repository/impl/LeadRepositoryImpl.class</exclude>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/repository/impl/IMaterialDataSummaryRepositoryImpl.class</exclude>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/repository/impl/BaseAccountRoleRepositoryImpl.class</exclude>-->
<!-- <!–bsm的不是所有方法都用,暂时规避调–>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/adapter/BsmAdapter.class</exclude>-->
<!-- <!– 账号同步的定时任务不测试 –>-->
<!-- <exclude>com/bytedance/cg/locallife/base/service/impl/AccountSaleBaseRefService.class</exclude>-->
<!-- <!– 数据导入脚本 –>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/listener/IndexOrNameDataListener.class</exclude>-->
<!-- <!–转换器不测试–>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/adapter/converter/**</exclude>-->
<!-- <exclude>com/bytedance/cg/locallife/base/infrastructure/repository/converter/**</exclude>-->
<!-- <!–工具类和配置不测试–>-->
<!-- <exclude>com/bytedance/cg/locallife/base/utils/**</exclude>-->
<!-- <exclude>com/bytedance/cg/locallife/base/entity/query/**</exclude>-->
<!-- <exclude>com/bytedance/cg/locallife/base/conf/**</exclude>-->
<!-- <!–自动生成的不测试–>-->
<!-- <exclude>**.equals**</exclude>-->
<!-- <exclude>**.hashCode**</exclude>-->
<!-- <exclude>**.toString**</exclude>-->
<!-- <exclude>**.canEqual**</exclude>-->
<!-- <!–dto对象过滤掉–>-->
<!-- <exclude>**/dto/**</exclude>-->
<!-- <exclude>**Dto.class</exclude>-->
<!-- <exclude>**/vo/**</exclude>-->
<!-- <exclude>**/req/**</exclude>-->
<!-- </excludes>-->
</configuration>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>merge</goal>
<goal>report-aggregate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
至此,整个收集的就搭建完了,我来测试一下,编写好单测后,输入命令行:mvn clean verify -Dmaven.test.failure.ignore=true 进行收集,我们会在你刚才新建的收集模块的target里面找到一个index.xml文件,右键运行就可以看到整个项目的单测情况,如下所示:
image.png
结果如图:
image.png
红色标注的就是整个项目的行覆盖率,每个包你都能点进行看那些代码覆盖到了,哪些代码没覆盖到,根据这些来进行单测补充。
好的,下次会给大家详细介绍一下,怎么去写单测的代码!