背景

一直以来,我都觉得写单元测试是一个很酷的事情,也是减少自己代码 BUG 的一种有效手段,但是在一开始写的时候会遇到各种各样的问题,而不得不中断了单元测试。下边的一些场景是开发过程中有效阻挡我们单测的情况。

  • 时间不够
    • 写需求的时间就不够
    • 构造数据太麻烦
  • 代码不好测
    • 不知道怎么测
    • 代码结构不支持优雅的单元测试
  • 某些代码不知道该不该单元测试

    单元测试

    时间不够

    时间不够没办法,要么适当的排期增加一点开发时间,要么收集一些工具来加快单元测试。时间可能由于客观原因不是自己能把控的。工具则是我们非常需要关注的,例如IDEA的 TestMe 插件,就可以有效的减少我们书写Mockito的代码,非常nice。

构建数据太麻烦,这个只能靠平时的积累,可以使用一些mock工具,快捷生成对应的bean,填充默认配置,但是在于一些数据的校验还是有必要的,可以验证程序执行的正确性。

代码不好测

不知道怎么测,这个应该是大多数Java程序员都存在的一个问题,我们的项目如果有单元测试,往往也是直接使用 @SpringBootTest 直接运行整个 Spring 上下文进行测试,它即会加载DB,也会加载Redis,这个在本地测试的时候还可以,但是接入CICD就会有很大的问题。 因为简单,这样的单元测试我是不推荐的,但是不妨碍大多数人都是这么用的。 合理的单元测试应该是不需要依赖 Spring 上下文的,在任何环境中打包都是可以执行的。这就要求Java程序员熟练的掌握一门测试框架,例如 Mockito的应用,这样可以非常高效的提升我们的效率,知道在什么场景下使用什么Mock。 是when, 还是 doNoting() 都是需要经验的。

代码结构不支持优雅的单元测试,如果在平时单元测试过程中,觉得单元测试很难执行下去,或者说觉得哪里不通畅,那么代码结构就需要改变了,例如有这样的一个代码场景.

  1. public class Example {
  2. public void testMe(){
  3. //第三方库的一个静态方法
  4. StaticClass.staticMethod('aa',2);
  5. }
  6. }
  7. public class StaticClass {
  8. public final static RedisTemplate redisTemplate = ApplicatonContext.getBean(RedisTemplate.class);
  9. public void staticMethod(... params){
  10. //第三方库提供的
  11. //redisTemplate....method()
  12. }
  13. }

这是一个非常常见的业务代码,但是在单元测试的执行过程中,因为不是出于spring的上下文中,可能会报错,直接抛出异常,进而可能由于 改动难度大?需求时间不够 的原因而慢慢的放弃了单元测试。

因此,对于这样不方便的代码,应该修改成合适的,方便测试的代码,按照上边的代码,结构就可以调整成

  1. public class Example {
  2. @Resource
  3. private CacheService cacheService;
  4. public void testMe(){
  5. //第三方库的一个静态方法
  6. cacheService.method();
  7. }
  8. }
  9. public class CacheService {
  10. public void method(){
  11. StaticClass.staticMethod('aa',2);
  12. }
  13. }
  1. 这样的好处就是, `CacheService` 就调用第三方库的方法,可以理解成代码可信的,而 `Example` 的单元测试就可以直接Mock `CacheService` 的方法调用,这样来规避问题。

同样重要的是,尽管这里我使用 包装 的形式来加强单元测试的,但也必须说明,静态方法中塞入Spring上下文的方式是不合适的。我们的项目代码应该尽量避免。

由于这里的经验,又让我明白了某本书中的一句话,大概意思是,如果你的代码不好进行单元测试,那就应该重构结构让它去适应单元测试。

某些代码不知道该不该单元测试

另外互联网上也存在一些 哪些需要测试哪些不需要测试 的讨论,这里的集中点一般都是 Dao 层面上的,主要是DB相关的。

各执一词的都有,我认为 Dao 是可以不测的,这里的测试也只能保证执行的sql语法是对的,但是这个又可以放到后边去进行。没有必要为了SQL的测试去有意的准备数据。

Mockito 的一些技巧

