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

一、消除误解

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

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

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

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

java 集成测试目录 java单元测试和集成测试_测试方法

单元测试的误解

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

First原则

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

什么时候用Mock

  • 被测试单元所依赖的第三方类运行结果不稳定(运行时间太长、网络异常)
  • 被测试单元所依赖的模块返回值比较难模拟,构造比较复杂
  • 被测试单元所依赖的模块返回结果不确定
  • 被测试单元所依赖的模块尚未开发完成

二、单元测试模板

准备执行,和校验

  • 准备数据-》Given
    这个部分创建我们将要测试方法的输入参数,或者Mock函数的返回值(mock的方法也会在这个部分中准备,因为mock属于测试执行的准备工作)。通常单元测试用例中,这个部分应该是最长,也是最复杂的。
  • 执行-》When
    这里一般只Call测试方法,这里标明了测试目的,因为这个部分的代码一般是最短的了。
  • 验证-》Then
    这个部分,执行环节的所有结果在这里得以声明。除此之外,也可以确认方法是否被执行。总之,主要的点都在这里进行Check。

三、单元测试命名

  • 类命名规则:测试类与被测试类的命名应保持一致,通常情况下,测试类的名称为:被测试类名称+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_correctly
guessHistory_should_record_every_guess_result
should_throw_OutOfRangeAnswerException_which_is_not_between_0_and_9
play_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 实例。

@Mock
private List<String> mockedList;
 
@Test
public void whenUseMockAnnotation_thenMockIsInjected() {
    Mockito.when(mockedList.size()).thenReturn(100);
    assertEquals(100, mockedList.size());
}

3.@InjectMocks 注解

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

@Mock
private Map<String, String> wordMap;
 
@InjectMocks
private MyDictionary dic = new MyDictionary();

//或者
//@InjectMocks
//private MyDictionary dic;
 
@Test
public 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
@Mock
LinkedList mockedList;

// 构造预期结果
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());

// 打印结果 "first"
System.out.println(mockedList.get(0));

// 会抛出 runtime exception
System.out.println(mockedList.get(1));

// 打印结果 "null",因为 get(999) 没有进行 Mock
System.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);
}

@Mock
FlowerService 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 有返回值方法
@Mock
private 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})

@Test
public 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{
	
	@InjectMocks
	private GameService gameService;
	
	@Test
	public 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;
    }
}

// ---
@Mock
private GameService gameService;

@Test
public 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());
}


@Captor
private ArgumentCaptor<Integer> LifeValueCaptor2;

@Test
public void demo6_mock_arg_captor_2() {
    doNothing().when(gameService).saveLifeValue(LifeValueCaptor2.capture());
    gameService.saveLifeValue(99);
    System.out.println(LifeValueCaptor2.getValue());
}

8. 参数值的修改

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

@Test
public 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:100
mock method is : killMonster
调用方法后:isDie:true, blood:0

9. spy

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

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})

@Test
public 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);
}


--
@Spy
private Calculator calculatorMock2 = new Calculator();    

@PrepareForTest({Calculator.class})

@Test
public 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%。

五、断言

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 assertions
assertThat(frodo.getName()).isEqualTo("Frodo");
assertThat(frodo).isNotEqualTo(sauron);

// chaining string specific assertions
assertThat(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 message
assertThat(frodo.getAge()).as("check %s's age", frodo.getName()).isEqualTo(33);

// exception assertion, standard style ...
assertThatThrownBy(() -> { throw new Exception("boom!"); }).hasMessage("boom!");
// ... or BDD style
Throwable 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。

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 参数捕获器进行配合。

@Test
public 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 mock
mockedList.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 default
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");

//exact number of invocations verification
verify(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 interacted
mockOne.add("one");

//ordinary verification
verify(mockOne).add("one");

//verify that method was never called on a mock
verify(mockOne, never()).add("two");

//verify that other mocks were not interacted
verifyZeroInteractions(mockTwo, mockThree);

7. 查找冗余调用

//using mocks
mockedList.add("one");
mockedList.add("two");

verify(mockedList).add("one");

//following verification will fail
verifyNoMoreInteractions(mockedList);

8. 调用顺序验证

// A. Single mock whose methods must be invoked in a particular order
List singleMock = mock(List.class);

//using a single mock
singleMock.add("was added first");
singleMock.add("was added second");

//create an inOrder verifier for a single mock
InOrder 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 order
List firstMock = mock(List.class);
List secondMock = mock(List.class);

//using mocks
firstMock.add("was called first");
secondMock.add("was called second");

//create inOrder object passing any mocks that need to be verified in order
InOrder inOrder = inOrder(firstMock, secondMock);

//following will make sure that firstMock was called before secondMock
inOrder.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 ms
verify(mock, timeout(100).times(2)).someMethod();

//equivalent: this also passes as soon as someMethod() has been called 2 times under 100 ms
verify(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

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

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

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

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

<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>
  1. 配置归档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>