01 单元测试

(一) 什么是单元测试

单元测试是软件开发中的一种测试方法,它对软件中最小可测试单元(方法/函数/模块)进行独立的测试。单元测试独立于用户界面,关注内部逻辑和功能。

(二) 单元测试的作用

单元测试的主要目的是发现程序中的错误,确保代码的正确性。通过编写和运行单元测试,开发人员可以及早发现并修复 bug,从而提高代码质量和开发效率。

02 单元测试框架——Junit

(一) JUnit 介绍

1. 单元测试框架

单元测试框架是一种用于编写和运行单元测试的软件工具。单元测试框架提供了一个标准化的环境,用于组织和执行单元测试。

它的主要功能包括:

  • 提供用例组织与执行: 大量的测试用例堆砌在一起,容易产生了扩展性与维护性等问题

  • 提供丰富的断言方法: 用例执行完之后都需要将实际结果与预期结果相比较(断言),从而断定用例是否执行通过。

  • 提供丰富的日志: 当测试用例执行失败时能抛出清晰的失败原因,当所有用例执行完成后能提供丰富的执行结果。例如,总执行时间、失败用例数、成功用例数等。

从这些特性来看单元测试框架的作用是:帮助我们更自动化完成测试,所以,它是自动化测试的基础。

2. Junit

JUnit 是 Java 语言中最流行和最广泛使用的单元测试框架之一。它提供了断言、测试套件和测试报告等功能。

JUnit 官网:junit.org/

(二) JUnit 安装

JUnit 目前分两个版本:JUnit4 和 JUnit5。JUnit5 在 JUnit4 上新增了一些特性,这里主要介绍 JUnit4安装:打开 Maven 项目的 pom.xml 文件,添加依赖:

  1. xml 代码解读复制代码<dependency>

  2. <groupId>junit</groupId>

  3. <artifactId>junit</artifactId>

  4. <version>4.12</version>

  5. <scope>test</scope>

  6. </dependency>

(三) JUnit 编写单元测试

1. 编写单元测试

假如我要测试的类名称是HelloWorld.Java ,那么我创建的测试类通常称为HelloWorldTest.Java 。

如果涉及到覆盖率统计的话,名称不对有可能会被忽略掉。

一个简单的单元测试用例

  1. typescript 代码解读复制代码import static org.junit.Assert.assertEquals;

  2. import org.junit.Test;

  3. public class HelloWorldTest {

  4. @Test

  5. public void helloTest() {

  6. //逻辑代码

  7. HelloWorld helloWorld = new HelloWorld();

  8. String result = helloWorld.sayHello();

  9. assertEquals("Hello, World!", result);

  10. }

  11. private class HelloWorld {

  12. public String sayHello() {

  13. return "Hello, World!";

  14. }

  15. }

  16. }

@Test:用来注释一个普通的方法为一条测试用例

assertEquals() :方法用于断言两个值是否相等

2. 测试功能模块

被测试类HelloWorld.Java

  1. typescript 代码解读复制代码public class HelloWorld {

  2. public String sayHello(){

  3. return "Hello, World!";

  4. }

  5. }

测试类HelloWorldTest.Java

  1. java 代码解读复制代码public class HelloWorldtTest {

  2. @Test

  3. public void testHello() {

  4. HelloWorld helloWorld = new HelloWorld();

  5. String result = helloWorld.sayHello();

  6. assertEquals("Hello, World!", result);

  7. }

  8. }

先 new 出 HelloWorld 类的实例,调用 hello 方法,通过 assertEquals() 断言返回结果。

03 万能的 Powermock

(一) PowerMock 介绍

PowerMock 扩展了其他流行的测试框架如 JUnit 和 Mockito,提供了更强大的功能来测试各种复杂的场景。它主要用于解决以下问题:

  1. 静态方法测试:PowerMock 可以测试静态方法、构造函数和静态初始化块。

  2. 私有方法测试:PowerMock 可以测试私有方法。final 类和方法测试:PowerMock 可以测试 final 类和 final 方法,即使它们不可覆盖。

  3. 单例模式测试:PowerMock 可以模拟单例类的行为,使得单元测试更加容易编写。

  4. 异常测试:PowerMock 可以更好地测试方法抛出的异常。

