一、背景
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。针对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依赖
```xml
<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>
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})
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();
@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
LinkedList mockedList;
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
System.out.println(mockedList.get(0));
System.out.println(mockedList.get(1));
System.out.println(mockedList.get(999));
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:等于给定值的参数。
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;
when(mockList.size()).thenReturn(9);
when(mockList.get(0)).thenReturn("A");
when(mockList.get(1)).thenReturn("B");
6.2 void方法
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() 的源码如下:
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
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})
@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%。
https://www.cnblogs.com/zendwang/p/mockito-mock-spy-usage.html
五、断言
断言工具介绍
验证行为、验证结果、验证参数、超时验证、调用顺序验证
import static org.assertj.core.api.Assertions.*;
https://www.cnblogs.com/bodhitree/p/9456515.html
六、Maven执行单元测试及集成测试
|