0%

基于JUnit和Mockito的单元测试

1. 测试驱动开发TDD

1.1. 什么是测试驱动开发

测试驱动开发(TDD),指先编写接口,紧接着编写测试。编写完测试后,我们才开始真正编写实现代码。在编写实现代码的过程中,一边写,一边测,什么时候测试全部通过了,那就表示编写的实现完成了:

1
2
3
4
5
6
7
graph TD
A[编写接口] --> B(编写测试)
B --> C(编写实现)
C --> D(运行测试)
D --> E{是否通过测试}
E -->|Y| F(任务完成)
E -->|N| C

1.2. 如何编写一个简单测试

实际代码

1
2
3
4
5
6
7
8
9
public class Factorial {
public static long fact(long n) {
long r = 1;
for (long i = 1; i <= n; i++) {
r = r * i;
}
return r;
}
}

测试代码

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
if (fact(10) == 3628800) {
System.out.println("pass");
} else {
System.out.println("fail");
}
}
}

使用上述测试方法有很多问题

  1. 一个类只会存在一个main()方法
  2. 无法将实际代码与测试代码分离
  3. 无法简单快捷的打印测试结果和期待结果,例如expected: 3628800, but actual: 123456
  4. 很难编写一组通用的测试代码

    因此我们需要一个通用的简单易用的框架来编写单元测试,这就是JUnit

2. JUnit介绍及使用

本章内容基于JUnit4

2.1. 一个简单的例子

1
2
3
4
5
6
7
8
@RunWith(JUnit4.class)
public class MathFunctionTest {
@Test
public void testAdd() {
int a = 1 + 2;
Assert.assertEquals(a, 3);
}
}

@RunWith就是一个运行器
@RunWith(JUnit4.class)就是指用JUnit4来运行
@RunWith(SpringRunner.class),让测试运行于Spring测试环境,以便在测试开始的时候自动创建Spring的应用上下文

2.2. JUnit4常见注解

注解 作用
@BeforeClass 所注解的方法是JUnit测试时首先被运行的方法且只能运行一次,通常用来进行预处理等操作。
@Before 所注解的方法在每个Test测试用例运行前运行,常用来进行初始化测试用例所需的资源。
@Test 所注解方法的代码为测试用例,包含对源程序的测试代码。包括expected和timeout两个可选参数。其中:expected表示测试用例运行后应该抛出的异常;timeout表示测试方法的运行时间,以避免程序测试时死循环或测试时间过长。
@After 所注解的方法是JUnit测试时最后一个被运行的方法且只能运行一次,通常用来释放相关使用资源。
@AfterClss 所注解的方法是JUnit测试时最后一个被运行的方法且只能运行一次,通常用来释放相关使用资源。
@Ignore 所注解的方法在测试过程中不会运行。

2.3. 注解举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@RunWith(JUnit4.class)
public class LearnTest {

@Before
public void before() {
System.out.println("before");
}

@BeforeClass
public static void beforeClass() {
System.out.println("beforeClass");

}

@Test
public void test1() {
System.out.println("test1");
}

@Test
public void test2() {
System.out.println("test2");
}

@Ignore
@Test
public void ignoreTest() {
System.out.println("ignoreTest");
}

@After
public void after() {
System.out.println("after");
}

@AfterClass
public static void afterClass() {
System.out.println("afterClass");
}
}

输出结果:

1
2
3
4
5
6
7
beforeClass
before
test1
after
before
test2
after

3. Mockito介绍及使用

3.1. Mockito

mockito是一个mock框架,他使得用户能够使用简介的Api做测试。

3.2. 为什么需要Mock

测试驱动的开发(TDD)要求我们先写单元测试,再写实现代码。在写单元测试的过程中,我们往往会遇到要测试的类有很多依赖,这些依赖的类/对象/资源又有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。如下图所示:

1
2
3
4
5
graph TD
A(Class A) --> B(Class B)
A --> C(Class C)
B --> D(Class D)
B --> E(Class E)

我们会发现需要测试Class A牵扯到多个类的依赖,这是我们就可以通过Mock B类和C类如下图所示

1
2
3
graph TD
A(Class A) --> B(Class B Mock)
A --> C(Class C Mock)

3.4. 项目集成Mockito

SpringBoot项目

spring-boot-starter-test中默认集成了Mockito,所以只需要引入spring-boot-starter-test依赖即可

1
2
3
4
5
6
7
8
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<!-- 具体依赖版本请根据实际情况进行选择 -->
<version>2.1.6.RELEASE</version>
<scope>test</scope>
</dependency>

非SpringBoot

1
2
3
4
5
6
7
8
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<!-- 具体依赖版本请根据实际情况进行选择 -->
<version>2.23.4</version>
<scope>test</scope>
</dependency>

3.5. Mockito常用功能实战

