自动化测试

1.基础知识

1.1.测试分层

image

https://developer.android.com/training/testing/fundamentals

这个是安卓官方对测试金字塔的描述。

  • Small tests是单元测试,在local JVM就可以运行。
  • Medium tests是集成测试,测试集成的几个模块,运行于真机或模拟器。
  • Large tests是UI测试,测试整个UI工作流程,运行于真机或模拟器。

开发写的单元测试和测试写的集成测试区别在哪里?

单元测试测试的测试单元一定是远小于集成测试,主要原因是:单元测试是运行在JVM上,直接去测试某个类或某几个类, 因为没有走整个App正常的启动和调用流程,导致非常多的模块是没有初始化的,为了测试用例不崩溃,必须把需要用到,但没初始化的类或方法mock, 如果测试单元很大,需要mock的东西就非常多,很难保证不出错。而集成测试是运行在真机或模拟器,可以按照App正常的启动和调用流程, 需要mock的东西不多,所以测试单元可以很大。

再者,单元测试是白盒测试。集成测试是黑盒测试。

1.2.单元测试

单元测试,是为了测试某一个类的某一个方法能否正常工作

单元测试目的

  • 验收。提前发现代码中的bug。以后代码有改动或重构,跑下用例就可以了。
  • 帮助开发。开发过程中,可以快速验证自己的代码逻辑,不用一遍遍完整跑起来。提高效率。
  • 规范代码。为了让代码可测,会尽量避免些耦合特别多的代码。以及特别大的类和函数。
public class NormalClassA {
  public int testAdd(int a, int b) {
    return a + b;
  }
}
public class NormalClassATest {
  @Test
  public void testAdd() {
    NormalClassA classA = new NormalClassA();
    Assert.assertEquals(5, classA.testAdd(1,4));
  }
}
  1. setup: 一般是new出你要测试的那个类,以及其他一些前提条件的设置:NormalClassA classA = new NormalClassA();
  2. 执行操作: 一般是调用你要测试的那个方法,获得运行结果:classA.testAdd(1,4)
  3. 验证结果: 验证得到的结果跟预期中是一样的:Assert.assertEquals(5, classA.testAdd(1,4));

《单元测试的艺术》一书中的优秀的单元测试特性

优秀单元测试的特性 一个单元测试应当具有如下特征:口它应该是自动化的,可重复执行;口它应该很容易实现;

  • 它应该第二天还有意义;
  • 任何人都应该能一键运行它;口它应该运行速度很快;
  • 它的结果应该是稳定的(如果运行之间没有进行修改的话,多次运行一个测试应该总是返回同样的结果);
  • 它应该能完全控制被测试的单元;
  • 它应该是完全隔离的(独立于其他测试的运行);
  • 如果它失败了,我们应该很容易发现什么是期待的结果,进而定位问题所在。

1.3.工具链

1.3.1.单元测试工具

谷歌推荐的是:junit4 + mockito + robolectric

我们最终选择的工具链:junit4 + mockito + powermock + robolectric

  • JUnit:JUnit 是 Java 中最流行的单元测试框架之一,它提供了一组用于编写和运行测试的 API 和注解。JUnit 可以帮助你编写可靠的单元测试,并提供了丰富的断言和测试运行器。
  • TestNG:TestNG 是另一个流行的 Java 单元测试框架,它与 JUnit 类似,但提供了更多的特性和灵活性。TestNG 支持测试分组、依赖性管理、参数化测试等功能。
  • mock+powermock:mockito功能有点弱,很多功能受限。 powermock可以跟mockito兼容而且功能更强。 与mockito比,支持修改和mock静态类或对象的私有方法/成员,还支持很多反射方法,非常实用。 powermock目前使用人数非常多,github上的star很高。
  • robolectric:可以比较完整的模拟安卓接口。这样case就可以运行在jvm上。目前这个没有其他类似框架,无可替代,而且谷歌官方推荐。 这套框架运行于JVM,不需要安卓模拟器和手机。

1.3.2.自动生成单测代码

  • idea和eclipse支持生成测试类,包括测试的方法等初级操作。
  • Squaretest。【idea插件,收费】非常好用、非常智能的单测代码生成工具。官网:https://squaretest.com/
  • TestMe。【idea插件】自动生成 Java JUnit 4/5, TestNG 单元测试 自动生成 Mockito mocks 自动生成 测试参数和断言语句 自动生成相关 mock 方法

1.3.3.UI测试

  • Selenium。用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。

1.3.4.单测高效构造参数和返回值神器

  • easy-random。Java高效构造对象的神器。
  • java-faker。Java 单元测试生成测试字符串的神器
    • 功能:如果我们想要随机构造人名、地名、天气、学校、颜色、职业,甚至符合某正则表达式的字符串
    • 官网:https://github.com/DiUS/java-faker

1.3.5.单测覆盖率报告

1.3.6.性能测试

  • JMeter:JMeter 是一个用于性能测试的工具,它可以模拟多种负载类型,并提供了丰富的图表和报告来帮助你分析测试结果。JMeter 可以测试 Web 应用程序、数据库、FTP 服务器等多种系统。
  • Apache Bench:Apache Bench 是一个简单但功能强大的命令行工具,用于测试 Web 服务器性能。它可以模拟多个并发用户,并生成详细的测试报告。
  • ContiPerf:轻量级的测试工具能轻松地利用的 JUnit 4 的性能测试

1.4.如何在android project中运行单元测试

1.4.1.安卓自动化测试有两种:
  1. Local tests:运行与JVM上
  2. Instrumented tests:运行于手机或模拟器,因为要依赖Android Framework,例如Context等。

image

新建模块时会自动生成androidTest和test两个目录。androidTest中的测试用例对应上面提到的 Instrumented tests。 test中的测试用例对应上面提到的Local tests

1.4.2.创建一个测试类

image

image

image

test的同级目录下就会生成测试类文件

1.4.2.如何运行?

1.4.2.1.方法一:通过Android Studio运行

image

函数内右键,运行单个测试用例

image

在类名上右键,运行该类的所有测试用例

1.4.2.2.方法二:通过命令行

在Terminal中运行:gradlew testDebug –continue

通过命令行运行后,会生成测试用例的报告,用Android Studio运行不会生成。

报告目录

对应模块的build目录下,build\reports\tests\testDebugUnitTest