PowerMock 实现了"一切皆可 Mock"

PowerMock 官网:Home · powermock/powermock Wiki · GitHub

(二) PowerMock 配置

注解和使用场景

在使用 PowerMock 时需要针对不同场景添加对应注解,主要是 @RunWith 和 @PrepareForTest 注解。

注解添加和场景对应如下所示。

添加依赖

  1. xml 代码解读复制代码<dependency>

  2. <groupId>org.mockito</groupId>

  3. <artifactId>mockito-core</artifactId>

  4. <version>2.23.0</version>

  5. <scope>test</scope>

  6. </dependency>

  7. <dependency>

  8. <groupId>org.powermock</groupId>

  9. <artifactId>powermock-api-mockito2</artifactId>

  10. <version>2.0.2</version>

  11. <scope>test</scope>

  12. </dependency>

  13. <dependency>

  14. <groupId>org.powermock</groupId>

  15. <artifactId>powermock-module-junit4</artifactId>

  16. <version>2.0.2</version>

  17. <scope>test</scope>

  18. </dependency>

在引入依赖时,需要注意核对 Mockito 和 PowerMock 的版本对应关系,否则会报错。版本对应关系可以去 PowerMock 官网进行查询。通常情况下,如果引入的mockito-core版本为 2.x,则PowerMock的 api 需要使用powermock-api-mockito2

(三) PowerMock 使用

1. mock public 方法

  1. typescript 代码解读复制代码public class Mock {

  2. public String sayHello() {

  3. return "Hello, World!";

  4. }

  5. }

  6. public class MockTest {

  7. @Test

  8. public void sayHello2() {

  9. // 创建被测试对象的Mock实例

  10. Mock mock = mock(Mock.class);

  11. // 设置模拟方法的返回值

  12. when(mock.sayHello()).thenReturn("Hello, World!");

  13. // 调用被测试方法

  14. String result = mock.sayHello();

  15. // 验证结果

  16. verify(mock, times(1)).sayHello(); // 验证sayHello方法被调用了一次

  17. assertEquals("Hello, World!", result);

  18. }

  19. }

经典的三步走:

  • a. 使用PowerMockito.mock(方法所在类.class)获取 mock 出来的对象,这里称之为 mock 实例。

    mock 实例的方法均为假方法,不对 mock 实例进行任何操作的情况下,调用 mock 实例的方法会返回(如果有返回值的话)返回值类型的默认值(零值,比如String返回 null,Integer返回 0)。

  • b. 使用PowerMockito.when(mock实例.方法).thenReturn() 设置想要返回的期望值,称为打桩

  • c. 断言

    可以使用whenNew()来实现在程序中 new 一个对象时得到一个 mock 实例。

    在使用 whenNew()时,需要添加 @PrepareForTest 注解,注解的内容是被测试类的 Class 对象。 

如下所示。

  1. scss 代码解读复制代码@RunWith(PowerMockRunner.class)

  2. @PrepareForTest({HelloWorld.class}) // 需要准备进行测试的类

  3. public class HelloWorldTest {

  4. @Test

  5. public void testSayHello() throws Exception {

  6. // 准备需要进行模拟的类

  7. PowerMockito.mockStatic(HelloWorld.class);

  8. // 模拟构造函数的行为

  9. HelloWorld mockedInstance = PowerMockito.mock(HelloWorld.class);

  10. whenNew(HelloWorld.class).withNoArguments().thenReturn(mockedInstance);

  11. // 设置模拟方法的返回值

  12. PowerMockito.when(mockedInstance.sayHello()).thenReturn("Hello, World!");

  13. // 调用被测试方法

  14. HelloWorld helloWorld = new HelloWorld();

  15. String result = helloWorld.sayHello();

  16. // 验证结果

  17. assertEquals("Hello, World!", result);

  18. }

  19. }

2. mock final public 方法

和 mock public 方法操作一致,只是需要添加 @PrepareForTest 注解 。如下所示。

  1. kotlin 代码解读复制代码public class Mock {

  2. public final boolean isTrue() {

  3. return true;

  4. }

  5. }

  6. @RunWith(PowerMockRunner.class)

  7. @PrepareForTest(Mock.class)

  8. public class PowerMockTest {

  9. @Test

  10. public void mockFinalPublic() {

  11. Mock mock = PowerMockito.mock(Mock.class);

  12. PowerMockito.when(mock.isTrue()).thenReturn(false);

  13. assertThat(mock.isTrue(), is(false));

  14. }

  15. }

