单元测试-SpringBoot Test和Mock
“单元测试”
“junit,mock,桩”
1. 什么是单元测试
定义:是指对软件中的最小可测试单元进行检查和验证。
Java里单元指一个方法。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
2. 单元测试与Spring Boot
2.1 引入依赖spring-boot-starter-test
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
spring-boot-starter-test中包含了junit和mockito等依赖
2.2 相关依赖
- junit – 标准的单元测试Java应用程序
- Spring Test & Spring Boot Test – 对Spring Boot应用程序的单元测试提供支持
- Mockito, Java mocking框架,用于模拟任何Spring管理的Bean,比如在单元测试中模拟一个第三方系统Service接口返回的数据,而不会去真正调用第三方系统;
- AssertJ,一个流畅的assertion库,同时也提供了更多的期望值与测试返回值的比较方式;
- JSONassert,对JSON对象或者JSON字符串断言的库。
- …………
2.3 标准的Spring Boot单元测试结构
@DisplayName("AlarmMsgstationController测试类")//起别名@SpringBootTest@AutoConfigureMockMvc@Transactional@TestMethodOrder(MethodOrderer.OrderAnnotation.class)publicclassTest{}
3. SpringBoot Test常用注解
4. 基本用法
类上添加注解,启动Spring Boot环境
@RunWith(SpringRunner.class)@SpringBootTestpublicclassFirstTest{@Testpublicvoidtest(){int a=1;Assertions.assertEquals(1,a);//判断二者是否相等}}
可以使用Assertions类来判断结果是否符合预期;
4.1 直接注入
对数据访问层(Service层同理)
ps:@Autowired直接注入的方法会真实操作数据库,如果在单元测试中不想改变数据数据库中的值,不能使用直接注入的方法
其实可以在类上再添加这两个注解,通过@Transactional可以知道调用了数据库,对其操作进行回滚
但是如果项目中使用了@Component注解(在SpringBoot项目启动的时候就会跟着实例化/启动),@Component注解的类里有多线程方法,那么在执行单元测试的时候,由于多线程任务的影响,就可能对数据库造成了数据修改,即使使用了事务回滚注解@Transactional。(我在百度上看到的,没找到具体的测试方法,所以没试)
@Component注解:带此注解的类看为组件,当使用基于该注解的配置和类路径扫描的时候,这些类就会被实例化。
@Transactional@Rollback(true)// 事务自动回滚,默认是true。可以不写
4.2 Mock注入
实现原理:使用Stub(桩)技术动态的替换原程序的功能。
直接跑Java代码,不需要启用Spring及连接数据库,模拟一切操作数据库的步骤,不执行任何SQL,也可以模拟任何返回值
4.2.1 使用Mock的优点:
- 可以完全脱离数据库
- 只针对某一个小方法(一个小的单元)来测试,测试过程中,不需要启动其他的东西,不免其他因素可能产生的干扰
4.2.2 编写Mock代码
- 不再使用@Autowired启动Spring会导致运行单元测试的时候的速度变慢(run->Junit Test),单元测试只针对某一个类的方法来测试,不需要启动Spring,只需要对应的实体实例就够了,在需要注入bean的时候直接new
- 不再使用@SpringBootTest
- 不调用数据库@Transactional @Rollback(true)这两个注解也不要
- 使用Assert断言
基本应用:
mock 对象的方法的返回值默认都是返回类型的默认值
importorg.junit.Assert;importorg.junit.Test;importjava.util.Random;importstaticorg.mockito.Mockito.*;publicclassMockitoDemo{@Testpublicvoidtest(){Random mockRandom =mock(Random.class);//mock了一个Random对象Assert.assertEquals(0, mockRandom.nextInt());//未进行打桩,每次返回值都是0when(mockRandom.nextInt()).thenReturn(100);// 进行打桩操作,指定调用 nextInt 方法时,永远返回 100Assert.assertEquals(100, mockRandom.nextInt());}}
4.2.3 Mock的注解和常用的方法
@Mock
@Mock 注解可以理解为对 mock 方法的一个替代。使用该注解时,要使用
MockitoAnnotations.initMocks
方法,让注解生效。旧版的是initMocks,新版的是openMocks
importorg.junit.Assert;importorg.junit.Before;importorg.junit.Test;importorg.mockito.Mock;importorg.mockito.MockitoAnnotations;importjava.util.Random;importstaticorg.mockito.Mockito.*;publicclassMockitoDemo{@MockprivateRandom random;@Beforepublicvoidbefore(){// 让注解生效MockitoAnnotations.initMocks(this);}@Testpublicvoidtest(){when(random.nextInt()).thenReturn(100);Assert.assertEquals(100, random.nextInt());}}
也可以用
MockitoJUnitRunner
来代替
MockitoAnnotations.initMocks
importorg.junit.Assert;importorg.junit.Test;importorg.junit.runner.RunWith;importorg.mockito.Mock;importorg.mockito.junit.MockitoJUnitRunner;importjava.util.Random;importstaticorg.mockito.Mockito.*;@RunWith(MockitoJUnitRunner.class)publicclassMockitoDemo{@MockprivateRandom random;@Testpublicvoidtest(){when(random.nextInt()).thenReturn(100);Assert.assertEquals(100, random.nextInt());}}
@Spy
mock()方法与spy()方法的不同:
- 被spy的对象会走真实的方法,而mock对象不会
- spy方法的参数是对象实例,mock的参数是class
@InjectMocks
mockito 会将
@Mock
、
@Spy
修饰的对象自动注入到
@InjectMocks
修饰的对象中
thenReturn
thenReturn 用来指定特定函数和参数调用的返回值;
thenReturn 中可以指定多个返回值。在调用时返回值依次出现。若调用次数超过返回值的数量,再次调用时返回最后一个返回值。
doReturn 的作用和 thenReturn 相同,但使用方式不同:
when(mockRandom.nextInt()).thenReturn(1);//返回值为1when(mockRandom.nextInt()).thenReturn(1,2,3);doReturn(1).when(random).nextInt();
thenThrow
thenThrow 用来让函数调用抛出异常。(可搭配try catch使用)
thenThrow 中可以指定多个异常。在调用时异常依次出现。若调用次数超过异常的数量,再次调用时抛出最后一个异常。
when(mockRandom.nextInt()).thenThrow(newRuntimeException("异常"));when(mockRandom.nextInt()).thenThrow(newRuntimeException("异常1"),newRuntimeException("异常2"));@Testpublicvoidtest(){Random mockRandom =mock(Random.class);when(mockRandom.nextInt()).thenThrow(newRuntimeException("异常1"),newRuntimeException("异常2"));try{
mockRandom.nextInt();Assert.fail();//上一行会抛出异常,到catch中去,走不到这里}catch(Exception ex){Assert.assertTrue(ex instanceofRuntimeException);Assert.assertEquals("异常1", ex.getMessage());}try{
mockRandom.nextInt();Assert.fail();}catch(Exception ex){Assert.assertTrue(ex instanceofRuntimeException);Assert.assertEquals("异常2", ex.getMessage());}}
对应返回类型是 void 的函数,thenThrow 是无效的,要使用 doThrow。也可以用 doThrow 让返回非void的函数抛出异常
doThrow(newRuntimeException("异常")).when(exampleService).hello();// 下面这句等同于 when(random.nextInt()).thenThrow(new RuntimeException("异常"));doThrow(newRuntimeException("异常")).when(random).nextInt();
reset
使用 reset 方法,可以重置之前自定义的返回值和异常。
reset(exampleService);
vetify
使用 verify 可以校验 mock 对象是否发生过某些操作,配合 time 方法,可以校验某些操作发生的次数
//判断backOutstockMapper.selectReportCountByMap()方法是否被调用1次verify(backOutstockMapper,times(1)).selectReportCountByMap(Mockito.any());//校验backOutstockMapper.selectReportCountByMap()方法是否被调用过verify(backOutstockMapper).selectReportCountByMap(Mockito.any());
4.2.5 断言
->assertTrue(String message,boolean condition) 要求condition ==true->assertFalse(String message,boolean condition) 要求condition ==false->assertEquals(String message,XXX expected,XXX actual) 要求expected期望的值能够等于actual
->assertArrayEquals(String message,XXX[] expecteds,XXX[] actuals) 要求expected.equalsArray(actual)->assertNotNull(String message,Object object) 要求object!=null->assertNull(String message,Object object) 要求object==null->assertSame(String message,Object expected,Object actual) 要求expected == actual
->assertNotSame(String message,Object unexpected,Object actual) 要求expected != actual
->assertThat(String reason,T actual,Matcher matcher) 要求matcher.matches(actual)==true->fail(String message) 要求执行的目标结构必然失败,同样要求代码不可达,即是这个方法在程序运行后不会成功返回,如果成功返回了则报错
4.3 Tips
- 对待类中私有的方法,可以用反射的方式进行测试
- 打包时跳过test
mvn deploy -f pom_http.xml-jar -Dmaven.test.skip=true
- Mockito 默认是不支持静态方法,可使用 PowerMock 让 Mockito 支持静态方法(新增依赖)
5. 总结
单元测试测试的不是整条业务线,而是类中的单个方法单元。
按照单一性原则的话,一个方法只做一件事,那么针对这个方法的单元测试就简单了。
当多个方法单元测试的结果都没问题的时候,多个方法聚合成的业务链照理说也是没问题的,一个方法中依赖了其他方法的处理结果或返回结果,那么这个结果应当是可预测的,所以也是可以mock出所有场景的,而单元测试也应该覆盖到不同结果对应的场景。
单元测试除了测试代码逻辑外,最大的好处是可以检验整体设计是否合理。一个方法做了太多事的话,就会导致单元测试很难覆盖,比如service层的方法,如果入参的校验,业务逻辑的处理,不同数据表DB的操作,DB返回结果的校验处理全部在单一方法中实现,那对于后期业务的扩展、维护、问题的排查都不好进行;如果把以上说的那些全部分离出来,封装成一个个独立的方法,最后只在一个方法中总调,这样不仅单元测试比较好实现,而且后期的维护,扩展都会很容易。
(ps:上面总结这段话不是我说的,是我在学习的过程中看到一位老哥写在评论区的。领导让我总结一下,我就抄过来敷衍领导了)
版权归原作者 Rigel爱科学 所有, 如有侵权,请联系我们删除。