以m_news_list模块为例:TencentNews\m_news_list\build\reports\tests\testDebugUnitTest

image

1.5.两种函数不同的测试方法

函数有两种:验证返回值和验证函数调用。

1.5.1.有返回值函数

既可以验证返回值,也可以验证函数调用

1.5.2.无返回值

验证函数调用

public class NormalClassA {
  public NormalClassB mClassB = new NormalClassB();
  public int testPlus(int a, int b) {
      return mClassB.testPlus(a, b);
  }
}

public class NormalClassB {
  public int testPlus(int a, int b) {
      return a + b;
  }
}

public class NormalClassATest {
  @Test
  public void testPlusInvokeMock() {
    NormalClassA classA = new NormalClassA();
    classA.mClassB = Mockito.mock(NormalClassB.class);
    //验证返回值
    Assert.assertEquals(6,   classA.testPlus(3,3));
    //验证函数调用,并且参数是3,3
    Mockito.verify(classA.mClassB).testPlus(3, 3);
  }
}

1.6. 单元测试基本规范

1.6.1.测试用例存放目录

单元测试文件放在对应模块的test目录下

1.6.2.单元测试代码规范

  • 测试类的包名:与被测类的包名保持一致
  • 测试类命名方式:被测类名+Test。示例:
  • 被测类名AppDownload,则测试类名为 AppDownloadTest
  • 测试用例命名方式:test+首字母大写的
  • 测方法名,如果同一个逻辑要测不同分支,产生多个同类测试用例的情况,用例前缀需保持一致,后面跟"_xxxx_xxxx",xxxx明确分支意图。格式为大写开头驼峰。。示例:
  • 被测方法名sendRequest,则测试用例名称 为testSendRequest,如有多个,则为 testSendRequest_HasLogin_Success, testSendRequest_HasLogin_Fail,… 用例命名规范参考谷歌代码: https://cs.android.com/android/platform/superproject/+/master:packages/apps/WallpaperPicker2/tests/src/com/android/wallpaper/picker/individual/IndividualPickerActivityTest.java
  • 测试类必须有setup()、cleanup()方法,用 于初始化和资源释放。
  • 测试用例必须有断言。

1.6.3.单元测试验证标准

通过代码覆盖率判断,代码覆盖率包含:行覆盖,函数覆盖,分支覆盖。

1.6.4.单元测试验证标准

通过代码覆盖率判断,代码覆盖率包含:行覆盖,函数覆盖,分支覆盖。

1.6.5.那些需要做单元测试

非UI的逻辑代码,例如:工具方法、MVP中的P。

1.6.6.代码架构规范

保持UI和逻辑的分析,推荐采用MVP模式,方便单元测试。

2.JUnit4

2.1.引入依赖

dependencies {testCompile 'junit:junit:4.12'}

2.2.常用注解

  • @Test代表一条测试用例。
  • @Before修饰的方法,在每个测试方法调用之前,这个方法都会得到调用。所以上面的例子中,testAdd()被运行之前,setup()会被调用一次
  • @After对应于@Before,每个测试方法运行结束之后,会运行的方法。比如一个测试文件操作的类,那么在它的测试类中,可能@Before里面需要去打开一个文件,而每个测试方法运行结束之后,都需要去close这个文件。这个时候就可以把文件close的操作放在@After里面,让它自动去执行。
  • @BeforeClass,@AfterClass。@BeforeClass的作用是,在跑一个测试类的所有测试方法之前,会执行一次被@BeforeClass修饰的方法,执行完所有测试方法之后,会执行一遍被@AfterClass修饰的方法。这两个方法可以用来setup和release一些公共的资源,需要注意的是,被这两个annotation修饰的方法必须是静态的。

这是比较常用的注解,其他注解可以参考下表:

  • @Test :表示此方法为测试方法
  • @Before :在每个测试方法前执行,可做初始化操作
  • @After :在每个测试方法后执行,可做释放资源操作
  • @Ignore :忽略的测试方法
  • @BeforeClass :在类中所有方法前运行。此注解修饰的方法必须是static void
  • @AfterClass :在类中最后运行。此注解修饰的方法必须是static void
  • @RunWith :指定该测试类使用某个运行器
  • @Parameters :指定测试类的测试数据集合
  • @Rule :重新制定测试类中方法的行为
  • @FixMethodOrder :指定测试类中方法的执行顺序

2.3.执行顺序

2.4.Assert类

  • assertNotEquals :断言传入的预期值与实际值是不相等的
  • assertArrayEquals :断言传入的预期数组与实际数组是相等的
  • assertNull :断言传入的对象是为空
  • assertNotNull :断言传入的对象是不为空
  • assertTrue :断言条件为真
  • assertFalse :断言条件为假
  • assertSame :断言两个对象引用同一个对象,相当于"=="
  • assertNotSame :断言两个对象引用不同的对象,相当于"!="
  • assertThat :断言实际值是否满足指定的条件

通过Assert类验证结果是否符合预期

2.5.assertThat验证结果

Assert.assertEquals(4, classA.testAdd(1,4)); 出错时的提示:junit.framework.AssertionFailedError: Expected :4Actual :5

assertThat(4, equalTo(5)); 出错时的提示: java.lang.AssertionError: Expected: <5>but: was <4>Expected :<5> Actual :<4>

assertThat的作用跟Assert类似,提供的方法更多一些,返回错误也更容易读。

匹配器 说明 例子

  • is :断言参数等于后面给出的匹配表达式
  • not :断言参数不等于后面给出的匹配表达式
  • equalTo :断言参数相等
  • equalToIgnoringCase :断言字符串相等忽略大小写
  • containsString :断言字符串包含某字符串
  • startsWith :断言字符串以某字符串开始
  • endsWith :断言字符串以某字符串结束
  • nullValue :断言参数的值为null
  • notNullValue :断言参数的值不为null
  • greaterThan :断言参数大于
  • lessThan :断言参数小于
  • greaterThanOrEqualTo :断言参数大于等于
  • lessThanOrEqualTo :断言参数小于等于
  • closeTo :断言浮点型数在某一范围内
  • allOf :断言符合所有条件,相当于&&
  • anyOf :断言符合某一条件,相当于或
  • hasKey :断言Map集合含有此键
  • hasValue :断言Map集合含有此值
  • hasItem :断言迭代对象含有此元素