Mockito中文文档

3.5.1. 创建一个简单mock对象以及测试桩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 创建 mock 对象
List mockedList = mock(List.class);

// 创建测试桩
// 下面这行等价于 when(mockedList.get(0)).thenReturn("first", "second");
when(mockedList.get(0)).thenReturn("first")
.thenReturn("second");

when(mockedList.get(1)).thenThrow(new RuntimeException("mockedList.get(1)异常"));

// 为返回值为void的函数通过Stub抛出异常
doThrow(new RuntimeException()).when(mockedList).clear();

// 使用内置的 anyInt() 参数匹配器
when(mockedList.get(anyInt())).thenReturn("element");

// 此处要注意,当使用基本类型时,应该使用对应的 xxxThat,不要使用argThat,否则会由于自动拆箱导致空指针
when(mockedList.get(intThat(argument -> argument > 10))).thenReturn("other");

// 使用 mock 对象
System.out.println(mockedList.get(0));
System.out.println(mockedList.get(0));
System.out.println(mockedList.get(1));
System.out.println(mockedList.get(9));
System.out.println(mockedList.get(11));
3.5.2. 验证调用次数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建 mock 对象
List mockedList = mock(List.class);
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(anyInt())).thenReturn("element");

// 使用 mock 对象
System.out.println(mockedList.get(0));
System.out.println(mockedList.get(0));

// 验证
// verify(mockedList, times(1)).add("once"); 等价于 verify(mockedList).get(0);
verify(mockedList).get(0);
// 验证从未被调用
verify(mockedList, never()).add("once");
// 验证至少调用一次
verify(mockedList, atLeastOnce()).get(0);
// 验证至少调用三次
verify(mockedList, atLeast(3)).get(0);
// 验证至多调用三次
verify(mockedList, atMost(3)).get(0);
3.5.3. 验证调用顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
List mockedList1 = mock(List.class);
mockedList1.add("first1");
mockedList1.add("second1");

// 为该 mock 对象创建一个 inOrder 对象
InOrder inOrderOne = inOrder(mockedList1);

// 验证执行顺序
inOrderOne.verify(mockedList1).add("first1");
inOrderOne.verify(mockedList1).add("second1");

List mockedList2 = mock(List.class);
mockedList2.add("first2");
mockedList2.add("second2");

// 为这两个 mock 对象创建 inOrder 对象
InOrder inOrderTwo = inOrder(mockedList1, mockedList2);
// 验证它们的执行顺序
inOrderTwo.verify(mockedList1).add("first1");
inOrderTwo.verify(mockedList2).add("second1");
3.5.4. 监控真实对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List<String> list = new ArrayList<>();
List<String> spy = spy(list);

//optionally, you can stub out some methods:
// 你可以为某些函数打桩
when(spy.size()).thenReturn(100);

//using the spy calls *real* methods
// 通过spy对象调用真实对象的函数
spy.add("one");
spy.add("two");

//prints "one" - the first element of a list
// 输出第一个元素
System.out.println(spy.get(0));

//size() method was stubbed - 100 is printed
// 因为size()函数被打桩了,因此这里返回的是100
System.out.println(spy.size());

对于真实对象进行打桩的时候要注意when方法的使用,例如像下方使用会导致报错

1
2
3
4
5
List<String> list = new ArrayList<>();
List<String> spy = spy(list);

when(spy.get(0)).thenReturn("mock str");
System.out.println(spy.get(0));

此时会抛出错误

1
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0

因为Mockito并不会为真实对象代理函数调用,而是真实调用get(0)方法,然后返回你模拟的返回。因此不要期待从监控对象得到正确的结果。那我们应该如果解决这种问题呢?

1
2
3
4
5
6
List<String> list = new ArrayList<>();
List<String> spy = spy(list);

// 你需要使用doReturn()来打桩
doReturn("foo").when(spy).get(0);
System.out.println(spy.get(0));

因此我们可以知道,在对于真是对象的监控对象时when(someMethod.method(xxx)).thenReturn(xxx)会实际调用该对象的真实方法而doReturn(xxx).when(someMethod).method(xxx)不需要调用真实方法。

3.5.5. 捕获参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class);
List list = mock(ArrayList.class);

// 你需要使用doReturn()来打桩
doReturn("foo1").when(list).get(0);
doReturn("foo2").when(list).get(1);
System.out.println(list.get(0));
System.out.println(list.get(0));
System.out.println(list.get(1));
verify(list, atLeastOnce()).get(argument.capture());
// 获取最后一个捕获的参数
System.out.println(argument.getValue());
// 获取所有捕获的参数
System.out.println(argument.getAllValues());

上面方法的输出是

1
2
3
4
5
foo1
foo1
foo2
1
[0, 0, 1]