3. mock private 方法

mock private 方法打桩时,需要使用PowerMockito.when(mock实例,"私有方法名",参数).thenReturn(期望返回值)的形式设置 mock 实例的私有方法的返回值。

如果私有方法有参数,还需要在私有方法名后面添加参数占位符,比如PowerMockito.when(mock实例,"私有方法名",anyInt()).thenReturn(期望返回值)。

  1. java 代码解读复制代码public class Mock {

  2. public boolean isTrue() {

  3. return returnTrue();

  4. }

  5. private boolean returnTrue() {

  6. return true;

  7. }

  8. }

  9. @RunWith(PowerMockRunner.class)

  10. @PrepareForTest(Mock.class)

  11. public class MockTest {

  12. @Test

  13. public void isTrueTest() throws Exception {

  14. Mock mock = PowerMockito.spy(new Mock());

  15. when(mock, "returnTrue").thenReturn(false);

  16. boolean result = mock.isTrue();

  17. assertEquals(false, result);

  18. }

  19. }

如果仅仅只是验证一个私有方法,可以使用Whitebox来方便的调用私有方法

  1. java 代码解读复制代码public class Mock {

  2. private boolean returnTrue() {

  3. return true;

  4. }

  5. }

  6. @RunWith(PowerMockRunner.class)

  7. @PrepareForTest(Mock.class)

  8. public class MockTest {

  9. @Test

  10. public void returnTrueTest() throws Exception {

  11. Mock mock = new Mock();

  12. boolean result = Whitebox.invokeMethod(mock, "returnTrue");

  13. assertTrue(result);

  14. }

  15. }

4. 属性注入 field

现有一个待测试的类 UserServiceImpl,该类中注入了一个 UserMapper 的类实例。

  1. kotlin 代码解读复制代码@Service

  2. public class UserServiceImpl {

  3. @Autowried

  4. private UserMapper userMapper;

  5. ........省略部分方法

  6. }

对这个类进行测试的时候,我们往往不仅需要 Mock 测试类,还要 Mock 测试类中的属性,并保证 mock 实例在执行方法时,如果用到某个属性,真正操作的是我们 Mock 后的属性。也就是需要 mock 属性,并注入到 mock 类 。

这个功能的实现往往依赖两个注解:

  • @Mock: 注解修饰会 mock 出来一个对象,这里 mock 出来的是 UserMapper 类实例。

  • @InjectMocks : 注解会主动将已存在的 mock 对象注入到 bean 中,按名称注入,这个注解修饰在我们需要测试的类上。必须要手动 new 一个实例,不然单元测试会有问题。

  1. java 代码解读复制代码@RunWith(PowerMockRunner.class)

  2. public class UserServiceImplTest {

  3. @Mock

  4. private UserMapper userMapper;

  5. @InjectMocks

  6. private UserServiceImpl userServiceImpl = new UserServiceImpl();

  7. }

@InjectMocks 和 @Mock 注解配合使用可以帮我们做自动注入,但是这样在单机环境下由于 spring 容器在启动的时候会自动完成很多初始化工作,一来比较耗时,二来会去连接一些其他中间件比方说配置中心等,单机下就会出现异常。

那么我们就需要 PowerMock 的 field 方法来帮助我们做一些装配的工作,通常这部分内容我们会放在 @Before 下面,因为每个测试用例都会使用到。

  1. csharp 代码解读复制代码@Before

  2. public void setup() throws Exception(){

  3. PowerMockito.field(UserServiceImpl.class, "userMapper").set(userService, userMapper);

  4. }

04 轻量 Mock 工具——TestableMock

(一) TestableMock 介绍

TestableMock ,一款特立独行的轻量 Mock 工具。能够快速地对各种方法,包括私有方法、静态方法、final 方法进行 mock,简化了 mock 的过程,实现模块之间的解耦,使得测试人员能够更加关注代码的功能和实现逻辑。