3.Mockito

3.1.为什么要Mock

当你想进行单元测试的时候,立刻会遇到几个致命的问题。

  1. 依赖外部环境。例如需要连接数据库,如果数据库没有启动或、网络不同、配置错误的时候,测试就会失败,但这并非是程序的错误。
  2. 测试速度慢:涉及数据库或网络 I/O的操作常缓慢。成百上千个这样的测试会让整个构建过程变得无法忍受。
  3. 难以控制场景:如何测试"用户名已存在"的场景?你必须先在数据库里插入一个同名用户。如何测试"数据库保存失败的场景?这几乎无法在测试中稳定地模拟。
  4. 测试不独立:一个测试在数据库里插入了数据可能会影响到下一个测试的结果,这破坏了测试的独立性。

Mockito 的解决方案:以 UserService 调用 UserRepository 为例

Mockito 通过创建 模拟对象(Mock Object) 来斩断这些依赖,从而完美地解决了上述所有问题

  1. 隔离(lsolation):Mockito 会创建一个 UserRepository 的"假"对象(替身)。这个假对象完全不连接数据库。你的UserService和这个假对象交互,从而与数据库完全隔离。
  2. 速度(Speed):所有操作都在内存中完成,速度极快。
  3. 控制(Control):你可以像导演一样为这个假"对象编写剧本。
    • 想测试"用户名已存在"? when(userRepository.findByUsername("existingUser")).thenReturn(newUser());
    • 想测试"用户不存在"? when(userRepository.findByUsername("newUser")).thenReturn(null);
    • 想测试"数据库保存失败"? ption(when(userRepository.save(any(User.class))).thenThrow(new DataAccessExcer.class("….")));
  4. 验证(verification):不仅可以控制方法的返回值,还能在测试后验证UserService是否按照预期完成与UserRepository的交互。

实现原理:在运行时创建代理对象,字节码增强使用的事byte Buddy实现的。

3.1.1.【核心原则-隔离】解决测试类对其他类的依赖

在实际的单元测试中,我们要测试的类很可能对其他类有依赖,要测试的方法对其他方法或变量有依赖,如果直接运行这个要测试的方法,很大可能会因为某些变量没初始化NP。这样我们就无法顺利的进行测试。如果mock方法模拟一个类,最大的特点是,该类所有的方法都是空,所有变量都是初始值。这样这个类就对其他类几乎没有依赖了。然后只针对你要测试的方法调用doCallRealMethod()真正执行方法的实现。这样依赖就很少了,就比较可控了。再把依赖的方法mock,方法运行了。

public class JudgePresenter extends AbstractJudgePresenter {
    public JudgePresenter(IJudgeContract.IView mView, QuestionInfo mCurrentQuestionInfo) {
        super(mView, mCurrentQuestionInfo);
    }

    @Override
    public void initUIData() {
        mView.setBg();
        updateLiveViewerCount();
    }

    private void updateLiveViewerCount() {
        DataLoader.getLiveViewerBuilder(GlobalLocalData.getInstance().getArticleId())
                .response(new TNResponseCallBack<LiveViewersRet>())
                .build()
                .submit();
    }
}

针对initUIData写测试用例:

@Test
public void initUIDataTest() {
  IJudgeContract.IView mockView = Mockito.mock(JudgeDialog.class);
  JudgePresenter presenter = new JudgePresenter(mockView, getFakeQuestionInfo());
  presenter.initUIData();
  Mockito.verify(mockView).setBg();
}

直接运行测试用例,会报错,提示网络库某个方法NP了。 这是因为initUIDataTest()调到了updateLiveViewerCount(),该函数中调用到了网络请求库,而网络库模块是需要初始化的,所以有些值为null,导致NP。那只要把网络库初始化一下,不就可以了吗?然后如果尝试去初始话网络库,就会发现,初始化网络库又需要一堆依赖…,这就把问题搞复杂了。回到最初的目的,我们只是想测试initUIDataTest方法,我们只关心setBg()被调用了,至于updateLiveViewerCount方法的执行过程,不属于我们这个测试用例考虑的范畴, 所以这里我们其实只需要mock一下updateLiveViewerCount方法,让他不执行就可以。用PowerMockito来实现,代码如下

PowerMockito.suppress(PowerMockito.method(JudgePresenter.class, "updateLiveViewerCount"));

所以完整的测试代码:

@Test
public void initUIDataTest() {
  IJudgeContract.IView mockView = Mockito.mock(JudgeDialog.class);
  JudgePresenter presenter = new JudgePresenter(mockView, getFakeQuestionInfo());
  PowerMockito.suppress(PowerMockito.method(JudgePresenter.class, "updateLiveViewerCount"));
  presenter.initUIData();
  Mockito.verify(mockView).setBg();
}

3.1.2.验证方法的调用

只有对mock的对象或类,才能调用verify方法,验证方法的调用。

举个例子:

@Test
public void initUIDataTest() {
    IJudgeContract.IView mockView = Mockito.mock(JudgeDialog.class);
    JudgePresenter presenter = new JudgePresenter(mockView, getFakeQuestionInfo());
    PowerMockito.suppress(PowerMockito.method(JudgePresenter.class, "updateLiveViewerCount"));
    presenter.initUIData();
    Mockito.verify(mockView).setBg();
}

为了验证JudgeDialog中setBg()方法被调用了,需要mock一个JudgeDialog。

3.2. Mockito

Mockito是安卓官方推荐的,也是目前使用最广泛的。

3.2.1.用法

dependencies {
  testCompile "org.mockito:mockito-core:2.8.9"
}
@RunWith(MockitoJUnitRunner.class)
public class NormalClassAMockitoTest {

    @Test
    public void testAPlusInvokeMock() {
        NormalClassA mockClassA = Mockito.mock(NormalClassA.class);
        mockClassA.testPlus(3,3);
        Mockito.verify(mockClassA).testPlus(3, 3);
    }
}

这个例子中,mock了NormalClassA用于验证该对象的testPlus()方法是否被调用了。注意类前面的注解@RunWith(MockitoJUnitRunner.class) 指明本类中的用例用MockitoJUnitRunner运行。

3.2.2 Mock和Spy的区别

