0


使用Mockito框架进行单元测试对@SpringBootTest的单元测试可能会造成影响,诸如导致空指针异常等

背景:

使用

@SpringBootTest

进行单元测试其实不符合单元测试理念,主要有以下几个原因:

  1. 测试范围过大:单元测试的目标是对应用中的单个“单元”进行测试,通常是类或方法级别的测试。使用@SpringBootTest会启动整个Spring应用程序上下文,这意味着测试的范围不仅包括目标单元,还包括其所有依赖项和配置。这更像是集成测试,而不是严格意义上的单元测试。
  2. 速度慢:由于@SpringBootTest会启动完整的Spring应用程序上下文,测试的执行速度会比纯单元测试慢得多。单元测试应该是快速的,以便能够频繁运行并快速反馈问题。
  3. 依赖环境:单元测试应该是独立的,不依赖外部环境或复杂的上下文配置。使用@SpringBootTest会引入对Spring上下文的依赖,使得测试可能会受到环境配置的影响,不再是独立和可重复的。
  4. 复杂性增加:Spring应用程序上下文的启动涉及大量的Bean初始化和配置,增加了测试的复杂性。单元测试应该尽量保持简单,专注于测试目标单元的逻辑。

单元测试的主要理念包括:

  • 隔离:每个单元测试应该只测试一个独立的单元(例如一个类或一个方法),而不依赖于其他单元或外部系统(例如数据库、网络等)。
  • 快速:单元测试应该执行得非常快,以便在开发过程中能够频繁运行,快速反馈。
  • 可重复:单元测试应该是完全可重复的,任何时间在任何环境下运行结果都应该是一样的。
  • 独立:每个单元测试应该是独立的,不应该依赖于其他测试或共享状态。

总的来说,使用

@SpringBootTest

适合于集成测试而不是单元测试。为了更好地遵循单元测试的理念,应避免在单元测试中使用

@SpringBootTest

,而是使用更轻量级的测试方法和工具。所以我们选取Mockito框架进行单元测试。

现象:

引入Mockito之后一些存量代码中带有@SpringBootTest注解的单元测试在打包时有可能会抛出空指针,并且在本地单个单元测试运行永远不会出现,只有使用maven -install时会出现。甚至报错的单元测试类也不一定,有时候是A测试类,有时候是B测试类,甚至有时候,A测试类的a方法还是正常的,b方法就开始报空指针,空指针定位最终原因全都是某个容器中的bean是空。

原因分析:

首先,需要明确一点

在使用

@SpringBootTest

进行单元测试时,Spring Boot 默认情况下会为每个测试类启动一次 Spring 应用程序上下文。这意味着如果你有多个带有

@SpringBootTest

注解的测试类,每个测试类在执行时都会各自启动一次应用程序上下文。

然而,Spring 框架非常智能,能够缓存应用程序上下文,以减少不必要的启动时间。具体来说:

上下文缓存机制

Spring Test 框架有一个默认的上下文缓存机制。默认情况下,Spring 会缓存相同配置的应用程序上下文,并在多个测试类之间共享它们。这意味着,如果两个测试类的配置完全相同,它们将共享同一个应用程序上下文,从而避免多次启动容器的开销。

示例

假设有两个测试类,它们都使用相同的

@SpringBootTest

配置:

@SpringBootTest 
public class MyFirstTest { 
    @Test 
    void testSomething() { // 测试逻辑 } 
} 

@SpringBootTest 
public class MySecondTest { 
    @Test 
    void testSomethingElse() { // 测试逻辑 } 
}

在这种情况下,如果这两个测试类的配置相同,Spring 将只启动一次应用程序上下文,并在两个测试类之间共享。

如何确保上下文被缓存

为了确保上下文缓存机制正常工作,需要注意以下几点:

  1. 相同配置:测试类的配置必须完全相同,包括使用的注解和属性配置。
  2. 不同配置:如果两个测试类有不同的配置,例如使用不同的 @TestPropertySource 或者 @MockBean,Spring 将认为它们需要不同的应用程序上下文,因此会分别启动。
强制隔离上下文

在某些情况下,你可能希望确保每个测试类都使用一个新的应用程序上下文。你可以使用

@DirtiesContext

注解来强制 Spring 在测试完成后关闭并重新创建应用程序上下文。

@SpringBootTest 
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_CLASS)
public class MyFirstTest { 
    @Test 
    void testSomething() { // 测试逻辑 } 
}

在上面的示例中,每个测试类在完成后,Spring 都会标记上下文为脏的,并在下一个测试类执行前重新创建上下文。

  • 默认行为:如果多个 @SpringBootTest 注解的测试类配置相同,Spring 将只启动一次应用程序上下文,并在这些测试类之间共享。
  • 上下文缓存:Spring 有一个上下文缓存机制,可以在相同配置的测试类之间共享上下文。
  • 配置影响:如果测试类有不同的配置,Spring 将启动不同的上下文。
  • 强制隔离:可以使用 @DirtiesContext 注解来强制每个测试类使用新的上下文。

通过了解和利用 Spring 的上下文缓存机制,可以有效地提高测试执行的效率,并减少不必要的资源消耗。

Mockito对容器Mock

了解了Spring单元测试的上下文缓存机制之后,还有一点就是项目中的代码使用Mockito框架对一个至关重要的工具类进行了Mock。相信大多数项目中都会由这样一个工具类吧,用静态方法获取容器中的Bean,因为某些代码在设计上是与容器时解耦的,但实际业务在运行到某处又需要容器里的一些Bean来处理,当然这是设计上的失误,此处先不提。

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringContextUtil.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }

    public static <T> T getBean(String name, Class<T> clazz) {
        return applicationContext.getBean(name, clazz);
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
}

我在使用Mockito对某些类写单元测试时,发现被测试的类引用了这个工具类,所以我需要对这个工具类进行Mock,因为如果我不Mock他,容器也没有启动,必然会造成空指针(ApplicationContextAware本身就是容器感知接口,容器创建后会触发setApplicationContext方法,这个方法如果没有执行,这个类其他方法肯定是会报空指针的)。但是由于项目中没有引入PowerMock框架,所以无法对静态方法进行Mock,我就退而求其次,对这个静态工具类的静态变量通过反射进行设置,我Mock了一个applicationContext,然后通过反射设置进了这个静态工具类。然后当时我要做的单元测试完美通过。隐患也就此埋下。

要知道打包时会跑所有的单元测试,如果此时已经有@SpringBootTest的单元测试运行过后,这个applicationContext其实就已经指向Spring容器,而所有的单元测试都是在同一个虚拟机运行,对容器的Mock导致了之前创建的Spring容器被我Mock掉了,所以后续的@SpringBootTest的单元测试由于上下文缓存机制,会沿用之前的Spring容器,必然会报空指针,而且每次单元测试运行的顺序不一定,导致了空指针出现的所在单元测试类也不确定。

以上就是单元测试空指针的根本原因。

解决方案:

首先强制隔离上下文理论上是可以的,但是每个单元测试都启动容器效率太低。

其次,引入PowerMock,对SpringContextUtil中静态方法进行Mock,这样可以避免Mock容器。但是需要注意,被Mock的静态方法可能会导致其他错误,还是因为那个问题,所有单元测试是在一个虚拟机里执行的,运行Mock单元测试前后的SpringContextUtil行为是不一致的,Mock之前他是真正的从容器拿Bean,Mock之后他返回的是一个假的Bean。还有就是PowerMock使用起来比Mockito更复杂,需要特别注意类加载器和其他配置。

或者对于引用SpringContextUtil的类进行单元测试继续沿用@SpringBootTest。我用的是这个方法。

标签: 单元测试

本文转载自: https://blog.csdn.net/m0_53421851/article/details/139511481
版权归原作者 Zoro.Xu 所有, 如有侵权,请联系我们删除。

“使用Mockito框架进行单元测试对@SpringBootTest的单元测试可能会造成影响,诸如导致空指针异常等”的评论:

还没有评论