TestableMock现在已不仅是一款轻量易上手的单元测试 Mock 工具,更是以简化 Java 单元测试为目标的综合辅助工具集,包含以下功能:

  • 快速 Mock 任意调用:使被测类的任意方法调用快速替换为 Mock 方法,实现"指哪换哪",解决传统 Mock 工具使用繁琐的问题。

  • 访问被测类私有成员:使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题。

  • 快速构造参数对象:生成任意复杂嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题。

  • 辅助测试 void 方法:利用 Mock 校验器对方法的内部逻辑进行检查,解决无返回值方法难以实施单元测试的问题。

  • 快速构造集合对象:提供简洁易用的集合构造和常用操作方法,解决准备测试数据时 Java 集合初始化代码冗长的问题。

在测试时使用 TestableMock 的一个重要原因是,在测试覆盖率时,PowerMock 的 @PrepareForTest 会导致一些测试类被忽略,降低覆盖率。采用 TestableMock 来代替可以很好地解决这个问题。

TestableMock 官网:TestableMock (alibaba.github.io)

(二) TestableMock 配置

在项目pom.xml文件中,增加testable-all依赖和maven-surefire-plugin配置,具体方法如下。

建议先添加一个标识 TestableMock 版本的property,便于统一管理:

  1. xml 代码解读复制代码<properties>

  2. <testable.version>0.7.9</testable.version>

  3. </properties>

在dependencies列表添加 TestableMock 依赖:

  1. xml 代码解读复制代码<dependencies>

  2. <dependency>

  3. <groupId>com.alibaba.testable</groupId>

  4. <artifactId>testable-all</artifactId>

  5. <version>${testable.version}</version>

  6. <scope>test</scope>

  7. </dependency>

  8. </dependencies>

最后在build区域的plugins列表里添加maven-surefire-plugin插件(如果已包含此插件则只需添加<argLine>部分配置):

  1. xml 代码解读复制代码<build>

  2. <plugins>

  3. <plugin>

  4. <groupId>org.apache.maven.plugins</groupId>

  5. <artifactId>maven-surefire-plugin</artifactId>

  6. <configuration>

  7. <argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>

  8. </configuration>

  9. </plugin>

  10. </plugins>

  11. </build>

若项目同时还使用了Jacoco的on-the-fly模式(默认模式)统计单元测试覆盖率,则需在<argLine>配置中添加一个@{argLine}参数,添加后的配置如下:

bash 代码解读复制代码<argLine>@{argLine} -javaagent:${settings.localRepository}/com

(三) TestableMock 使用

TestableMock让每个业务类(被测类)关联一组可复用的 Mock 方法集合(使用 Mock 容器类承载),并遵循约定优于配置的原则,按照规则自动在测试运行时替换被测类中的指定方法调用。

实际规则约定归纳起来只有两条:

  • Mock 非构造方法,拷贝原方法定义到 Mock 容器类,加@MockInvoke注解;

  • Mock 构造方法,拷贝原方法定义到 Mock 容器类,返回值换成构造的类型,方法名随意,加@MockNew注解

具体使用方法如下:

1. 前置步骤,准备 Mock 容器

首先为测试类添加一个关联的 Mock 类型,作为承载其 Mock 方法的容器,最简单的做法是在测试类里添加一个名称为Mock的静态内部类。例如:

  1. kotlin 代码解读复制代码public class DemoTest {

  2. public static class Mock {

  3. // 放置Mock方法的地方

  4. }

  5. }

2. 覆写任意类的方法调用

在 Mock 容器类中定义一个有@MockInvoke注解的普通方法,使它与需覆写的方法名称、参数、返回值类型完全一致,并在注解的targetClass参数指定该方法原本所属对象类型。

此时被测类中所有对该需覆写方法的调用,将在单元测试运行时,将自动被替换为对上述自定义 Mock 方法的调用。

例如,被测类中有一处"something".substring(0, 4)调用,我们希望在运行测试的时候将它换成一个固定字符串,则只需在 Mock 容器类定义如下方法:

  1. arduino 代码解读复制代码// 原方法签名为`String substring(int, int)`

  2. // 调用此方法的对象`"something"`类型为`String`

  3. @MockInvoke(targetClass = String.class)

  4. private String substring(int i, int j) {

  5. return "sub_string";

  6. }