3.2.2.1 Mock方法创建的对象,所有方法都是空方法。非void方法都将返回默认值,比如int方法将返回0,对象方法将返回null等,而void方法将什么都不做。
3.2.2.2 Spy方法创建的对象,跟正常类对象一样,是正常对象的替身。
3.2.2.3 被测试代码:
public class NormalClassB {

    public int testPlus(int a, int b) {
        return a + b;
    }

}
3.2.2.4 用Mock方式的代码:
@Test
public void testPlusValueMock() {
    NormalClassA classA = new NormalClassA();
    classA.mClassB = Mockito.mock(NormalClassB.class);
    Assert.assertEquals(0, classA.testPlus(3, 3));
}

在这个例子中,Mock了NormalClassB,所以它的testPlus方法就会是空的。调用classA.testPlus(3, 3)后,将会返回int的默认值0,并不会返回6。

3.2.2.5 用Spy方式的代码:
@Test
public void testPlusValueSpy() {
    NormalClassA classA = new NormalClassA();
    classA.mClassB = Mockito.spy(new NormalClassB());
    Assert.assertEquals(6, classA.testPlus(3, 3));
}

在这个例子中,Spy了NormalClassB,它的testPlus方法不为空,是具体实现的。调用classA.testPlus(3, 3)后,将会返回6。

好的,让我来帮您整理一下这个Markdown的格式。同样的,我会将标题的层次结构进行调整,将代码块语法应用于代码,并将更改应用于Markdown中。

以下是更改后的Markdown:

3.2.3 Mock和Spy各自适用的场景

3.2.3.1 Mock

Mock适用于类中大多数方法只需要验证是否被调用,而不关心方法内部执行结果的场景。通过让类的所有方法都为空实现,可以减少对其他类的依赖。

如果想要在Mock对象中执行某个函数的具体实现,该怎么办呢?可以使用doCallRealMethod()方法。

@Test
public void testPlusValueMockWithCallRealMethod() {
    NormalClassA classA = new NormalClassA();
    classA.mClassB = Mockito.mock(NormalClassB.class);
    doCallRealMethod().when(classA.mClassB).testPlus(Mockito.anyInt(), Mockito.anyInt());
    Assert.assertEquals(6, classA.testPlus(3, 3));
}
3.2.3.2 Spy

Spy与Mock相反,适用于类中大部分函数需要关注具体实现的场景。

如果想要在Spy对象中,某个函数不执行,该怎么办呢?有两种情况:有返回值和无返回值。

3.2.3.3 有返回值的函数
@Test
public void testPlusValueSpyWithDoReturn() {
    NormalClassA classA = new NormalClassA();
    classA.mClassB = Mockito.spy(NormalClassB.class);
    doReturn(0).when(classA.mClassB).testPlus(Mockito.anyInt(), Mockito.anyInt());
    Assert.assertEquals(0, classA.testPlus(3, 3));
}
3.2.3.4 无返回值的函数
@Test
public void testPlusValueSpyWithDoThrow() throws Exception {
    NormalClassA classA = new NormalClassA();
    classA.mClassB = Mockito.spy(NormalClassB.class);
    doNothing().when(classA.mClassB).voidMethod(Mockito.anyInt(), Mockito.anyInt());
}

这些是Mockito的方法。后面我们将介绍PowerMock,它还提供了其他方法来禁用某个函数的执行。

3.2.4. 打桩方法

在Java中,我们常常需要使用Mockito等框架进行单元测试。在这些框架中,打桩方法是一个非常重要的概念。其中,doCallRealMethod(),doReturn()和doNothing()是我们经常使用的打桩方法。除此之外,还有以下全部打桩方法:

方法名 方法描述
thenReturn(T value) 设置要返回的值
thenThrow(Throwable… throwables) 设置要抛出的异常
thenAnswer(Answer<?> answer) 对结果进行拦截
doReturn(Object toBeReturned) 提前设置要返回的值
doThrow(Throwable… toBeThrown) 提前设置要抛出的异常
doAnswer(Answer answer) 提前对结果进行拦截
doCallRealMethod() 调用某一个方法的真实实现
doNothing() 设置void方法什么也不做
3.2.5. 验证方法

在Mockito中,Mockito.verify()用于验证方法是否被调用,调用次数等。下面是一些常用的验证方法:

方法名 方法描述
after(long millis) 在给定的时间后进行验证
timeout(long millis) 验证方法执行是否超时
atLeast(int minNumberOfInvocations) 至少进行n次验证
atMost(int maxNumberOfInvocations) 至多进行n次验证
description(String description) 验证失败时输出的内容
times(int wantedNumberOfInvocations) 验证调用方法的次数
never() 验证交互没有发生,相当于times(0)
only() 验证方法只被调用一次,相当于times(1)

以下是一个示例代码:

@Test
public void testAPlusInvokeMock() {
    NormalClassA mockClassA = Mockito.mock(NormalClassA.class);
    mockClassA.testPlus(3,3);
    Mockito.verify(mockClassA).testPlus(3, 3);
    Mockito.verify(mockClassA, times(1)).testPlus(3, 3);
    Mockito.verify(mockClassA, atLeast(1)).testPlus(3, 3);
}

3.2.6.参数匹配器

@RunWith(MockitoJUnitRunner.class)
public class NormalClassAMockitoTest {
    @Test
    public void testAPlusInvokeMock() {
        NormalClassA mockClassA = Mockito.mock(NormalClassA.class);
        mockClassA.testPlus(3,3);
        Mockito.verify(mockClassA).testPlus(3, 3);
    }
}

Mockito.verify(mockClassA).testPlus(3, 3); 这行验证成立条件有2个:

  1. testPlus被调用。
  2. testPlus的参数是3,3。

如果调用 mockClassA.testPlus(3,4); 验证是失败,因为参数不一致。 那么如何忽略具体的参数呢。这就需要用到参数匹配器。

Mockito.verify(mockClassA).testPlus(Mockito.anyInt(),Mockito.anyInt());

这样写,只要参数是整型就能验证成功。

3.2.6.1.需要注意的特殊case
  • 一个参数确定,另一个参数是匹配器
  • Mockito.verify(mockClassA).testPlus(3 ,Mockito.anyInt());
  • 如果这样写,能配置成功吗? 答案是不能。 正确写法: Mockito.verify(mockClassA).testPlus(Mockito.eq(3) ,Mockito.anyInt());
  • 匹配非基础类型
    //如果函数原型是这样:
    void putItemToReportMap(String channel, Properties prop, HashMap<String, HashSet<Properties>> map)
    //正确的验证方法:
    Mockito.verify(mockClassA).putItemToReportMap(Mockito.anyString(), Mockito.any(Properties.class), Mockito.anyMap());
    
