背景
一直以来,我都觉得写单元测试是一个很酷的事情,也是减少自己代码 BUG 的一种有效手段,但是在一开始写的时候会遇到各种各样的问题,而不得不中断了单元测试。下边的一些场景是开发过程中有效阻挡我们单测的情况。
- 时间不够
- 写需求的时间就不够
- 构造数据太麻烦
- 代码不好测
- 不知道怎么测
- 代码结构不支持优雅的单元测试
- 某些代码不知道该不该单元测试
单元测试
时间不够
时间不够没办法,要么适当的排期增加一点开发时间,要么收集一些工具来加快单元测试。时间可能由于客观原因不是自己能把控的。工具则是我们非常需要关注的,例如IDEA的TestMe插件,就可以有效的减少我们书写Mockito的代码,非常nice。
构建数据太麻烦,这个只能靠平时的积累,可以使用一些mock工具,快捷生成对应的bean,填充默认配置,但是在于一些数据的校验还是有必要的,可以验证程序执行的正确性。
代码不好测
不知道怎么测,这个应该是大多数Java程序员都存在的一个问题,我们的项目如果有单元测试,往往也是直接使用 @SpringBootTest 直接运行整个 Spring 上下文进行测试,它即会加载DB,也会加载Redis,这个在本地测试的时候还可以,但是接入CICD就会有很大的问题。 因为简单,这样的单元测试我是不推荐的,但是不妨碍大多数人都是这么用的。 合理的单元测试应该是不需要依赖 Spring 上下文的,在任何环境中打包都是可以执行的。这就要求Java程序员熟练的掌握一门测试框架,例如 Mockito的应用,这样可以非常高效的提升我们的效率,知道在什么场景下使用什么Mock。 是when, 还是 doNoting() 都是需要经验的。
代码结构不支持优雅的单元测试,如果在平时单元测试过程中,觉得单元测试很难执行下去,或者说觉得哪里不通畅,那么代码结构就需要改变了,例如有这样的一个代码场景.
public class Example {public void testMe(){//第三方库的一个静态方法StaticClass.staticMethod('aa',2);}}public class StaticClass {public final static RedisTemplate redisTemplate = ApplicatonContext.getBean(RedisTemplate.class);public void staticMethod(... params){//第三方库提供的//redisTemplate....method()}}
这是一个非常常见的业务代码,但是在单元测试的执行过程中,因为不是出于spring的上下文中,可能会报错,直接抛出异常,进而可能由于 改动难度大?、需求时间不够 的原因而慢慢的放弃了单元测试。
因此,对于这样不方便的代码,应该修改成合适的,方便测试的代码,按照上边的代码,结构就可以调整成
public class Example {@Resourceprivate CacheService cacheService;public void testMe(){//第三方库的一个静态方法cacheService.method();}}public class CacheService {public void method(){StaticClass.staticMethod('aa',2);}}
这样的好处就是, `CacheService` 就调用第三方库的方法,可以理解成代码可信的,而 `Example` 的单元测试就可以直接Mock `CacheService` 的方法调用,这样来规避问题。
同样重要的是,尽管这里我使用 包装 的形式来加强单元测试的,但也必须说明,静态方法中塞入Spring上下文的方式是不合适的。我们的项目代码应该尽量避免。
由于这里的经验,又让我明白了某本书中的一句话,大概意思是,如果你的代码不好进行单元测试,那就应该重构结构让它去适应单元测试。
某些代码不知道该不该单元测试
另外互联网上也存在一些 哪些需要测试,哪些不需要测试 的讨论,这里的集中点一般都是 Dao 层面上的,主要是DB相关的。
各执一词的都有,我认为 Dao 是可以不测的,这里的测试也只能保证执行的sql语法是对的,但是这个又可以放到后边去进行。没有必要为了SQL的测试去有意的准备数据。
Mockito 的一些技巧
Mock实例方法调用
class TestServiceTest {@SpyExample example;@InjectMocksTestService testService;@BeforeEachvoid setUp() {MockitoAnnotations.openMocks(this);}@Testvoid testService() {//返回voiddoNothing().when(example).example();//不执行testExecute(), 代码运行时会返回first testExecute,then returndoReturn("first testExecute,then return").when(example).testExecute();//调用直接返回thenwhen(example.testExecute()).thenReturn("then");testService.service();}@Testvoid test2() {//由于使用@Spy,如果不进行Mock就会调用真实方法,在http调用时非常有效doNothing().when(example).example();testService.service();}}
Mock静态方法调用
注意: void返回的静态方法不需要进行mock,
loginUtilsMockedStatic = mockStatic(Goo.class)会自动注册doNothing().when(example).method();
@Service("testService")public class TestService {@Resourceprivate Example example;public void service() {System.out.println("ok");example.example();System.out.println(example.testExecute());}public void invoke() {example.example();}public void handle() {System.out.println("test handle");Goo.handle();System.out.println(Goo.returnValue());System.out.println(Goo.returnValueWithParams("ff"));}public void invokeHandle(){System.out.println("test invokeHandle");Goo.handle();}}public class Goo {public static void handle() {System.out.println("static go");}public static String returnValue() {System.out.println("returnValue");return "returnValue";}public static String returnValueWithParams(String value) {return "returnValueWithParams";}}//test codeimport static org.mockito.Mockito.*;class TestServiceStaticTest {@SpyExample example;@InjectMocksTestService testService;@BeforeEachvoid setUp() {MockitoAnnotations.openMocks(this);}static MockedStatic<Goo> loginUtilsMockedStatic;@BeforeAllpublic static void beforeClass() throws Exception {loginUtilsMockedStatic = mockStatic(Goo.class);}@Testvoid test5() {testService.invokeHandle();}@Testvoid test4() {loginUtilsMockedStatic.when(Goo::returnValue).thenReturn("ok1");loginUtilsMockedStatic.when(() -> Goo.returnValueWithParams(anyString())).thenReturn("ok2");testService.handle();}@Testvoid test2() {doNothing().when(example).example();testService.service();}@Testvoid test3() {testService.service();verify(example, times(1)).example();verify(example, times(1)).testExecute();}@AfterAllpublic static void afterClass() throws Exception {loginUtilsMockedStatic.close();}}
Mock方法和真实方法混用
一些方法之间的区别
- doReturn(“G”).when(example).valueCantBeNull(anyInt());
- 直接mock,不会调用真实方法
- when(example.valueCantBeNull(anyInt())).thenReturn(“G”);
- 会调用一次真实方法
anyInt的值是0,如果真实代码内部做了如下的校验,那么使用 when(mock.method(anyInt())).thenReturn("g") 会报错。
public String valueCantBeNull(int value) {Assert.isTrue(value > 10, "value必须大于10");return "valueCantBeNull";}
java.lang.IllegalArgumentException: value必须大于10at org.springframework.util.Assert.isTrue(Assert.java:121)at io.github.chenshun00.springcloud.provider.test.Example.valueCantBeNull(Example.java:14)
引用链接
https://stackoverflow.com/questions/64717683/mockito-donothing-with-mockito-mockstatic