Mock实例方法调用

  1. class TestServiceTest {
  2. @Spy
  3. Example example;
  4. @InjectMocks
  5. TestService testService;
  6. @BeforeEach
  7. void setUp() {
  8. MockitoAnnotations.openMocks(this);
  9. }
  10. @Test
  11. void testService() {
  12. //返回void
  13. doNothing().when(example).example();
  14. //不执行testExecute(), 代码运行时会返回first testExecute,then return
  15. doReturn("first testExecute,then return").when(example).testExecute();
  16. //调用直接返回then
  17. when(example.testExecute()).thenReturn("then");
  18. testService.service();
  19. }
  20. @Test
  21. void test2() {
  22. //由于使用@Spy,如果不进行Mock就会调用真实方法,在http调用时非常有效
  23. doNothing().when(example).example();
  24. testService.service();
  25. }
  26. }

Mock静态方法调用

注意: void返回的静态方法不需要进行mock,loginUtilsMockedStatic = mockStatic(Goo.class) 会自动注册 doNothing().when(example).method();

  1. @Service("testService")
  2. public class TestService {
  3. @Resource
  4. private Example example;
  5. public void service() {
  6. System.out.println("ok");
  7. example.example();
  8. System.out.println(example.testExecute());
  9. }
  10. public void invoke() {
  11. example.example();
  12. }
  13. public void handle() {
  14. System.out.println("test handle");
  15. Goo.handle();
  16. System.out.println(Goo.returnValue());
  17. System.out.println(Goo.returnValueWithParams("ff"));
  18. }
  19. public void invokeHandle(){
  20. System.out.println("test invokeHandle");
  21. Goo.handle();
  22. }
  23. }
  24. public class Goo {
  25. public static void handle() {
  26. System.out.println("static go");
  27. }
  28. public static String returnValue() {
  29. System.out.println("returnValue");
  30. return "returnValue";
  31. }
  32. public static String returnValueWithParams(String value) {
  33. return "returnValueWithParams";
  34. }
  35. }
  36. //test code
  37. import static org.mockito.Mockito.*;
  38. class TestServiceStaticTest {
  39. @Spy
  40. Example example;
  41. @InjectMocks
  42. TestService testService;
  43. @BeforeEach
  44. void setUp() {
  45. MockitoAnnotations.openMocks(this);
  46. }
  47. static MockedStatic<Goo> loginUtilsMockedStatic;
  48. @BeforeAll
  49. public static void beforeClass() throws Exception {
  50. loginUtilsMockedStatic = mockStatic(Goo.class);
  51. }
  52. @Test
  53. void test5() {
  54. testService.invokeHandle();
  55. }
  56. @Test
  57. void test4() {
  58. loginUtilsMockedStatic.when(Goo::returnValue).thenReturn("ok1");
  59. loginUtilsMockedStatic.when(() -> Goo.returnValueWithParams(anyString())).thenReturn("ok2");
  60. testService.handle();
  61. }
  62. @Test
  63. void test2() {
  64. doNothing().when(example).example();
  65. testService.service();
  66. }
  67. @Test
  68. void test3() {
  69. testService.service();
  70. verify(example, times(1)).example();
  71. verify(example, times(1)).testExecute();
  72. }
  73. @AfterAll
  74. public static void afterClass() throws Exception {
  75. loginUtilsMockedStatic.close();
  76. }
  77. }

Mock方法和真实方法混用

使用 Spy 来解决这个问题。

一些方法之间的区别

  • doReturn(“G”).when(example).valueCantBeNull(anyInt());
    • 直接mock,不会调用真实方法
  • when(example.valueCantBeNull(anyInt())).thenReturn(“G”);
    • 会调用一次真实方法

anyInt的值是0,如果真实代码内部做了如下的校验,那么使用 when(mock.method(anyInt())).thenReturn("g") 会报错。

  1. public String valueCantBeNull(int value) {
  2. Assert.isTrue(value > 10, "value必须大于10");
  3. return "valueCantBeNull";
  4. }
  1. java.lang.IllegalArgumentException: value必须大于10
  2. at org.springframework.util.Assert.isTrue(Assert.java:121)
  3. at io.github.chenshun00.springcloud.provider.test.Example.valueCantBeNull(Example.java:14)

引用链接

https://stackoverflow.com/questions/64717683/mockito-donothing-with-mockito-mockstatic

https://tonydeng.github.io/2017/10/10/junit-5-annotations/