方法名 方法描述
anyObject() 匹配任何对象
any(Class type) 与anyObject()一样
any() 与anyObject()一样
anyBoolean() 匹配任何boolean和非空Boolean
anyByte() 匹配任何byte和非空Byte
anyCollection() 匹配任何非空Collection
anyDouble() 匹配任何double和非空Double
anyFloat() 匹配任何float和非空Float
anyInt() 匹配任何int和非空Integer
anyList() 匹配任何非空List
anyLong() 匹配任何long和非空Long
anyMap() 匹配任何非空Map
anyString() 匹配任何非空String
contains(String substring) 参数包含给定的substring字符串
argThat(ArgumentMatcher matcher) 创建自定义的参数匹配模式

3.2.7.其他方法

以下是Mockito库中一些其他方法的介绍:

方法名 方法描述
reset(T … mocks) 重置Mock对象
spy(Class classToSpy) 创建一个真实的对象,并记录该对象的所有方法调用
inOrder(Object… mocks) 验证执行顺序
@InjectMocks注解 自动将模拟对象注入到被测试对象中

其中,@InjectMocks注解可以自动将模拟对象注入到被测试对象中,以便进行单元测试。需要注意的是,使用@InjectMocks注解时,被测试对象中的模拟对象必须已经创建并初始化。 另外,在使用@InjectMocks注解时,需要确保正确地配置了依赖关系,以便正确地注入模拟对象。

3.3.Mockito局限性

针对上面的代码,可以使用Mockito框架编写单元测试。以下是一个测试initUIData()方法,需要验证mView.setBg()、updateLiveViewerCount被调用的示例:

public class JudgePresenterTest {

    private IJudgeContract.IView mView = Mockito.mock(IJudgeContract.IView.class);
    private QuestionInfo mCurrentQuestionInfo = Mockito.mock(QuestionInfo.class);
    private JudgePresenter mPresenter = new JudgePresenter(mView, mCurrentQuestionInfo);

    @Test
    public void testInitUIData() {
        mPresenter.initUIData();
        Mockito.verify(mView).setBg();
        Mockito.verify(mPresenter).updateLiveViewerCount();
    }

}

在这个测试中,我们首先创建了一个JudgePresenter实例,并将其与一个模拟的IJudgeContract.IView和QuestionInfo对象一起传递给构造函数。 然后,我们调用initUIData()方法,并使用Mockito.verify()方法验证了mView.setBg()和mPresenter.updateLiveViewerCount()方法是否被调用。 需要注意的是,由于updateLiveViewerCount()方法是私有的,因此我们无法直接调用它。 但是,我们可以通过验证mPresenter.updateLiveViewerCount()方法是否被调用来验证该方法是否被正确执行。

需要注意的是,Mockito框架有一些限制,例如无法访问私有方法、mock匿名类、final类、static方法等。

4.PowerMock

4.1.PowerMock介绍

PowerMock是Mockito的扩展增强版,支持mock private、static、final方法和类,还增加了很多反射方法可以方便修改静态和非静态成员等。功能比Mockito增加很多。

为什么 Mockito 不自己支持 private 方法?

image

https://github.com/mockito/mockito/wiki/Mockito-And-Private-Methods

简单点说:

  1. 改动较大,不是特别必要。
  2. powermock已经实现了。
  3. 被测代码不够规范

4.2.PowerMock版本

PowerMock是依赖Mockito的,所以使用时要一同引入。 PowerMock跟Mockito的版本必须对应:

https://github.com/powermock/powermock/wiki/Mockito

image

目前我们使用的版本:

testCompile 'junit:junit:4.12'
testCompile "org.mockito:mockito-core:2.8.9"
testCompile "org.powermock:powermock-module-junit4:1.7.1"
testCompile "org.powermock:powermock-api-mockito2:1.7.1"//注意,这里要用mockito2,否则报错
testCompile 'org.powermock:powermock-core:1.7.1'
testCompile "org.powermock:powermock-module-junit4-rule:1.7.1"
testCompile "org.powermock:powermock-classloading-xstream:1.7.1"

4.3.基本使用

@RunWith(PowerMockRunner.class)
@PrepareForTest({NormalClassA.class})
public class NormalClassAPowerMockTest {
  @Test
  public void testPrivateMethod() throws Exception {
    NormalClassA classA = new NormalClassA();
    NormalClassA mockClassA = PowerMockito.spy(classA);
    mockClassA.publicAdd(2, 3);
    PowerMockito.verifyPrivate(mockClassA, times(1)).invoke("privateAdd",
    Mockito.anyInt(), Mockito.anyInt());
  }
}
  • @RunWith(PowerMockRunner.class),使用PowerMockRunner,使用这个Runner时,跟Mockito是兼容的,Mockito中的各种方法也是可以正常使用的。
  • @PrepareForTest({NormalClassA.class}), PowerMock要mock的类一定要写在这个注解里,切记切记! 写的时候实在太容易忘了,而且,就算不写也不会报错,只是verify永远成功,所以特别隐蔽,忘记写了也很难发现。
  • 通过PowerMockito.verifyPrivate验证私有方法的调用。Mocktio的次数验证times(1)和参数匹配符也都是可以用的。

4.4.针对对象的操作

4.4.1.读取对象私有成员

public class ObjectWhiteBoxClass {
    private final List<String> mObjectList = new ArrayList<>();

    public void publicObjectAdd(String str) {
        mObjectList.add(str);
    }

    public List<String> getObjectList() {
        return mObjectList;
    }
}

@Test
public void testGetInternalState() {
    ObjectWhiteBoxClass objectWhiteBoxClass = new ObjectWhiteBoxClass();
    objectWhiteBoxClass.publicObjectAdd("test1str");
    objectWhiteBoxClass.publicObjectAdd("test2str");
    objectWhiteBoxClass.publicObjectAdd("test3str");

    List<String> list = Whitebox.getInternalState(objectWhiteBoxClass, "mObjectList");

    Assert.assertEquals(3, list.size());
    Assert.assertEquals("test3str", list.get(list.size()-1));
}

4.4.2.修改对象私有成员

