抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

SpringBoot单元测试整理

Spring boot 作为当前最流行的java后端开发框架,集成了很多非常有用的东西,今天探索:Springboot test
引入依赖:

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

一、测试方式的整理

1.1、单元测试

在维基百科中的解释:

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

例如在开发过程中,我们一般都是这么做单元测试的:

1
2
3
4
5
6
7
8
public class UtilsTest {
@Test
public void testStringBuilder(){
String result = new StringBuilder().append(1).append(1).toString();
assert "11".equals(result);
}
}

通常来说,开发每当修改一次方法,都会重新进行单元测试,以保证程序的方法是正确的。虽然单元测试在开发过程中不是必须的,但是对于开发来说是非常有必要的,属于开发自测的一种非常实用的手段,减少代码出现bug的机率。

1.2、集成测试

集成测试(有时称为集成和测试(Integration Testing),缩写为I&T)是软件测试的阶段,在该阶段中,将各个软件模块组合在一起并作为一个整体进行测试。集成测试中进行评估依从性的系统或部件的与指定的功能要求。[1]它发生在单元测试之后和验证测试之前。集成测试以经过单元测试的输入模块为准,将它们按较大的聚合分组,应用集成测试计划中定义的测试这些聚合,并提供集成系统作为系统测试准备就绪的输出。

集成测试和单元测试的区别在于,单元测试仅仅测试的一般是一个方法(单元),不涉及到方法组合测试,因此集成测试将完成多个单元的组合测试,以测试在具有业务场景的逻辑下进行的测试集成,所以一般单元测试在集成测试之前。

1.3、Mock测试

Mock测试属于一种特殊的测试场景,在开发的过程中,可能由于协同开发的原因,自己需要测试自己开发的业务,但是业务中又依赖了同事开发的一些功能。按照正常的测试逻辑,可能会出现无法正常的完成测试需求,甚至代码都无法执行,此时就可以使用Mock测试的方法方法。
Mock测试的原理大致为:如果测试的方法依赖未知的对象时,可以通过Mock去抽象这个未知的对象,并对未知的对象的方法进行抽象返回我们想要的数据,以供我们的测试使用。以此完成我们的方法测试需求。
Spring Boot的测试模块支持Mock测试和junit测试。因此我们在SpringBoot开发时,可以在引入了Springboot测试模块后直接享受Mock测试和Junit单元测试带来的便利。

二、Springboot常用的测试方法

2.1、测试普通工具方法

所谓普通的方法,类似于工具类中的工具方法,不依赖于任何外部环境,例如Spring上下文信息,redis template,Mysql jdbc等。那么我们可以直接使用Junit对其进行测试。

1
2
3
4
5
6
7
public class UtilsTest {
@Test
public void testStringBuilder(){
String result = new StringBuilder().append(1).append(1).toString();
assert "11".equals(result);
}
}

2.3、测试业务方法

在Spring中的业务方法,都是通过IOC直接注入到Spring 容器中的,简单的使用Junit是无法直接执行的,必须指定Spring的上下文信息,保证其运行时能够从Spring容器中获取到对应的依赖Bean。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
@RunWith(SpringRunner.class)
public class CarServiceTest {
@Autowired
public CarService carService;

@Test
@Rollback
@Transactional
public void testAdd(){
Car car = new Car();
car.setType("SUV");
car.setName("BMW");
car = carService.add(car);
System.out.println(car);
}
}

测试CarService中的一个add方法,并且注入了CarService的Bean,@Transactional 开启了该方法的事务,@Rollback的意思是,执行玩测试方法,事务回滚,即不会修改数据库的数据。@Rollback必须配合@Transactional ,也就是只有事务开启了,才会生效。

2.3、测试Restful接口

在开发过程中,需要测试提供给前端的接口,一般有两种方式:

  • postman:好处能够和对应的google插件一起配合使用,共享google浏览器的cookie信息
  • MockMvc的方式:好处就是纯代码测试,可以控制数据入库,也能得到更详细请求数据。
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@SpringBootTest
@RunWith(SpringRunner.class)
public class CarControllerTest {
private MockMvc mockMvc;

@Autowired
private WebApplicationContext wac;

@Before
public void setupMockMvc(){
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}

@SneakyThrows
@Test
public void testListCar(){
mockMvc.perform(get("/car/list/")).andDo(MockMvcResultHandlers.print());
}

@SneakyThrows
@Test
@Rollback
@Transactional
public void testAdd(){
String json = "{\n" +
" \"name\": \"BMW\",\n" +
" \"type\": \"SUV\"\n" +
"}";

mockMvc.perform(post("/car/").accept(MediaType.APPLICATION_JSON_VALUE).content(json).contentType(MediaType.APPLICATION_JSON_VALUE))
.andDo(MockMvcResultHandlers.print());
}
}

