springboot单元测试
本文内容纲要:
-相关注解
-@SpringBootTest
-单元测试回滚
-断言
-基本的单元测试例子
-Controller测试
-Mock数据
SpringBoot测试支持由两个模块提供:
- spring-boot-test包含核心项目
- spring-boot-test-autoconfigure支持测试的自动配置
通常我们只要引入spring-boot-starter-test
依赖就行,它包含了一些常用的模块Junit、SpringTest、AssertJ、Hamcrest、Mockito等。
相关注解
SpringBoot使用了Junit4作为单元测试框架,所以注解与Junit4是一致的。
注解 | 作用 |
---|---|
@Test(excepted==xx.class,timeout=毫秒数) | 修饰一个方法为测试方法,excepted参数可以忽略某些异常类 |
@Before | 在每一个测试方法被运行前执行一次 |
@BeforeClass | 在所有测试方法执行前执行 |
@After | 在每一个测试方法运行后执行一次 |
@AfterClass | 在所有测试方法执行后执行 |
@Ignore | 修饰的类或方法会被测试运行器忽略 |
@RunWith | 更改测试运行器 |
@SpringBootTest
SpringBoot提供了一个@SpringBootTest注解用于测试SpringBoot应用,它可以用作标准spring-test@ContextConfiguration注释的替代方法,其原理是通过SpringApplication在测试中创建ApplicationContext。
1@RunWith(SpringRunner.class)
2@SpringBootTest
3publicclassApplicationTest{
4}
该注解提供了两个属性用于配置:
-
webEnvironment:指定Web应用环境,它可以是以下值
- MOCK:提供一个模拟的Servlet环境,内置的Servlet容器没有启动,配合可以与@AutoConfigureMockMvc结合使用,用于基于MockMvc的应用程序测试。
- RANDOM_PORT:加载一个EmbeddedWebApplicationContext并提供一个真正嵌入式的Servlet环境,随机端口。
- DEFINED_PORT:加载一个EmbeddedWebApplicationContext并提供一个真正嵌入式的Servlet环境,默认端口8080或由配置文件指定。
- NONE:使用SpringApplication加载ApplicationContext,但不提供任何servlet环境。
-
classes:指定应用启动类,通常情况下无需设置,因为SpringBoot会自动搜索,直到找到@SpringBootApplication或@SpringBootConfiguration注解。
单元测试回滚
如果你添加了@Transactional注解,它会在每个测试方法结束时会进行回滚操作。
但是如果使用RANDOM_PORT或DEFINED_PORT这种真正的Servlet环境,HTTP客户端和服务器将在不同的线程中运行,从而分离事务。在这种情况下,在服务器上启动的任何事务都不会回滚。
断言
JUnit4结合Hamcrest提供了一个全新的断言语法——assertThat
,结合Hamcrest提供的匹配符,就可以表达全部的测试思想。
//一般匹配符
ints=newC().add(1,1);
//allOf:所有条件必须都成立,测试才通过
assertThat(s,allOf(greaterThan(1),lessThan(3)));
//anyOf:只要有一个条件成立,测试就通过
assertThat(s,anyOf(greaterThan(1),lessThan(1)));
//anything:无论什么条件,测试都通过
assertThat(s,anything());
//is:变量的值等于指定值时,测试通过
assertThat(s,is(2));
//not:和is相反,变量的值不等于指定值时,测试通过
assertThat(s,not(1));
//数值匹配符
doubled=newC().div(10,3);
//closeTo:浮点型变量的值在3.0±0.5范围内,测试通过
assertThat(d,closeTo(3.0,0.5));
//greaterThan:变量的值大于指定值时,测试通过
assertThat(d,greaterThan(3.0));
//lessThan:变量的值小于指定值时,测试通过
assertThat(d,lessThan(3.5));
//greaterThanOrEuqalTo:变量的值大于等于指定值时,测试通过
assertThat(d,greaterThanOrEqualTo(3.3));
//lessThanOrEqualTo:变量的值小于等于指定值时,测试通过
assertThat(d,lessThanOrEqualTo(3.4));
//字符串匹配符
Stringn=newC().getName("Magci");
//containsString:字符串变量中包含指定字符串时,测试通过
assertThat(n,containsString("ci"));
//startsWith:字符串变量以指定字符串开头时,测试通过
assertThat(n,startsWith("Ma"));
//endsWith:字符串变量以指定字符串结尾时,测试通过
assertThat(n,endsWith("i"));
//euqalTo:字符串变量等于指定字符串时,测试通过
assertThat(n,equalTo("Magci"));
//equalToIgnoringCase:字符串变量在忽略大小写的情况下等于指定字符串时,测试通过
assertThat(n,equalToIgnoringCase("magci"));
//equalToIgnoringWhiteSpace:字符串变量在忽略头尾任意空格的情况下等于指定字符串时,测试通过
assertThat(n,equalToIgnoringWhiteSpace("Magci"));
//集合匹配符
List<String>l=newC().getList("Magci");
//hasItem:Iterable变量中含有指定元素时,测试通过
assertThat(l,hasItem("Magci"));
Map<String,String>m=newC().getMap("mgc","Magci");
//hasEntry:Map变量中含有指定键值对时,测试通过
assertThat(m,hasEntry("mgc","Magci"));
//hasKey:Map变量中含有指定键时,测试通过
assertThat(m,hasKey("mgc"));
//hasValue:Map变量中含有指定值时,测试通过
assertThat(m,hasValue("Magci"))
基本的单元测试例子
下面是一个基本的单元测试例子,对某个方法的返回结果进行断言:
1@Service
2publicclassUserService{
3
4publicStringgetName(){
5return"lyTongXue";
6}
7
8}
1@RunWith(SpringRunner.class)
2@SpringBootTest
3publicclassUserServiceTest{
4
5@Autowired
6privateUserServiceservice;
7
8@Test
9publicvoidgetName(){
10Stringname=service.getName();
11assertThat(name,is("lyTongXue"));
12}
13
14}
Controller测试
Spring提供了MockMVC用于支持RESTful风格的SpringMVC测试,使用MockMvcBuilder来构造MockMvc实例。MockMvc有两个实现:
-
StandaloneMockMvcBuilder:指定WebApplicationContext,它将会从该上下文获取相应的控制器并得到相应的MockMvc
1@RunWith(SpringRunner.class) 2@SpringBootTest 3publicclassUserControllerTest{ 4@Autowired 5privateWebApplicationContextwebApplicationContext; 6privateMockMvcmockMvc; 7@Before 8publicvoidsetUp()throwsException{ 9mockMvc=MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 10}
-
DefaultMockMvcBuilder:通过参数指定一组控制器,这样就不需要从上下文获取了
1@RunWith(SpringRunner.class) 2publicclassUserControllerTest{ 3privateMockMvcmockMvc; 4@Before 5publicvoidsetUp()throwsException{ 6mockMvc=MockMvcBuilders.standaloneSetup(newUserController()).build(); 7} 8}
下面是一个简单的用例,对UserController的/v1/users/{id}
接口进行测试。
1@RestController
2@RequestMapping("v1/users")
3publicclassUserController{
4
5@GetMapping("/{id}")
6publicUserget(@PathVariable("id")Stringid){
7returnnewUser(1,"lyTongXue");
8}
9
10@Data
11@AllArgsConstructor
12publicclassUser{
13privateIntegerid;
14privateStringname;
15}
16
17}
1//...
2importstaticorg.hamcrest.Matchers.containsString;
3importstaticorg.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
4importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
5importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
6
7@RunWith(SpringRunner.class)
8@SpringBootTest
9publicclassUserControllerTest{
10
11@Autowired
12privateWebApplicationContextwebApplicationContext;
13privateMockMvcmockMvc;
14
15@Before
16publicvoidsetUp(){
17mockMvc=MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
18}
19
20@Test
21publicvoidgetUser(){
22mockMvc.perform(get("/v1/users/1")
23.accept(MediaType.APPLICATION_JSON_UTF8))
24.andExpect(status().isOk())
25.andExpect(content().string(containsString("\"name\":\"lyTongXue\"")));
26}
27
28}
方法描述
- perform:执行一个RequestBuilder请求,返回一个ResultActions实例对象,可对请求结果进行期望与其它操作
- get:声明发送一个get请求的方法,更多的请求类型可查阅→MockMvcRequestBuilders文档
- andExpect:添加ResultMatcher验证规则,验证请求结果是否正确,验证规则可查阅→MockMvcResultMatchers文档
- andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台,更多处理器可查阅→MockMvcResultHandlers文档
- andReturn:返回执行请求的结果,该结果是一个恩MvcResult实例对象→MvcResult文档
Mock数据
在单元测试中,Service层的调用往往涉及到对数据库、中间件等外部依赖。而在单元测试AIR原则中,单元测试应该是可以重复执行的,不应受到外界环境的影响的。此时我们可以通过Mock一个实现来处理这种情况。
如果不需要对静态方法,私有方法等特殊进行验证测试,则仅仅使用Springboot自带的Mockito即可完成相关的测试数据Mock。若需要则可以使用PowerMock,简单实用,结合Spring可以使用注解注入。
@MockBean
SpringBoot在执行单元测试时,会将该注解的Bean替换掉IOC容器中原生Bean。
例如下面代码中,ProjectService中通过ProjectMapper的selectById方法进行数据库查询操作:
1@Service
2publicclassProjectService{
3
4@Autowired
5privateProjectMappermapper;
6
7publicProjectDOdetail(Stringid){
8returnmapper.selectById(id);
9}
10
11}
此时我们可以对Mock一个ProjectMapper对象替换掉IOC容器中原生的Bean,来模拟数据库查询操作,如:
1@RunWith(SpringRunner.class)
2@SpringBootTest
3publicclassProjectServiceTest{
4
5@MockBean
6privateProjectMappermapper;
7@Autowired
8privateProjectServiceservice;
9
10@Test
11publicvoiddetail(){
12ProjectDemoDOmodel=newProjectDemoDO();
13model.setId("1");
14model.setName("dubbo-demo");
15Mockito.when(mapper.selectById("1")).thenReturn(model);
16ProjectDemoDOentity=service.detail("1");
17assertThat(entity.getName(),containsString("dubbo-demo"));
18}
19
20}
Mockito常用方法
Mockito更多的使用可查看→官方文档
mock()对象
1Listlist=mock(List.class);
verify()验证互动行为
1@Test
2publicvoidmockTest(){
3Listlist=mock(List.class);
4list.add(1);
5//验证add(1)互动行为是否发生
6Mockito.verify(list).add(1);
7}
when()模拟期望结果
1@Test
2publicvoidmockTest(){
3Listlist=mock(List.class);
4when(mock.get(0)).thenReturn("hello");
5assertThat(mock.get(0),is("hello"));
6}
doThrow()模拟抛出异常
1@Test(expected=RuntimeException.class)
2publicvoidmockTest(){
3Listlist=mock(List.class);
4doThrow(newRuntimeException()).when(list).add(1);
5list.add(1);
6}
@Mock注解
在上面的测试中我们在每个测试方法里都mock
了一个List对象,为了避免重复的mock
,使测试类更具有可读性,我们可以使用下面的注解方式来快速模拟对象:
1//@RunWith(MockitoJUnitRunner.class)
2publicclassMockitoTest{
3@Mock
4privateListlist;
5
6publicMockitoTest(){
7//初始化@Mock注解
8MockitoAnnotations.initMocks(this);
9}
10
11@Test
12publicvoidshorthand(){
13list.add(1);
14verify(list).add(1);
15}
16}
when()参数匹配
1@Test
2publicvoidmockTest(){
3Comparablecomparable=mock(Comparable.class);
4//预设根据不同的参数返回不同的结果
5when(comparable.compareTo("Test")).thenReturn(1);
6when(comparable.compareTo("Omg")).thenReturn(2);
7assertThat(comparable.compareTo("Test"),is(1));
8assertThat(comparable.compareTo("Omg"),is(2));
9//对于没有预设的情况会返回默认值
10assertThat(list.get(1),is(999));
11assertThat(comparable.compareTo("Notstub"),is(0));
12}
Answer修改对未预设的调用返回默认期望
1@Test
2publicvoidmockTest(){
3//mock对象使用Answer来对未预设的调用返回默认期望值
4Listlist=mock(List.class,newAnswer(){
5@Override
6publicObjectanswer(InvocationOnMockinvocation)throwsThrowable{
7return999;
8}
9});
10//下面的get(1)没有预设,通常情况下会返回NULL,但是使用了Answer改变了默认期望值
11assertThat(list.get(1),is(999));
12//下面的size()没有预设,通常情况下会返回0,但是使用了Answer改变了默认期望值
13assertThat(list.size(),is(999));
14}
spy()监控真实对象
Mock不是真实的对象,它只是创建了一个虚拟对象,并可以设置对象行为。而Spy是一个真实的对象,但它可以设置对象行为。
1@Test(expected=IndexOutOfBoundsException.class)
2publicvoidmockTest(){
3Listlist=newLinkedList();
4Listspy=spy(list);
5//下面预设的spy.get(0)会报错,因为会调用真实对象的get(0),所以会抛出越界异常
6when(spy.get(0)).thenReturn(3);
7//使用doReturn-when可以避免when-thenReturn调用真实对象api
8doReturn(999).when(spy).get(999);
9//预设size()期望值
10when(spy.size()).thenReturn(100);
11//调用真实对象的api
12spy.add(1);
13spy.add(2);
14assertThat(spy.size(),is(100));
15assertThat(spy.size(),is(1));
16assertThat(spy.size(),is(2));
17verify(spy).add(1);
18verify(spy).add(2);
19assertThat(spy.get(999),is(999));
20}
reset()重置mock
1@Test
2publicvoidreset_mock(){
3Listlist=mock(List.class);
4when(list.size()).thenReturn(10);
5list.add(1);
6assertThat(list.size(),is(10));
7//重置mock,清除所有的互动和预设
8reset(list);
9assertThat(list.size(),is(0));
10}
times()验证调用次数
1@Test
2publicvoidverifying_number_of_invocations(){
3Listlist=mock(List.class);
4list.add(1);
5list.add(2);
6list.add(2);
7list.add(3);
8list.add(3);
9list.add(3);
10//验证是否被调用一次,等效于下面的times(1)
11verify(list).add(1);
12verify(list,times(1)).add(1);
13//验证是否被调用2次
14verify(list,times(2)).add(2);
15//验证是否被调用3次
16verify(list,times(3)).add(3);
17//验证是否从未被调用过
18verify(list,never()).add(4);
19//验证至少调用一次
20verify(list,atLeastOnce()).add(1);
21//验证至少调用2次
22verify(list,atLeast(2)).add(2);
23//验证至多调用3次
24verify(list,atMost(3)).add(3);
25}
inOrder()验证执行顺序
1@Test
2publicvoidverification_in_order(){
3Listlist=mock(List.class);
4Listlist2=mock(List.class);
5list.add(1);
6list2.add("hello");
7list.add(2);
8list2.add("world");
9//将需要排序的mock对象放入InOrder
10InOrderinOrder=inOrder(list,list2);
11//下面的代码不能颠倒顺序,验证执行顺序
12inOrder.verify(list).add(1);
13inOrder.verify(list2).add("hello");
14inOrder.verify(list).add(2);
15inOrder.verify(list2).add("world");
16}
verifyZeroInteractions()验证零互动行为
1@Test
2publicvoidmockTest(){
3Listlist=mock(List.class);
4Listlist2=mock(List.class);
5Listlist3=mock(List.class);
6list.add(1);
7verify(list).add(1);
8verify(list,never()).add(2);
9//验证零互动行为
10verifyZeroInteractions(list2,list3);
11}
verifyNoMoreInteractions()验证冗余互动行为
1@Test(expected=NoInteractionsWanted.class)
2publicvoidmockTest(){
3Listlist=mock(List.class);
4list.add(1);
5list.add(2);
6verify(list,times(2)).add(anyInt());
7//检查是否有未被验证的互动行为,因为add(1)和add(2)都会被上面的anyInt()验证到,所以下面的代码会通过
8verifyNoMoreInteractions(list);
9
10Listlist2=mock(List.class);
11list2.add(1);
12list2.add(2);
13verify(list2).add(1);
14//检查是否有未被验证的互动行为,因为add(2)没有被验证,所以下面的代码会失败抛出异常
15verifyNoMoreInteractions(list2);
16}
注:
- 如果使用异步的servlet,不能用StandaloneMockMvcBuilder方式进行测试,AsyncContext.getResponse得出的response是一个null,即使加上@SpringTest加载了上下文也是这样
2.@SpringBootTest会开启模拟容器来模拟正式运行,所以会加载相关注解(component)和配置
3.可以使用maven-surefire-plugin的或者mvntest-DargLine在测试的时候jvm启动参数或者增加启动命令,比如我的程序执行需要用javaagent去修改字节码,则可以使用:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M4</version>
<configuration>
<argLine>-javaagent:xxxxxxx.jar</argLine>
</configuration>
</plugin>
或者
mvntest-DargLine=-javaagentxxxxxxx.jar
4.如果想设置默认跳过单测,可以用maven-surefire-plugin设置skipTests=${skipTests},然后如果想进行单测,则可以直接mvntest-DskipTests=true。skipTests优先级为configuration>命令>properties中配置的。是否跳过单测最终结果为skipTests||maven.test.skip
<properties>
<skipTests>true</skipTests>
</properties>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M4</version>
<configuration>
<skipTests>${skipTests}</skipTests>
</configuration>
</plugin>
5.maven多module项目中千万不要引入其他模块的单元测试代码:
经过参考一些Maven的资料得知,其工作机制实际上是包的依赖管理。在规定的标准目录下,能够在模块之间引用的代码只能存在于main目录下。而单元测试(test目录下的代码)模型是建立在“独立”的思想之上的,目的就是不受其他环境的干扰从而纯粹地验证自身模块的可用性和正确性。因此单元测试代码之间是不能被其他模块引用的
6.如果使用了jacoco,它会依赖maven-surefire-plugin的argLine,所以如果你在该插件中用了argLine,建议按以下方式增加${argLine}
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M4</version>
<configuration>
<argLine>${argLine}-javaagent:xxxxxxx.jar</argLine>
</configuration>
</plugin>
参考:
https://maven.apache.org/surefire/maven-surefire-plugin/examples/skipping-tests.html
http://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html
https://blog.csdn.net/chaijunkun/article/details/35796335
https://stackoverflow.com/questions/18107375/getting-skipping-jacoco-execution-due-to-missing-execution-data-file-upon-exec
本文内容总结:相关注解,@SpringBootTest,单元测试回滚,断言,基本的单元测试例子,Controller测试,Mock数据,
原文链接:https://www.cnblogs.com/fnlingnzb-learner/p/12068505.html