java
Copy code
public class MockPrivateObjectClass {
    private InnerClass innerClass;

    public MockPrivateObjectClass() {

    }

    public int doPlus(int a, int b) {
        return innerClass.plus(a, b);
    }

    public static class InnerClass {
        public int plus(int a, int b) {
            return a + b;
        }
    }
}
4.4.2.1.第一种方式:Whitebox.setInternalState
@RunWith(PowerMockRunner.class)
@PrepareForTest({MockPrivateObjectClass.class})
public class MockPrivateObjectClassTest {
    @Test
    public void doPlusTest() {
        MockPrivateObjectClass objectClass = new MockPrivateObjectClass();
        MockPrivateObjectClass.InnerClass mockInnerClass = Mockito.spy(MockPrivateObjectClass.InnerClass.class);
        Whitebox.setInternalState(objectClass, "innerClass", mockInnerClass);

        int ret = objectClass.doPlus(2, 3);
        Assert.assertEquals(5, ret);
        Mockito.verify(mockInnerClass).plus(2, 3);
    }
}
4.4.2.2.第二种方式:MemberModifier.field
@RunWith(PowerMockRunner.class)
@PrepareForTest({MockPrivateObjectClass.class})
public class MockPrivateObjectClassTest {

    @Test
    public void doPlusMethod2Test() throws Exception {
        MockPrivateObjectClass objectClass = new MockPrivateObjectClass();
        MockPrivateObjectClass.InnerClass mockInnerClass = Mockito.spy(MockPrivateObjectClass.InnerClass.class);
        MemberModifier.field(MockPrivateObjectClass.class, "innerClass").set(objectClass, mockInnerClass);

        int ret = objectClass.doPlus(2, 3);
        Assert.assertEquals(5, ret);
        Mockito.verify(mockInnerClass).plus(2, 3);
    }
}

4.4.3.Verify对象私有方法

public class NormalClassA {
    public int publicAdd(int a, int b) {
        return privateAdd(a, b);
    }

    private int privateAdd(int a, int b) {
        return a + b;
    }
}

@RunWith(PowerMockRunner.class)
@PrepareForTest({NormalClassA.class})
public class NormalClassAPowerMockTest {
    @Test
    public void testPrivateMethod() throws Exception {
        NormalClassA classA = new NormalClassA();
        NormalClassA mockClassA = PowerMockito.spy(classA);

        mockClassA.publicAdd(2, 3);

        PowerMockito.verifyPrivate(mockClassA, times(1)).invoke("privateAdd", Mockito.anyInt(), Mockito.anyInt());
    }
}

4.4.4.Invoke对象私有方法

public class ObjectWhiteBoxClass {
    private final List<String> mObjectList = new ArrayList<>();

    private void privateObjectAdd(String str) {
        mObjectList.add(str);
    }

    private void privateObjectRemove(String str) {
        mObjectList.remove(str);
    }

    public List<String> getObjectList() {
        return mObjectList;
    }
}

@Test
public void testInvokeMethod() throws Exception {
    ObjectWhiteBoxClass objectWhiteBoxClass = new ObjectWhiteBoxClass();

    Whitebox.invokeMethod(objectWhiteBoxClass, "privateObjectAdd", "test1");
    Whitebox.invokeMethod(objectWhiteBoxClass, "privateObjectAdd", "test2");

    Assert.assertEquals(2, objectWhiteBoxClass.getObjectList().size());

    Whitebox.invokeMethod(objectWhiteBoxClass, "privateObjectRemove", "test1");

    Assert.assertEquals(1, objectWhiteBoxClass.getObjectList().size());
}

4.4.5.修改对象私有函数

public class MockPrivateObjectClass {
    public String stepName;

    public MockPrivateObjectClass() {

    }

    private void setStepName() {
        System.out.print("enter setStepName");
        stepName = "has set name";
    }

    public void testStepName() {
        setStepName();
        System.out.print(stepName);
    }
}

@Test
public void replacePrivateMethodTest() {
    MockPrivateObjectClass objectClass = new MockPrivateObjectClass();

    PowerMockito.replace(PowerMockito.method(MockPrivateObjectClass.class, "setStepName")).with(new InvocationHandler() {
        @Override
        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
            Whitebox.setInternalState(o, "stepName", "modify step name");
            return null;
        }
    });

    objectClass.testStepName();
}

运行结果:

image

替换了setStepName方法的实现。

4.4.5.1.这在写单测时,特别有用,譬如,将异步执行的代码,替换为同步,可以方便测试。

4.4.6. 调用私有构造方法

public class PrivateConstructorClass {
    private final List<String> mObjectList;

    private PrivateConstructorClass() {
        mObjectList = new ArrayList<>();
    }

    public void add(String str) {
        mObjectList.add(str);
    }

    public List<String> getObjectList() {
        return mObjectList;
    }
}

@Test
public void testInvokePrivateConstructor() throws Exception{
    PrivateConstructorClass object = Whitebox.invokeConstructor(PrivateConstructorClass.class);
    object.add("test1");
    Assert.assertEquals(1, object.getObjectList().size());
}

// 调用了私有的构造函数 PrivateConstructorClass()
4.4.6.1.这个对单例等特别有用

4.4.7. mock对象的创建

public class NormalClassA {
    public String getClassBName() {
        NormalClassB b = new NormalClassB();
        return b.getName();
    }
}

public class NormalClassB {
    public String getName(){
        return "NormalClassB";
    }
}

public static class MockNormalClassB extends NormalClassB {
    @Override
    public String getName() {
        return "MockNormalClassB";
    }
}

@Test
public void testClassB() throws Exception {
    NormalClassA classA = new NormalClassA();
    String name = classA.getClassBName();
    System.out.println("before mock:" + name);
    Assert.assertEquals("NormalClassB", name);
    NormalClassB b = new MockNormalClassB();
    NormalClassB mockB = Mockito.spy(b);
    PowerMockito.whenNew(NormalClassB.class).withNoArguments().thenReturn(mockB);
    name = classA.getClassBName();
    System.out.println("after mock:" + name);
    Assert.assertEquals("MockNormalClassB", name);
    Mockito.verify(mockB).getName();
}

运行结果:

image

  1. new NormalClassB()动作是在函数内部,并没有赋给某个类成员,我们没法访问到NormalClassB对象,也就没法mock,这种情况特别适合用WhenNew方法。
  2. 注意:whenNew跟匿名类一起使用时,很可能会有问题,本文最后的总结中有介绍。

4.4.8.跳过某个方法

Mockito中介绍过,通过doReutrn、doNothing,不执行某个函数。

PowerMock中有另一种方式:

public class MockPrivateObjectClass {
    public String stepName;

    private void setStepName() {
        System.out.print("enter setStepName");
        stepName = "has set name";
    }

    public void testStepName() {
        setStepName();
        System.out.print(stepName);
    }
}

@Test
public void skipPrivateMethodTest() {
    MockPrivateObjectClass objectClass = new MockPrivateObjectClass();
    PowerMockito.suppress(PowerMockito.method(MockPrivateObjectClass.class, "setStepName"));
    objectClass.testStepName();
}

执行结果

image

enter setStepName log没有被打印,说明方法被跳过了。

4.4.9.打桩对象私有方法

public class H5JsApiScriptInterface {
    private boolean shouldTriggerForAccount(int account) {
        return !isAccountValid(account) || !isMainAvailable();
    }
}

// 注意:要增加 PrepareForTest
@PrepareForTest({LoginManager.class, H5JsApiScriptInterface.class})
public class H5JsApiScriptInterfaceTest extends MainTNUnitTest {
    // qq connect
    @Test
    public void testShowLoginWithType_1() throws Exception{
        PowerMockito.mockStatic(LoginManager.class);
        H5JsApiScriptInterface mockScriptInterface = PowerMockito.mock(H5JsApiScriptInterface.class);
        Mockito.doCallRealMethod().when(mockScriptInterface).showLoginWithType(Mockito.anyString(), Mockito.anyString());
        // 打桩对象私有方法,mock 对象私有方法
        // 注意:要增加 PrepareForTest
        PowerMockito.when(mockScriptInterface, PowerMockito.method(H5JsApiScriptInterface.class, "shouldTriggerForAccount", int.class)).withArguments(Mockito.anyInt()).thenReturn(true);
        mockScriptInterface.showLoginWithType("qqconnect", "callback");
        PowerMockito.verifyStatic(LoginManager.class, Mockito.times(1));
        LoginManager.triggerLogin(Mockito.eq(LOGIN_FROM_WEB_ONLY_QQCONNECT), Mockito.any(Subscriber.class));
    }
}

4.5. 针对静态类

4.5.1.访问静态私有成员

public class StaticWhiteBoxClass {
    private final static List<String> mStaticList = new ArrayList<>();

    public static void publicStaticAdd(String str) {
        mStaticList.add(str);
    }

    public static List<String> getStaticList() {
        return mStaticList;
    }
}

@Test
public void testGetInternalState() {
    StaticWhiteBoxClass.publicStaticAdd("test1str");
    StaticWhiteBoxClass.publicStaticAdd("test2str");
    StaticWhiteBoxClass.publicStaticAdd("test3str");

    List<String> list = Whitebox.getInternalState(StaticWhiteBoxClass.class, "mStaticList");

    Assert.assertEquals(3, list.size());
    Assert.assertEquals("test3str", list.get(list.size()-1));
}

4.5.2.修改静态私有成员

public class StaticWhiteBoxClass {
    private final static List<String> mStaticList = new ArrayList<>();

    public static void publicStaticAdd(String str) {
        mStaticList.add(str);
    }

    public static List<String> getStaticList() {
        return mStaticList;
    }
}

@Test
public void testSetInternalState() {
    List<String> list = new ArrayList<>();
    list.add("outer1");
    list.add("outer2");
    list.add("outer3");

    Whitebox.setInternalState(StaticWhiteBoxClass.class, "mStaticList", list);

    List<String> objList = StaticWhiteBoxClass.getStaticList();

    Assert.assertEquals(3, objList.size());
    Assert.assertEquals("outer3", list.get(objList.size()-1));
}

4.5.3.调用静态私有方法

public class StaticWhiteBoxClass {
    private final static List<String> mStaticList = new ArrayList<>();

    private static void privateStaticAdd(String str) {
        mStaticList.add(str);
    }

    public static List<String> getStaticList() {
        return mStaticList;
    }
}

@Test
public void testInvokeMethod() throws Exception{
    Whitebox.invokeMethod(StaticWhiteBoxClass.class, "privateStaticAdd", "test1");
    Whitebox.invokeMethod(StaticWhiteBoxClass.class, "privateStaticAdd", "test2");

    Assert.assertEquals(2, StaticWhiteBoxClass.getStaticList().size());
}

4.5.4.替换静态函数

public class StaticWhiteBoxClass {
    public static void originalMethod(String name) {
        System.out.println("enter originalMethod name:" + name);
    }
}

@Test
public void replacePrivateMethodTest() {
    PowerMockito.replace(PowerMockito.method(StaticWhiteBoxClass.class, "originalMethod")).with(new InvocationHandler() {
        @Override
        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
            System.out.println("enter modify method param:" + objects[0]);
            return null;
        }
    });

    StaticWhiteBoxClass.originalMethod("test replace");
}

运行结果:

image

4.5.5.打桩静态Public方法

public class StaticClass {
    public static String ask(){
        return "I'm Static class";
    }
}

@Test
public void should_mock_static_method() {
    final String value = "10";
    mockStatic(StaticClass.class);
    when(StaticClass.ask()).thenReturn(value);
    Assert.assertNotEquals("I'm Static class", StaticClass.ask());
}
  1. 静态方法mock,一定要调用mockStatic(StaticClass.class);
  2. when(StaticClass.ask()).thenReturn(value);PowerMock的这个语句跟Mockito是很像的,基本上Mockito中的的那些打桩方法在PowerMock都有对应的。

4.5.6.打桩静态private方法

这种public或private的都可以用

public class H5JsApiScriptInterface {
    // 静态私有方法
    private static boolean isQQorWxAvailable() {
        return true;
    }
}