//测试的结果:
/**
*
* MockHttpServletRequest:
* HTTP Method = POST
* Request URI = /car/
* Parameters = {}
* Headers = [Content-Type:"application/json", Accept:"application/json", Content-Length:"40"]
* Body = <no character encoding set>
* Session Attrs = {}
*
* Handler:
* Type = com.yiyi.controller.CarController
* Method = com.yiyi.controller.CarController#add(Car)
*
* Async:
* Async started = false
* Async result = null
*
* Resolved Exception:
* Type = null
*
* ModelAndView:
* View name = null
* View = null
* Model = null
*
* FlashMap:
* Attributes = null
*
* MockHttpServletResponse:
* Status = 200
* Error message = null
* Headers = [Content-Type:"application/json"]
* Content type = application/json
* Body = {"id":27,"name":"BMW","type":"SUV"}
* Forwarded URL = null
* Redirected URL = null
* Cookies = []
*/


在面的测试例子中,做了两件事:
1、开启了Springboot的测试,初始化了Spring的上下文环境
2、通过web上下文,初始化了MockMvc的Bean,便于接口的请求

2.4、特殊场景下,自定义配置的测试

2.4.1、指定profile的方式

新增配置文件application-test.xml
将@ActiveProfiles设置为test,在运行测试时,将会按照上面的配置文件进行初始化Spring bean

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
@SpringBootTest
@RunWith(SpringRunner.class)
@ActiveProfiles("test")
public class CarServiceTest {
@Autowired
public CarService carService;

@Value("${test.value}")
private int value;

@Test
public void testAdd(){
Car car = new Car();
car.setType("SUV");
car.setName("BMW");
car = carService.add(car);
System.out.println(car);
}

@Test
public void testGetValue(){
assert value == 2;
}

}

2.5、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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@SpringBootTest
@RunWith(SpringRunner.class)
public class MockTest {
@MockBean
private CarService carService;

public MockMvc mockMvc;

@Autowired
public WebApplicationContext context;

@Before
public void setUp(){
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}

@SneakyThrows
@Test
public void testAddCar(){
//测试添加CarController 的addCar接口,使用MockBean模拟的CarService的Bean
Car car = new Car();
car.setId(111L);
car.setName("BYD");
car.setType("SUV");
Car car1 = new Car();
//期望调用addCar方法传入一个car1返回一个car的对象,对象的数据为 id=111L,name=BYD, type=SUV
Mockito.when(carService.add(car1)).thenReturn(car);

String json = "{}";

mockMvc.perform(post("/car/").accept(MediaType.APPLICATION_JSON_VALUE).content(json).contentType(MediaType.APPLICATION_JSON_VALUE))
.andDo(MockMvcResultHandlers.print());
//执行结果:
/**
* MockHttpServletRequest:
* HTTP Method = POST
* Request URI = /car/
* Parameters = {}
* Headers = [Content-Type:"application/json", Accept:"application/json", Content-Length:"2"]
* Body = <no character encoding set>
* Session Attrs = {}
*
* Handler:
* Type = com.yiyi.controller.CarController
* Method = com.yiyi.controller.CarController#add(Car)
*
* Async:
* Async started = false
* Async result = null
*
* Resolved Exception:
* Type = null
*
* ModelAndView:
* View name = null
* View = null
* Model = null
*
* FlashMap:
* Attributes = null
*
* MockHttpServletResponse:
* Status = 200
* Error message = null
* Headers = [Content-Type:"application/json"]
* Content type = application/json
* Body = {"id":111,"name":"BYD","type":"SUV"}
* Forwarded URL = null
* Redirected URL = null
* Cookies = []
*/

}
}

在这个测试代码中,模拟了一个CarService的bean,并且模拟了CarService#add(car)的方法参数及返回的结果。所以Controller的接口调用依赖了这个模拟的方法,所以返回的结果和我们预期模拟的结果是一致的。
针对于上面的例子,如果在不mock的方法的前提下调用Mock出来的CarSerVice#list()方法,结果会是什么呢?回去执行原本的Spring的代理方法操作数据库还是报错呢?

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
@SpringBootTest
@RunWith(SpringRunner.class)
public class MockTest {
@MockBean
private CarService carService;

public MockMvc mockMvc;

@Autowired
public WebApplicationContext context;

@Before
public void setUp(){
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}

@Test
public void testList(){
Car car = new Car();
car.setName("BYD");
car.setType("SUV");
Car car1 = carService.add(car);
System.out.println("car:"+car1);
List<Car> list = carService.list();
System.out.println("carList:"+list);

//result:
/**
* car:null
* carList:[]
*/
}
}

结果表明:没有执行数据库的操作,而是mock返回的是方法的默认值。

评论