当遇到待覆写方法有重名时,可以将需覆写的方法名写到@MockInvoke注解的targetMethod参数里,这样 Mock 方法自身就可以随意命名了。

下面这个例子展示了targetMethod参数的用法,其效果与上述示例相同:

  1. arduino 代码解读复制代码// 使用`targetMethod`指定需Mock的方法名

  2. // 此方法本身现在可以随意命名,但方法参数依然需要遵循相同的匹配规则

  3. @MockInvoke(targetClass = String.class, targetMethod = "substring")

  4. private String use_any_mock_method_name(int i, int j) {

  5. return "sub_string";

  6. }

@MockInvoke 适用于任何方法,包括任意类的 public 方法,被测类自身的成员方法,任意类的静态方法,任意类的 final 方法

3. 覆写任意类的 new 操作

在 Mock 容器类里定义一个返回值类型为要被创建的对象类型,且方法参数与要 Mock 的构造函数参数完全一致的方法,名称随意,然后加上@MockNew注解。

此时被测类中所有用new创建指定类的操作(并使用了与 Mock 方法参数一致的构造函数)将被替换为对该自定义方法的调用。

例如,在被测类中有一处new BlackBox("something")调用,希望在测试时将它换掉(通常是换成 Mock 对象,或换成使用测试参数创建的临时对象),则只需定义如下 Mock 方法:

  1. arduino 代码解读复制代码// 要覆写的构造函数签名为`BlackBox(String)`

  2. // Mock方法返回`BlackBox`类型对象,方法的名称随意起

  3. @MockNew

  4. private BlackBox createBlackBox(String text) {

  5. return new BlackBox("mock_" + text);

  6. }

4. 在 Mock 方法中区分调用来源

通过TestableTool.MOCK_CONTEXT变量为 Mock 方法注入“额外的上下文参数”,从而区分处理不同的调用场景。

例如,在测试用例中验证当被 Mock 方法返回不同结果时,对被测目标方法的影响:

  1. typescript 代码解读复制代码@Test

  2. public void testDemo() {

  3. MOCK_CONTEXT.put("case", "data-ready");

  4. assertEquals(true, demo());

  5. MOCK_CONTEXT.put("case", "has-error");

  6. assertEquals(false, demo());

  7. }

在 Mock 方法中取出注入的参数,根据情况返回不同结果:

  1. csharp 代码解读复制代码@MockInvoke

  2. private Data mockDemo() {

  3. switch((String)MOCK_CONTEXT.get("case")) {

  4. case "data-ready":

  5. return new Data();

  6. case "has-error":

  7. throw new NetworkException();

  8. default:

  9. return null;

  10. }

  11. }

05 自动化生成测试——好用的插件:Testme

(一) Testme 介绍

Testme 是一款免费的能够自动生成单元测试的插件,同时它本身有支持 Powermock 单测的模板。

优点:Spring 的 Bean 生成单测代码时,即使 @Component 这类注解标注,属性通过 Setter 注解注入时,也会自动给添加 @Mock 和 @InjectMock 这类属性。

缺点:默认模板会在生成的方法上都加上 throws Exception

官网地址:TestMe Plugin for JetBrains IDEs | JetBrains Marketplace

(二) Testme 插件安装

安装插件

在需要生成单元测试的类上点击右键,Generate->TestMe->JUnit4(选择提供的模板之一)

(三) Testme 使用模板