@PrepareForTest({LoginManager.class, H5JsApiScriptInterface.class})
public class H5JsApiScriptInterfaceTest extends MainTNUnitTest { // qq and wx
    @Test
    public void testShowLoginWithType_4() throws Exception{
        PowerMockito.mockStatic(LoginManager.class);
        PowerMockito.mockStatic(H5JsApiScriptInterface.class);
        H5JsApiScriptInterface mockScriptInterface = PowerMockito.mock(H5JsApiScriptInterface.class);
        Mockito.doCallRealMethod().when(mockScriptInterface).showLoginWithType(Mockito.anyString(), Mockito.anyString());
        // 打桩静态私有方法,mock静态私有方法,注意增加@PrepareForTest
        PowerMockito.doReturn(false).when(H5JsApiScriptInterface.class, "isQQorWxAvailable");
        mockScriptInterface.showLoginWithType("qqorweixin", "callback");
        PowerMockito.verifyStatic(LoginManager.class, Mockito.times(1));
        LoginManager.triggerLogin(Mockito.eq(LOGIN_FROM_WEB_BOTH_QQ_AND_WX), Mockito.any(Subscriber.class));
    }
}

4.5.7.thenCallRealMethod方法

public class StaticClass {
    public static int add(int a, int b) {
        return a + b;
    }
    public static int sum() {
        int ret = 0;
        for (int i = 0; i < 10; i++) {
            ret = add(i, i+1);
        }
        return ret;
    }
}

@Test
public void call_mock_sum_static_method() {
    mockStatic(StaticClass.class);
    // 如果不用 spy,就用 thenCallRealMethod 也可以
    when(StaticClass.sum()).thenCallRealMethod();
    when(StaticClass.add(Mockito.anyInt(),Mockito.anyInt())).thenCallRealMethod();
    int sum = StaticClass.sum();
    Assert.assertEquals(19, sum);
}

4.5.8.spy静态方法

public class StaticClass {
    public static int add(int a, int b) {
        return a + b;
    }
    public static int sum() {
        int ret = 0;
        for (int i = 0; i < 10; i++) {
            ret = add(i, i+1);
        }
        return ret;
    }
}

@Test
public void call_spy_sum_static_method() {
    mockStatic(StaticClass.class);
    // mock 的话,函数都是空的切记,如果需要用到函数逻辑,要用 spy
    spy(StaticClass.class);
    int sum = StaticClass.sum();
    Assert.assertEquals(19, sum);
}

4.5.9.Verify Public 静态方法

public class StaticClass {
    public static int add(int a, int b) {
        return a + b;
    }
    public static int sum() {
        int ret = 0;
        for (int i = 0; i < 10; i++) {
            ret = add(i, i+1);
        }
        return ret;
    }
}

@Test
public void call_spy_sum_static_method() {
    mockStatic(StaticClass.class);
    // mock 的话,函数都是空的切记,如果需要用到函数逻辑,要用 spy
    spy(StaticClass.class);
    int sum = StaticClass.sum();
    Assert.assertEquals(19, sum);
    verifyStatic(StaticClass.class, Mockito.times(10)); // 验证调用了 10 次
    StaticClass.add(Mockito.anyInt(),Mockito.anyInt()); // 验证的是 add 方法
}

4.5.10.Verify Private 静态方法

public class StaticClass {
    public static int dec(int total) {
        int ret = total;
        for (int i = 0; i < 10; i++) {
            ret = sub(ret, i);
        }
        return ret;
    }
    private static int sub(int a, int b) {
        return a - b;
    }
}

@Test
public void call_private_dec_static_method() throws Exception {
    mockStatic(StaticClass.class);
    spy(StaticClass.class);
    int ret = StaticClass.dec(100);
    Assert.assertEquals(55, ret);
    verifyPrivate(StaticClass.class, Mockito.atLeastOnce()).invoke("sub",
            Mockito.anyInt(), Mockito.anyInt());
}

4.5.11.跳过静态方法

public class StaticClass {
    public static String mStepName;
    public static void testStepName() {
        setStepName();
    }
    private static void setStepName() {
        System.out.println("enter setStepName");
        mStepName = "setStepName";
    }
}

@Test
public void skipStaticMethodTest() {
    PowerMockito.suppress(PowerMockito.method(StaticClass.class, "setStepName"));
    StaticClass.testStepName();
    System.out.println("step name:" + StaticClass.mStepName);
}

运行结果:

image

enter setStepName log没有打印,说明已经跳过了。无论函数是否有返回值,都可以用这中方法跳过该函数。

4.6.6. Final类

public final class FinalClassA {
    public final String finalMethod() {
        return "finalMethod";
    }
}

@Test
public final void mockPowerFinalClassTest() {
    FinalClassA instance = new FinalClassA();
    FinalClassA mock = PowerMockito.mock(FinalClassA.class);
    Mockito.when(mock.finalMethod()).thenReturn("mock method");
    Assert.assertNotEquals(instance.finalMethod(), mock.finalMethod());
}

上里面例子演示了,通过PowerMock可以mock Final类。

4.7.总结

上面列出了PowerMoc差不多所有会使用到的场景下的使用范例,通过这些场景范例基本上可以解决我们单元测试中所有的需要mock的情况。

4.7.1.目前能想到的,PowerMock解决不了的是匿名类的场景:

public class NormalClassA {
    public void setAbsCallback() {
        AbsCallback callback = new AbsCallback() {
            @Override
            void call() {
                System.out.println("impl AbsCallback");
            }
        };
        callback.call();
    }
    public static class AbsCallback {
        public AbsCallback() {
        }
        void call(){
            System.out.println("impl default AbsCallback");
        };
    }
    public static class MockAbsCallback extends NormalClassA.AbsCallback {
        @Override
        void call() {
            System.out.println("mock impl AbsCallback");
        }
    }
}

如果想mock setAbsCallback函数中的new AbsCallback(),这种匿名类,目前是无法用WhenNew mock的。

还有在匿名类中new 对象A,用whenNew来mock对象A也是不行的。

见这里的例子,同时解释了具体原因 https://stackoverflow.com/questions/7523389/how-do-i-use-powermockito-to-mock-the-construction-of-new-objects-when-testing-a

5.JaCoCo

可以帮我们非常直观的看到单元测试的代码层面的覆盖情况。其实除了JaCoCo我们还能用idea自带的code coverage插件来查看单元测试的覆盖率。

引入maven插件

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.2</version>
</plugin>
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.2</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

maven命令

mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent package -Dmaven.test.failure.ignore=true org.jacoco:jacoco-maven-plugin:report surefire-report:report

文章信息

张雪随笔
master
Homepage
Issues
Download

This Software is under the terms of The Unlicense.