3.5.6. 重置mock对象
1
2
3
4
List mock = mock(List.class);
when(mock.size()).thenReturn(10);
mock.add(1);
reset(mock);
3.5.7. 验证超时
1
2
3
4
5
6
7
8
9
verify(mock, timeout(100)).someMethod();
// 等价于
verify(mock, timeout(100).times(1)).someMethod();

// 100毫秒超时并且次数两次
verify(mock, timeout(100).times(2)).someMethod();

// 100毫秒超时并且最少调用两次
verify(mock, timeout(100).atLeast(2)).someMethod();
3.5.8. Mockito常用注解
注解名 描述
@Mock 简化mock(Object)对象的创建
@Captor 简化 ArgumentCaptor 的创建
@Spy 代替 spy(Object)方法
@InjectMocks Mockito 会注入模拟对象到添加到当前注解的对象

4. MockMvc简介以及与Mockito的集成

MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。简而言之就是可以直接在测试方法中模拟发送一个请求,而不是使用类似于postman等工具进行测试。

4.1. 集成MockMvc

引入的spring-boot-starter-test间接引入了spring-test

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

4.2. MockMvc对象引入

SpringBoot项目中引入MockMvc对象有两种方式

  1. 在类上加入@AutoConfigureMockMvc注解,然后注入MockMvc对象
1
2
3
4
5
6
7
@RunWith(SpringRunner.class)
@SpringBootTest()
@AutoConfigureMockMvc
public class MockMvcControllerTest {
@Autowired
MockMvc mockMvc;
}
  1. 手动创建一个MockMvc实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RunWith(SpringRunner.class)
@SpringBootTest()
public class MockMvcControllerTest {

@Autowired
MockMvcController mockMvcController;

MockMvc mockMvc;

@Before
public void before() throws Exception {
// 此处并不一样要使用注入的bean对象也可以是手动创建的对象
mockMvc = MockMvcBuilders.standaloneSetup(mockMvcController).build();
}
}

那上面两种有什么区别呢?

  • 对于集成Mockito来说推荐使用第二种方式,因为第一种方式表示 MockMvc由spring容器构建,无法灵活的将生成的mock对象放入需要测试的对象中。
  • 第二种方式创建的时候我们可以将MockMvcBuilders.standaloneSetup(xxx).build()中的类选择为mock产生的对象就能使得测试更加灵活。

4.3. MockMvc与Mockito集成

4.3.1. MockMvc测试纯Mock对象

代码样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@RunWith(SpringRunner.class)
@SpringBootTest()
public class AiControllerTest {
@Mock
MockMvcService mockMvcService;
@InjectMocks
MockMvcController mockMvcController;

MockMvc mockMvc;

@Before
public void before() throws Exception {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(mockMvcController).build();
}

@After
public void after() throws Exception {
}

@Test
public void testGetRankConfig() throws Exception {
when(mockMvcService.get(anyString())).thenReturn("result");
final String result = mockMvc.perform(MockMvcRequestBuilders
.get("/get")
.accept(MediaType.APPLICATION_JSON)
.param("name", "zhangsan")
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("nnn"))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
}
4.3.2. MockMvc测试真实对象

在测试真实对象,然后对象中部分接口需要mock时
代码样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@RunWith(SpringRunner.class)
@SpringBootTest()
public class AiControllerTest {
@Mock
MockMvcService mockMvcService;
@Autowired
MockMvcController mockMvcController;

MockMvc mockMvc;

@Before
public void before() throws Exception {
MockitoAnnotations.initMocks(this);
AiController aiController = new AiController();
ReflectionTestUtils.setField(mockMvcController, "mockMvcService", mockMvcService);
mockMvc = MockMvcBuilders.standaloneSetup(mockMvcController).build();
}

@After
public void after() throws Exception {
}

@Test
public void testGetRankConfig() throws Exception {
when(mockMvcService.get(anyString())).thenReturn("result");
final String result = mockMvc.perform(MockMvcRequestBuilders
.get("/get")
.accept(MediaType.APPLICATION_JSON)
.param("name", "zhangsan"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("nnn"))
.andReturn().getResponse().getContentAsString();
System.out.println(result);

HashMap<String, Object> map = new HashMap<String, Object>() {{
put("aaa", "1");
}};

final String result = mockMvc.perform(MockMvcRequestBuilders
.post("/xxx")
.content(JSON.toJSONString(map))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn().getResponse().getContentAsString();
}
}

4. Idea单元测试工具使用

参考文章:
https://www.cnblogs.com/Ming8006/p/6297333.html
https://www.liaoxuefeng.com/wiki/1252599548343744/1304048154181666
https://www.w3cschool.cn/junit/ari41hv9.html
https://changingfond.github.io/mockito-zh-doc.html
https://blog.csdn.net/u010675669/article/details/86574956