本文说明了单元测试与集成测试的区别,并从单元测试命名规范,编写(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。
三、单元测试命名
- 类命名规则:测试类与被测试类的命名应保持一致,通常情况下,测试类的名称为:被测试类名称+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>
- 配置归档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>