背景
一直以来,我都觉得写单元测试是一个很酷的事情,也是减少自己代码 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 {
@Resource
private 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 {
@Spy
Example example;
@InjectMocks
TestService testService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testService() {
//返回void
doNothing().when(example).example();
//不执行testExecute(), 代码运行时会返回first testExecute,then return
doReturn("first testExecute,then return").when(example).testExecute();
//调用直接返回then
when(example.testExecute()).thenReturn("then");
testService.service();
}
@Test
void 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 {
@Resource
private 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 code
import static org.mockito.Mockito.*;
class TestServiceStaticTest {
@Spy
Example example;
@InjectMocks
TestService testService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
static MockedStatic<Goo> loginUtilsMockedStatic;
@BeforeAll
public static void beforeClass() throws Exception {
loginUtilsMockedStatic = mockStatic(Goo.class);
}
@Test
void test5() {
testService.invokeHandle();
}
@Test
void test4() {
loginUtilsMockedStatic.when(Goo::returnValue).thenReturn("ok1");
loginUtilsMockedStatic.when(() -> Goo.returnValueWithParams(anyString())).thenReturn("ok2");
testService.handle();
}
@Test
void test2() {
doNothing().when(example).example();
testService.service();
}
@Test
void test3() {
testService.service();
verify(example, times(1)).example();
verify(example, times(1)).testExecute();
}
@AfterAll
public 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必须大于10
at 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