在生成样例代码时,也可以创建自己的模板,点击上图的Configure ,进入 Testme 的模板创建页,可以创建或者修改自己的模板。下面是我觉得比较好用的一个模板,是基于 PowerMock 进行操作的:


  1. swift 代码解读复制代码#parse("TestMe macros.java")

  2. #set($hasMocks=$PowerMockBuilder.hasMocks($TESTED_CLASS))

  3. #set($mockBuilder = $PowerMockBuilder)

  4. #if($PACKAGE_NAME)

  5. package ${PACKAGE_NAME};

  6. #end

  7. import org.junit.Assert;

  8. import org.junit.Test;

  9. #if($hasMocks)

  10. import static org.powermock.api.mockito.PowerMockito.*;

  11. import org.powermock.api.mockito.PowerMockito;

  12. import org.powermock.core.classloader.annotations.PowerMockIgnore;

  13. import org.powermock.core.classloader.annotations.PrepareForTest;

  14. import org.powermock.modules.junit4.PowerMockRunner;

  15. import org.junit.Before;

  16. import org.mockito.InjectMocks;

  17. import org.mockito.Mock;

  18. import org.mockito.MockitoAnnotations;

  19. import org.junit.runner.RunWith;

  20. import static org.mockito.ArgumentMatchers.*;

  21. import static org.mockito.Mockito.verify;

  22. import org.junit.runner.RunWith;

  23. import org.powermock.core.classloader.annotations.PowerMockIgnore;

  24. import org.powermock.modules.junit4.PowerMockRunner;

  25. #end

  26. /**

  27. * @author xxx

  28. * @date ${DATE} ${TIME}

  29. */

  30. #parse("File Header.java")

  31. @RunWith(PowerMockRunner.class)

  32. @PowerMockIgnore("javax.management.*")

  33. public class ${CLASS_NAME} {

  34. #renderMockedFields($hasMocks, $TESTED_CLASS)

  35. #renderTestSubjectInit($TESTED_CLASS,$TestSubjectUtils.hasTestableInstanceMethod($TESTED_CLASS.methods),$hasMocks)

  36. #if($hasMocks)

  37. @Before

  38. public void setUp() {

  39. MockitoAnnotations.${PowerMockBuilder.initMocksMethod}(this);

  40. }

  41. private static class Mock{

  42. }

  43. #end

  44. #foreach($method in $TESTED_CLASS.methods)

  45. #if($TestSubjectUtils.shouldBeTested($method))

  46. @Test

  47. public void #renderTestMethodName($method.name)()#if($method.methodExceptionTypes) throws $method.methodExceptionTypes#end {

  48. #if($hasMocks && $PowerMockBuilder.shouldStub($method, $TESTED_CLASS))

  49. #renderMockStubs($method, $TESTED_CLASS)

  50. #end

  51. #if($PowerMockBuilder.hasInternalMethodCall($method, $TESTED_CLASS))

  52. #renderInternalMethodCallsStubs($method, $TESTED_CLASS)

  53. #renderMethodCallWithSpy($method,$TESTED_CLASS.name)

  54. #else

  55. #renderMethodCall($method,$TESTED_CLASS.name)

  56. #end

  57. #if($hasMocks && $PowerMockBuilder.shouldVerify($method,$TESTED_CLASS))

  58. #renderMockVerifies($method,$TESTED_CLASS)

  59. #end

  60. }

  61. #end

  62. #end

  63. }

06 jacoco 覆盖率生成

(一) jacoco 介绍

代码覆盖(Code coverage)是软件测试中的一种度量,描述程序中源代码被测试的比例和程度,所得比例称为代码覆盖率。简单来理解,就是单元测试中代码执行量与代码总量之间的比率。

Java 常用的单元测试覆盖率框架有:JaCoCo、EMMA 和 Cobertura;

JaCoCo 为基于 Java VM 的环境中的代码覆盖率分析提供标准技术。重点是提供一个轻量级,灵活且文档齐全的库,以与各种构建和开发工具集成。

JaCoCo 官方文档:www.eclemma.org/jacoco/trun…

(二) jacoco 配置

引入 Maven 插件

  1. xml 代码解读复制代码<dependency>

  2. <groupId>org.jacoco</groupId>

  3. <artifactId>jacoco-maven-plugin</artifactId>

  4. <version>0.8.7</version>

  5. </dependency>

配置该插件的执行标签

对于运行简单的单元测试,在执行标签中设置的两个目标可以正常工作。最低限度是设置准备代理(prepare-agent)和报告目标(report),配置如下:

  1. xml 代码解读复制代码<plugin>

  2. <groupId>org.jacoco</groupId>

  3. <artifactId>jacoco-maven-plugin</artifactId>

  4. <version>0.8.6</version>

  5. <executions>

  6. <execution>

  7. <id>prepare-agent</id>

  8. <goals>

  9. <goal>prepare-agent</goal>

  10. </goals>

  11. </execution>

  12. <execution>

  13. <id>report</id>

  14. <phase>test</phase>

  15. <goals>

  16. <goal>report</goal>

  17. </goals>

  18. <configuration>

  19. <!--定义输出的文件夹-->

  20. <outputDirectory>target/jacoco-report</outputDirectory>

  21. </configuration>

  22. </execution>

  23. </executions>

  24. </plugin>

