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"); } } }
|
使用上述测试方法有很多问题
- 一个类只会存在一个
main()
方法
- 无法将实际代码与测试代码分离
- 无法简单快捷的打印测试结果和期待结果,例如
expected: 3628800, but actual: 123456
- 很难编写一组通用的测试代码
因此我们需要一个通用的简单易用的框架来编写单元测试,这就是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
| <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
| <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
| List mockedList = mock(List.class);
when(mockedList.get(0)).thenReturn("first") .thenReturn("second");
when(mockedList.get(1)).thenThrow(new RuntimeException("mockedList.get(1)异常"));
doThrow(new RuntimeException()).when(mockedList).clear();
when(mockedList.get(anyInt())).thenReturn("element");
when(mockedList.get(intThat(argument -> argument > 10))).thenReturn("other");
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
| List mockedList = mock(List.class); when(mockedList.get(0)).thenReturn("first"); when(mockedList.get(anyInt())).thenReturn("element");
System.out.println(mockedList.get(0)); System.out.println(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");
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");
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);
when(spy.size()).thenReturn(100);
spy.add("one"); spy.add("two");
System.out.println(spy.get(0));
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("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("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();
verify(mock, timeout(100).times(2)).someMethod();
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对象有两种方式
- 在类上加入@AutoConfigureMockMvc注解,然后注入MockMvc对象
1 2 3 4 5 6 7
| @RunWith(SpringRunner.class) @SpringBootTest() @AutoConfigureMockMvc public class MockMvcControllerTest { @Autowired MockMvc mockMvc; }
|
- 手动创建一个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 { 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