匹配器:Java单元测试实践-理论->实践->Sonar集成

本文说明了单元测试与集成测试的区别,并从单元测试命名规范,编写(Given-When-Then),单元测试工具(mock工具及断言工具),以及maven插件实现单元测试和集成测试的分开执行方面进行了实践。

一、消除误解

单元测试:是指对软件中的最小可测试单元进行检查和验证。是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。针对Java程序而言,单元测试则是对某个类进行测试,主要以public方法作为入口,方法中调用了其他类的方法,则需要进行屏蔽(mock)。

单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。

单元测试与集成测试的区别

  • 测试对象不同。单元测试对象是实现了具体功能的程序单元;集成测试对象是概要设计规划中的模块及模块间的组合。
  • 测试方法不同。单元测试中的主要方法是基于代码的白盒测试;集成测试中主要使用基于功能的黑盒测试。
  • 测试时间不同。集成测试晚于单元测试。
  • 测试内容不同。单元测试主要是模块内程序的逻辑、功能、参数传递、变量引用、出错处理及需求和设计中具体要求方面的测试;集成测试主要验证各个接口、接口之间的数据传递关系,及模块组合后能否达到预期效果。

解放这头牛

单元测试的误解

  • 它浪费了太多的时间,逻辑太复杂写起来比较浪费时间。
  • 我是个很棒的程序员, 我是不是可以不进行单元测试?
  • 不管怎样,集成测试将会抓住所有的Bug。
  • 运行一次单元测试要等待很久,反馈太慢。
  • First原则

    • Fast : 测试要非常快,毫秒级,每秒能完成多个单元测试,这样开发人员可以对每一个小更改运行测试,而不用中断思绪去等待测试运行。
    • Isolated : 测试能够清楚的隔离一个失败,不同的测试用例之间是隔离的。一个测试不会依赖另一个测试。不同测试的故障是相互隔离的。
    • Repeatable : 测试应该可以重复运行,且每次都以同样的方式成功或失败。
    • Self-verifying : 测试要无歧义的表达成功或失败,自我验证而不是人工判断。
    • Timely : 测试是及时的,频繁、小规模的修改代码,及时的运行测试。

    什么时候用Mock

    • 被测试单元所依赖的第三方类运行结果不稳定(运行时间太长、网络异常)

    • 被测试单元所依赖的模块返回值比较难模拟,构造比较复杂

    • 被测试单元所依赖的模块返回结果不确定

    • 被测试单元所依赖的模块尚未开发完成

    二、单元测试模板

    准备,执行,和校验

    • 准备数据-》Given

      这个部分创建我们将要测试方法的输入参数,或者Mock函数的返回值(mock的方法也会在这个部分中准备,因为mock属于测试执行的准备工作)。通常单元测试用例中,这个部分应该是最长,也是最复杂的。

    • 执行-》When

      这里一般只Call测试方法,这里标明了测试目的,因为这个部分的代码一般是最短的了。

    • 验证-》Then

      这个部分,执行环节的所有结果在这里得以声明。除此之外,也可以确认方法是否被执行。总之,主要的点都在这里进行Check。

    参考:

    • https://blog.codecentric.de/en/2017/09/given-when-then-in-junit-tests/ (译文:https://1991421.cn/2019/04/21/71b0d191/)

    • https://www.jianshu.com/p/f40e20be624f

    三、单元测试命名

    • 类命名规则:测试类与被测试类的命名应保持一致,通常情况下,测试类的名称为:被测试类名称+Test后缀。
      如:GameService的测试类命名为:GameServiceTest

    • 包路径规则:package的路径主要与被测试类的路径保持一致,同时在合适的地方增加一个层级,用于区分“单元测试”、“集成测试”。

      如:有个被测试的类的全路径是:com.iccboy.project.scene.GameService,则测试的包路径是:com.iccboy.project.unit.scene.GameServiceTest

    • 方法命名规则:测试方法应表述业务含义,这样就能使得测试类可以成为文档。测试方法名可以足够长,以便于清晰的表述业务。为了更好地辨别方法名表述的含义,建议采用Ruby风格的命名方法,即下划线分隔方法的每个单词。建议测试方法名以should开头,此时,默认的主语为被测试类。为了更容易定位到被测试方法,命名也可以是 被测试方法名_should_xxx_when_xxx

    should_return_0A0B_when_no_number_guessed_correctlyguessHistory_should_record_every_guess_resultshould_throw_OutOfRangeAnswerException_which_is_not_between_0_and_9play_should_end_game_and_display_sucessful_message_when_number_is_correct_in_first_round

    四、Mock

    • Mockito:EasyMock之后流行的mock工具。相对EasyMock学习成本低,而且具有非常简洁的API,验证语法简洁,测试代码的可读性很高。StackOverflow 社区将 Mockito 评为 Java 的最佳模拟框架。
    • PowerMock: 这个工具是在EasyMock和Mockito上扩展出来的,目的是为了解决EasyMock和Mockito不能解决的问题,比如对static, final, private方法均不能mock。其实测试架构设计良好的代码,一般并不需要这些功能,但如果是在已有项目上增加单元测试,老代码有问题且不能改时,就不得不使用这些功能了。

    需要引入maven依赖

    <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>2.0.9</version> <scope>test</scope> <exclusions> <exclusion> <artifactId>objenesis</artifactId> <groupId>org.objenesis</groupId> </exclusion> </exclusions></dependency><dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4-rule</artifactId> <version>2.0.9</version> <scope>test</scope></dependency><dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>2.0.9</version> <scope>test</scope></dependency><!-- mockito-core对应的版本2.23.4--><dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.23.4</version> <scope>test</scope></dependency><dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <!-- use 2.9.1 for Java 7 projects --> <version>3.22.0</version> <scope>test</scope></dependency>
    1. 启用 Mockito 注解

    使用 MockitoJUnitRunner 注解 JUnit 测试,如以下示例所示:

    import org.mockito.junit.MockitoJUnitRunner;@RunWith(MockitoJUnitRunner.class)public class GameServiceTest { ...}

    如果需要 Mock 静态方法,需要使用 PowerMock 代替 Mockito。使用 @RunWith(PowerMockRunner.class)和 @PrepareForTest 注解。如以下示例所示:

    import org.powermock.modules.junit4.PowerMockRunner;@RunWith(PowerMockRunner.class)@PrepareForTest({RedissioUtil.class, GenerateUtil.class}) // RedissioUtil.class, GenerateUtil.class 包含 static 方法public class GameServiceTest { ...}

    开发中,直接使用 @RunWith(PowerMockRunner.class) 即可。

    2.@Mock 注解

    Mockito 中使用最广泛的注释是 @Mock。 可以使用 @Mock 来创建和注入 Mock 实例。

    @Mockprivate List<String> mockedList; @Testpublic void whenUseMockAnnotation_thenMockIsInjected() { Mockito.when(mockedList.size()).thenReturn(100); assertEquals(100, mockedList.size());}
    3.@InjectMocks 注解

    如何使用 @InjectMocks 注解,将 Mock 字段自动注入到测试对象中。在以下示例中,使用 @InjectMocks 将 mock 的 wordMap 注入到 MyDictionary dic:

    @Mockprivate Map<String, String> wordMap; @InjectMocksprivate MyDictionary dic = new MyDictionary();//或者//@InjectMocks//private MyDictionary dic; @Testpublic void whenUseInjectMocksAnnotation_thenCorrect() { Mockito.when(wordMap.get("aWord")).thenReturn("aMeaning"); assertEquals("aMeaning", dic.getMeaning("aWord"));}

    这是 MyDictionary 类:

    public class MyDictionary { Map<String, String> wordMap; public MyDictionary() { wordMap = new HashMap<String, String>(); } public void add(final String word, final String meaning) { wordMap.put(word, meaning); } public String getMeaning(final String word) { return wordMap.get(word); }}

    说明:

    @Mock: 创建一个Mock.

    @InjectMocks: 创建一个实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。

    注意:必须使用@RunWith(MockitoJUnitRunner.class) 或 Mockito.initMocks(this)进行mocks的初始化和注入。

    4.When/Then,Mock 预期结果
    // Mock mockedList@MockLinkedList mockedList;// 构造预期结果when(mockedList.get(0)).thenReturn("first");when(mockedList.get(1)).thenThrow(new RuntimeException());// 打印结果 "first"System.out.println(mockedList.get(0));// 会抛出 runtime exceptionSystem.out.println(mockedList.get(1));// 打印结果 "null",因为 get(999) 没有进行 MockSystem.out.println(mockedList.get(999)); // 静态引入Mockito相关的方法import static org.mockito.Mockito.*;
    5.ArgumentMatchers 参数匹配器
    5.1 内置参数匹配器

    Mockito 通过 equals() 方法,来对方法参数进行验证。

    when(flowerService.analyze("poppy")).thenReturn("Flower");

    在上面的示例中,仅当 flowerService 收到字符串“poppy”时才返回字符串“Flower”。

    但有时我们需要更加灵活的参数需求,更大范围的值或预先未知的值做出验证。比如,匹配任何的String类型的参数等等。参数匹配器就是一个能够满足这些需求的工具。

    Mockito框架中的Matchers类内建了很多参数匹配器。这些内建的参数匹配器如,anyInt()匹配任何int类型参数,anyString()匹配任何字符串,anySet()匹配任何Set, any(T t)匹配任何T类型的对象等。下面通过例子来说明如何使用内建的参数匹配器:

    when(flowerService.analyze(anyString())).thenReturn("Flower");

    现在,由于anyString参数匹配器,无论我们传递什么值,结果都将是相同的。

    注意:如果一个方法具有多个参数,则不可能仅对某些参数使用 ArgumentMatchers。 Mockito要求您通过匹配器或精确值提供所有参数。

    错误事例:

    abstract class FlowerService { public abstract boolean isABigFlower(String name, int petals);}@MockFlowerService mock; when(mock.isABigFlower("poppy", anyInt())).thenReturn(true);

    如果只输入字符串”poppy”是会报错的,必须使用 Matchers 类内建的 eq 匹配器:

    when(mock.isABigFlower(eq("poppy"), anyInt())).thenReturn(true);

    eq:等于给定值的参数。

    // 静态引入Mockito相关的方法import static org.mockito.ArgumentMatchers.*;
    5.2 自定义参数匹配器

    有时我们还是需要更灵活的匹配,所以需要自定义参数匹配器。

    自定义参数匹配器的时候需要继承 ArgumentMatcher 抽象类,并实现 matches 方法,在方法中定义规则即可。

    下面是自定义的参数匹配器是用于匹配分页参数的 PageableMatcher:

    public class PageableMatcher implements ArgumentMatcher<Pageable> { private Pageable pageable; public PageableMatcher(Pageable pageable) { this.pageable = pageable; } @Override public boolean matches(Pageable pageable) { return this.pageable.getPageNumber() == pageable.getPageNumber() && this.pageable.getPageSize() == pageable.getPageSize(); }}

    matches 的逻辑用于比较 pageNumber 与 pageSize 是否与构造函数中传入的 pageNumber 与 pageSize 相等。

    使用自定义参数匹配器 PageableMatcher 的例子如下:

    when(repository.find(argThat(new PageableMatcher(pageable)))) .thenReturn(list);

    argThat(Matcher<T> matcher)方法用来应用自定义的规则,可以传入任何实现 Matcher 接口的实现类。

    6.不同情况的Mock

    被mock的方法有以下情况

    6.1 有返回值方法
    @Mockprivate List<String> mockList;//int size();when(mockList.size()).thenReturn(9);// E get(int index);when(mockList.get(0)).thenReturn("A");when(mockList.get(1)).thenReturn("B");
    6.2 void方法
    //void add(int index, E element);doNothing().when(mockList).add(eq(2), anyString());
    6.3 抛异常
    when(mockList.get(3)).thenThrow(new IndexOutOfBoundsException());doThrow(new IndexOutOfBoundsException()).when(mockList).get(3);
    6.4 静态方法

    静态方法的 Mock 需使用 PowerMockito,Mockito(3.4版本之前) 不支持静态方法的 Mock。

    import org.powermock.api.mockito.PowerMockito;import org.powermock.core.classloader.annotations.PrepareForTest;import org.powermock.modules.junit4.PowerMockRunner;@RunWith(PowerMockRunner.class)@PrepareForTest({JSON.class})PowerMockito.mockStatic(JSON.class);PowerMockito.when(JSON.toJSONString(any())).thenReturn("{\"name\":\"iccboy\"}");
    6.5 静态void方法
    class XxxUtil { public static void clean(String id){ System.out.println("clean id is:" + id); }}// -----@RunWith(PowerMockRunner.class)@PrepareForTest({XxxUtil.class})@Testpublic void demo5_mock_void_static_method() throws Exception { PowerMockito.mockStatic(XxxUtil.class); PowerMockito.doNothing().when(XxxUtil.class, "clean", anyString());}
    6.6 私有方法
    public class Calculator { private int sumXX(int a, int b) {return a + b;} public int callSumXX(int a, int b){ return sumXX(a, b); }}@PrepareForTest({Calculator.class})PowerMockito.when(calculatorMock, "sumXX", 1, 2).thenReturn(2);
    6.7 @Value的mock

    通过Spring提供的反射工具类来实现

    public class GameService{@Value("${game.times}")private Integer times;}// ---------import org.springframework.test.util.ReflectionTestUtils;@RunWith(MockitoJUnitRunner.class)public class GameServiceTest{@InjectMocksprivate GameService gameService;@Testpublic void mock_atvalue() { ReflectionTestUtils.setField(messageBizServiceImpl, "times", 99);}}

    ReflectionTestUtils.setField() 的源码如下:

    /** * targetObject – 设置字段的目标对象 * name – 要设置的字段名称 * value – 要设置的值 */public static void setField(Object targetObject, String name, @Nullable Object value) { setField(targetObject, name, value, null);}
    7. 参数的捕获

    如果要获取被mock的方法在被调用时方法参数的值,可以通过ArgumentCaptor (或者:@Captor)进行参数的捕获。

    class GameService { public void saveLifeValue(Integer LifeValue){ System.out.println("保存生命值:" + LifeValue); } public void killMonster(Monster LifeValue){ LifeValue.setDie(true); }}class Monster { private Boolean isDie; public void setDie(Boolean isDie) { this.isDie = isDie; }}// ---@Mockprivate GameService gameService;@Testpublic void demo6_mock_arg_captor() { ArgumentCaptor<Integer> LifeValueCaptor = ArgumentCaptor.forClass(Integer.class); doNothing().when(gameService).saveLifeValue(LifeValueCaptor.capture()); gameService.saveLifeValue(99); System.out.println(LifeValueCaptor.getValue());}@Captorprivate ArgumentCaptor<Integer> LifeValueCaptor2;@Testpublic void demo6_mock_arg_captor_2() { doNothing().when(gameService).saveLifeValue(LifeValueCaptor2.capture()); gameService.saveLifeValue(99); System.out.println(LifeValueCaptor2.getValue());}
    8. 参数值的修改

    被mock的方法,如果方法逻辑中对参数(引用)进行了修改,此时可以通过Answer进行操作

    @Testpublic void demo7_mock_answer() { doAnswer(invocation -> { Method method = invocation.getMethod(); System.out.println("mock method is : " + method.getName()); Monster monster = invocation.getArgument(0); monster.setBlood(0); monster.setDie(true); return monster; }).when(gameService).killMonster(any()); Monster monster = new Monster(); monster.setBlood(100); monster.setDie(false); System.out.println("调用方法前:" + monster); Monster monster1 = gameService.killMonster(monster); System.out.println("调用方法后:" + monster1);}// 输出:调用方法前:isDie:false, blood:100mock method is : killMonster调用方法后:isDie:true, blood:0
    9. spy

    通过spy可以监视真实对象,同时可以进行mock

    https://blog.csdn.net/b1480521874/article/details/100972837

    class Calculator { private int sumXX(int a, int b) { return a + b; } public int callSumXX(int a, int b){ return sumXX(a, b); }}//---@PrepareForTest({Calculator.class})@Testpublic void demo8_mock_private_method() throws Exception { Calculator calculatorMock = PowerMockito.spy(new Calculator()); PowerMockito.when(calculatorMock, "sumXX", 1, 2).thenReturn(2); assertThat(calculatorMock.callSumXX(1, 2)).isEqualTo(2);}--@Spyprivate Calculator calculatorMock2 = new Calculator(); @PrepareForTest({Calculator.class})@Testpublic void demo8_mock_private_method_2() throws Exception { PowerMockito.when(calculatorMock2, "sumXX", 1, 2).thenReturn(2); assertThat(calculatorMock2.callSumXX(1, 2)).isEqualTo(2);}

    spy与mock的区别

    1.默认行为不同

    对于未指定mock的方法,spy默认会调用真实的方法,有返回值的返回真实的返回值,而mock默认不执行,有返回值的,默认返回null

    2.使用方式不同

    Spy中用when…thenReturn私有方法总是被执行,预期是私有方法不应该执行,因为很有可能私有方法就会依赖真实的环境。
    Spy中用doReturn…when才会不执行真实的方法。

    mock中用 when…thenReturn 私有方法不会执行。

    3.代码统计覆盖率不同
    @spy使用的真实的对象实例,调用的都是真实的方法,所以通过这种方式进行测试,在进行sonar覆盖率统计时统计出来是有覆盖率;
    @mock出来的对象可能已经发生了变化,调用的方法都不是真实的,在进行sonar覆盖率统计时统计出来的Calculator类覆盖率为0.00%。

    https://www.cnblogs.com/zendwang/p/mockito-mock-spy-usage.html

    五、断言

    1. 断言工具AssertJ

    任何单元测试方法都需要对测试结果进行验证,以决定测试结果是否通过。AssertJ提供了丰富流畅的验证API。以assertThat()方法开始,以isNotNull()、IsIn()、has()等方法连接起来,形成了一种类似自然语言的描述,提高了测试代码的可读性。http://www.javadoc.io/doc/org.assertj/assertj-core/是assertj core javadoc的最新版本,每个断言都有解释,大部分都附有代码示例。

    // entry point for all assertThat methods and utility methods (e.g. entry)import static org.assertj.core.api.Assertions.*;// basic assertionsassertThat(frodo.getName()).isEqualTo("Frodo");assertThat(frodo).isNotEqualTo(sauron);// chaining string specific assertionsassertThat(frodo.getName()).startsWith("Fro") .endsWith("do") .isEqualToIgnoringCase("frodo");// collection specific assertions (there are plenty more)// in the examples below fellowshipOfTheRing is a List<TolkienCharacter>assertThat(fellowshipOfTheRing).hasSize(9) .contains(frodo, sam) .doesNotContain(sauron);// as() is used to describe the test and will be shown before the error messageassertThat(frodo.getAge()).as("check %s's age", frodo.getName()).isEqualTo(33);// exception assertion, standard style ...assertThatThrownBy(() -> { throw new Exception("boom!"); }).hasMessage("boom!");// ... or BDD styleThrowable thrown = catchThrowable(() -> { throw new Exception("boom!"); });assertThat(thrown).hasMessageContaining("boom");// using the 'extracting' feature to check fellowshipOfTheRing character's names (抽取)assertThat(fellowshipOfTheRing).extracting(TolkienCharacter::getName) .doesNotContain("Sauron", "Elrond");// extracting multiple values at once grouped in tuples (元组)assertThat(fellowshipOfTheRing).extracting("name", "age", "race.name") .contains(tuple("Boromir", 37, "Man"), tuple("Sam", 38, "Hobbit"), tuple("Legolas", 1000, "Elf"));// filtering a collection before asserting(过滤)assertThat(fellowshipOfTheRing).filteredOn(character -> character.getName().contains("o")) .containsOnly(aragorn, frodo, legolas, boromir);// combining filtering and extraction (yes we can)assertThat(fellowshipOfTheRing).filteredOn(character -> character.getName().contains("o")) .containsOnly(aragorn, frodo, legolas, boromir) .extracting(character -> character.getRace().getName()) .contains("Hobbit", "Elf", "Man");// and many more assertions: iterable, stream, array, map, dates, path, file, numbers, predicate, optional ...
    2. 结果验证

    这个理解起来比较简单,就是根据不同的方法入参,可预期方法结果,然后对方法实际的结果进行验证。这里对结果的验证推荐使用上面讲的AssertJ。

    下面是mockito的断言能力。

    3. 行为验证
    //Let's import Mockito statically so that the code looks clearer import static org.mockito.Mockito.*; //mock creation List mockedList = mock(List.class); //using mock object mockedList.add("one"); mockedList.clear(); //verification verify(mockedList).add("one"); verify(mockedList).clear();

    一旦创建 mock 将会记得所有的交互。你可以选择验证你感兴趣的任何交互

    4. 参数验证

    参数验证表示的是:一般是对被mock的方法的入参进行验证,因为被mock的方法,真实逻辑已经被屏蔽掉了,那么要验证的话,只能验证入参值是否正确。参数的验证需要用到上面介绍的ArgumentCaptor 参数捕获器进行配合。

    @Testpublic void demo6_mock_arg_captor() { ArgumentCaptor<Integer> LifeValueCaptor = ArgumentCaptor.forClass(Integer.class); doNothing().when(gameService).saveLifeValue(LifeValueCaptor.capture()); gameService.saveLifeValue(99); assertThat(LifeValueCaptor.getValue()).isEqualTo(99);}
    5. 验证次数

    验证准确的调用次数/ 至少 x次/从不

    //using mockmockedList.add("once");mockedList.add("twice");mockedList.add("twice");mockedList.add("three times");mockedList.add("three times");mockedList.add("three times");//following two verifications work exactly the same - times(1) is used by defaultverify(mockedList).add("once");verify(mockedList, times(1)).add("once");//exact number of invocations verificationverify(mockedList, times(2)).add("twice");verify(mockedList, times(3)).add("three times");//verification using never(). never() is an alias to times(0)verify(mockedList, never()).add("never happened");//verification using atLeast()/atMost()verify(mockedList, atLeastOnce()).add("three times");verify(mockedList, atLeast(2)).add("three times");verify(mockedList, atMost(5)).add("three times");
    6. 确保交互从未在模拟中发生
    //using mocks - only mockOne is interactedmockOne.add("one");//ordinary verificationverify(mockOne).add("one");//verify that method was never called on a mockverify(mockOne, never()).add("two");//verify that other mocks were not interactedverifyZeroInteractions(mockTwo, mockThree);
    7. 查找冗余调用
    //using mocksmockedList.add("one");mockedList.add("two");verify(mockedList).add("one");//following verification will failverifyNoMoreInteractions(mockedList);
    8. 调用顺序验证
    // A. Single mock whose methods must be invoked in a particular orderList singleMock = mock(List.class);//using a single mocksingleMock.add("was added first");singleMock.add("was added second");//create an inOrder verifier for a single mockInOrder inOrder = inOrder(singleMock);//following will make sure that add is first called with "was added first", then with "was added second"inOrder.verify(singleMock).add("was added first");inOrder.verify(singleMock).add("was added second");// B. Multiple mocks that must be used in a particular orderList firstMock = mock(List.class);List secondMock = mock(List.class);//using mocksfirstMock.add("was called first");secondMock.add("was called second");//create inOrder object passing any mocks that need to be verified in orderInOrder inOrder = inOrder(firstMock, secondMock);//following will make sure that firstMock was called before secondMockinOrder.verify(firstMock).add("was called first");inOrder.verify(secondMock).add("was called second");// Oh, and A + B can be mixed together at will
    9. 超时验证
    //passes when someMethod() is called no later than within 100 ms//exits immediately when verification is satisfied (e.g. may not wait full 100 ms)verify(mock, timeout(100)).someMethod();//above is an alias to:verify(mock, timeout(100).times(1)).someMethod();//passes as soon as someMethod() has been called 2 times under 100 msverify(mock, timeout(100).times(2)).someMethod();//equivalent: this also passes as soon as someMethod() has been called 2 times under 100 msverify(mock, timeout(100).atLeast(2)).someMethod();

    六、Maven执行单元测试及集成测试

    1. 约定

    约定根据不同的包名或者类名来区分不同的测试类型。

    单元测试

    • 在unit包及子包中的测试类

    • 以UnitTest结尾的测试类

    集成测试

    • 在integration包及子包中的测试类
    • 以IT结尾的测试类
    • 以Test结尾(但不包含UnitTest)的测试类(主要是为了兼容之前的单元测试)
    2. maven plugin配置

    结合maven的maven-surefire-plugin插件,通过命令控制分类执行。

    在maven项目中的父pom.xml中配置 plugin,如下:

    <project> ... <build> ... <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0-M5</version> <configuration> <!--执行单元测试失败后继续--> <testFailureIgnore>true</testFailureIgnore> </configuration> <executions> <execution> <id>default-test</id> <goals> <goal>test</goal> </goals> <configuration> <excludes> <exclude>**/*.java</exclude> </excludes> </configuration> </execution> <execution> <id>unit</id> <goals> <goal>test</goal> </goals> <configuration> <includes> <include>**/unit/**/*.java</include> <include>**/*UnitTest.java</include> </includes> <excludes> <exclude>**/*IT.java</exclude> </excludes> </configuration> </execution> <execution> <id>it</id> <goals> <goal>test</goal> </goals> <configuration> <includes> <include>**/integration/**/*.java</include> <include>**/*IT.java</include> <include>**/*Test.java</include> </includes> <excludes> <exclude>**/*UnitTest.java</exclude> <exclude>**/unit/**/*.java</exclude> </excludes> </configuration> </execution> </executions> </plugin> </plugins> ... </build> </project>
    3. 命令执行

    根据上面的配置,下面的命令可控制执行不同的测试类别。

    • mvn test 同时执行单元测试和集成测试
    • mvn test-compile surefire:test@unit 只执行单元测试
    • mvn test-compile surefire:test@it 只执行集成测试

    如果要跳过测试,mvn clean package -Dmaven.test.skip=true 或者mvn clean package -DskipTests

    参考:

    • Maven命令分开执行单元测试和集成测试

    • Maven 如何配置Integration Test

    • maven-failsafe-plugin

    • maven-surefire-plugin

    • 学习Maven之Maven Surefire Plugin(JUnit篇)

    七、Sonar统计集成测试覆盖率

    如果需要支持maven多模块的集成测试,则参考下面的配置。

    多模块的集成测试,表现举例:

    说明:A模块依赖了 B 和 C模块,那我们在写集成测试时,测试代码应该是写在A模块的,然后执行时测试时,代码会从 A模块然后调用到B、C模块,如果不做下面配置,sonar只会统计到A模块代码的覆盖率,进行下面的配置后,就会把 B、C模块代码的分支覆盖率也会一并统计到。

    参考官方demo: 点击查看

  • 首先在项目父pom.xml中增加如下配置:

    <build> <plugins> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>${jacoco.version}</version> <executions> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>verify</phase> <goals> <goal>report-aggregate</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
  • 配置归档sonar扫描报告地址(告知各个模块的sonar报告结果 都归档到写有集成测试的模块)

    主要通过sonar.coverage.jacoco.xmlReportPaths属性进行配置。

    下面的配置可以放到父pom.xml中,也可以放到每个需要执行单元测试的模块中

    <properties> <aggregate.report.dir>tests/target/site/jacoco-aggregate/jacoco.xml</aggregate.report.dir> <sonar.coverage.jacoco.xmlReportPaths>${basedir}/../${aggregate.report.dir}</sonar.coverage.jacoco.xmlReportPaths> </properties>

    如果是单元测试,则改成下面的配置:

    <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>${jacoco.version}</version> <executions> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>prepare-package</phase> <goals> <goal>report</goal> </goals> </execution> </executions></plugin>
  • 相关推荐

    相关文章