prepare-agent: prepare-agent 目标在 JaCoCo 运行时记录执行数据。它记录了执行的行数、回溯的行数等。默认情况下,将执行数据写入文件 target/jacoco-ut.exec。

report: report 目标根据 JaCoCo 运行时记录的执行数据创建代码覆盖率报告。由于我们已经指定了阶段属性,报告将在测试阶段编译后创建。默认从文件 target/jacoco-ut.exec 中读取执行数据,将代码覆盖率报告写入目录target/site/jacoco/index.html

中可以定义输出的文件夹。

其他所有配置的 Goals,可以详见官网www.eclemma.org/jacoco/trun…

(三) jacoco 使用

1. 生成报告

执行 jacoco,命令是mvn package / mvn clean test ,这两个命令都可以

最后,打开文件 target/site/jacoco/index.html,即可查看覆盖率报告

2. 多模块工程覆盖率统计

假如一个代码是多模块工程,如下面的结构:

├── module1 │ └── pom.xml ├── module2 │ └── pom.xml ├── module3 │ └── pom.xml ├── pom.xml

每个模块都这么配置的话,生成的报告是各自独立的,即会生成 3 个报告,那么怎么把各个模块的代码覆盖率统计在一起,生成一个聚合的报告呢?

简单来说,分为两步:

a.新建一个子模块作为聚合模块在聚合模块中配置 jacoco 聚合报告:

  1. xml 代码解读复制代码<plugin>

  2. <groupId>org.jacoco</groupId>

  3. <artifactId>jacoco-maven-plugin</artifactId>

  4. <version>0.8.5</version>

  5. <executions>

  6. <execution>

  7. <id>my-report</id>

  8. <phase>test</phase>

  9. <goals>

  10. <goal>report-aggregate</goal>

  11. </goals>

  12. </execution>

  13. </executions>

  14. </plugin>

b.在聚合模块中引用其他模块(只有引用的模块的覆盖率才会被聚合到报告中)

  1. xml 代码解读复制代码<dependency>

  2. <groupId>@project.groupId@</groupId>

  3. <artifactId>module1</artifactId>

  4. <version>${project.version}</version>

  5. </dependency>

  6. <dependency>

  7. <groupId>@project.groupId@</groupId>

  8. <artifactId>module2</artifactId>

  9. <version>${project.version}</version>

  10. </dependency>

  11. <dependency>

  12. <groupId>@project.groupId@</groupId>

  13. <artifactId>module3</artifactId>

  14. <version>${project.version}</version>

  15. </dependency>

  • 1. 新建一个模块配置 jacoco 的 report-aggregate;

  • 2. 这个模块需要引用所有的其他模块

    具体做法如下:

3. 消除 lombok 对覆盖率测试的影响

Lombok 是一个 Java 库,它可以通过注解来简化 Java 代码的编写。其中,@Data 注解可以自动生成 JavaBean 的 getter、setter、equals、hashCode 和 toString 方法。但是,使用 @Data 注解会影响代码覆盖率,因为自动生成的方法没有被测试覆盖到。

为了处理这个问题,最有效的方法是:

在项目的根目录下新建一个名字为 lombok.config 的文件,里面有如下的内容,

  1. ini 代码解读复制代码config.stopBubbling = true

  2. lombok.addLombokGeneratedAnnotation = true

这个方法要求 Lombok >= 1.16.14, jacoco>0.8.0

07 总结

本篇文章主要介绍了一个 maven 项目如何进行单元测试并统计覆盖率报告。总的说来,使用 JUnit 框架,采用 Powermock + Testablemock 的方式,利用 Testme 插件辅助生成单元测试,最后采用 Jacoco 生成覆盖率报告。

关于其中的每一个工具的使用,更加详细的可以参照官方文档,这里列出的是我认为在写一个项目时必要的使用方法,基本能够让项目的覆盖率达到 70%以上。

感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取   

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