@SpringBootTest、@ContextConfiguration和@Import在Spring Boot单元测试中的区别

33

我正在开发一个Spring Boot项目。 我正在编写基于TDD的Unit Test代码,这有点困难。

@SpringBootTest加载所有bean,导致测试时间更长。

因此,我使用了@SpringBootTest的类指定。

我正常完成了测试,但我不确定使用@ContextConfiguration和使用@Import之间的区别。

所有三个选项都可以正常运行。我想知道哪个选择最好。

@Service
public class CoffeeService {

    private final CoffeeRepository coffeeRepository;

    public CoffeeService(CoffeeRepository coffeeRepository) {
        this.coffeeRepository = coffeeRepository;
    }

    public String getCoffee(String name){
        return coffeeRepository.findByName(name);
    }
}

public interface CoffeeRepository {
    String findByName(String name);
}

@Repository
public class SimpleCoffeeRepository implements CoffeeRepository {

    @Override
    public String findByName(String name) {
        return "mocha";
    }
}

选项1 使用@SpringBootTest - OK

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {CoffeeService.class, SimpleCoffeeRepository.class})
public class CoffeeServiceTest {

    @Autowired
    private CoffeeService coffeeService;

    @Test
    public void getCoffeeTest() {
        String value = coffeeService.getCoffee("mocha");
        assertEquals("mocha", value);
    }
}

选项2使用@ContextConfiguration - 可行

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {SimpleCoffeeRepository.class, CoffeeService.class})
public class CoffeeServiceTest {

    @Autowired
    private CoffeeService coffeeService;

    @Test
    public void getCoffeeTest() {
        String value = coffeeService.getCoffee("mocha");
        assertEquals("mocha", value);
    }
}

选项3使用@Import - 可行

@RunWith(SpringRunner.class)
@Import({SimpleCoffeeRepository.class, CoffeeService.class})
public class CoffeeServiceTest {

    @Autowired
    private CoffeeService coffeeService;

    @Test
    public void getCoffeeTest() {
        String value = coffeeService.getCoffee("mocha");
        assertEquals("mocha", value);
    }
3个回答

57
我认为,如果你的目的是进行适当的单元测试,那么这三个选项都不好。 一个单元测试必须非常快速,你应该能够在一秒钟内运行数百个测试(当然,这取决于硬件,但你明白我的意思)。 所以,一旦你说“我为每个测试启动Spring” - 它就不再是一个单元测试了。 为每个测试启动Spring是一个非常昂贵的操作。
有趣的是,你的CoffeeService代码的编写方式使其非常适合测试:只需使用像Mockito这样的库来模拟存储库类,你就可以在完全没有Spring的情况下测试服务逻辑。 你不需要任何Spring运行器,也不需要任何Spring注解。你还会发现这些测试运行得更快。
class MyServiceTest {

    @Test
    public void test_my_service_get_coffee_logic() {
          
           // setup:
           CoffeeRepository repo = Mockito.mock(CoffeeRepository.class);
           Mockito.when(repo.findByName("mocha")).thenReturn("coffeeFound");

           CoffeeService underTest = new CoffeeService(repo);

           // when:
           String actualCoffee  =  underTest.getCoffee("mocha");

           // then:
           assertEquals(actualCoffee, "coffeeFound");
    }
}
 

关于Spring测试库的问题,
你可以将其视为一种测试需要与其他组件进行一些互联的代码的方式,并且很难将所有内容都模拟出来。它是在同一个JVM中进行的一种集成测试。 你所提到的所有方法都会运行一个应用程序上下文,实际上,在底层有很多复杂的事情发生,关于应用程序上下文启动过程,YouTube上有很多关于这个问题的讲座,虽然超出了问题的范围,但重点是执行上下文启动需要时间。
@SpringBootTest更进一步,试图模拟Spring Boot框架为创建上下文添加的过程:根据包结构决定要扫描什么,从预定义位置加载外部配置,可选择运行自动配置启动器等等。
现在,可能会加载应用程序中的所有bean的应用程序上下文可能非常庞大,对于某些测试来说,这是不必要的。 这通常取决于测试的目的。
例如,如果你测试的是REST控制器(你已经正确放置了所有注解),可能就不需要启动数据库连接。
你所提到的所有方法都会过滤出确切应该运行的内容,加载哪些bean,并将它们注入到彼此之间。
通常,这些限制是应用于“层”而不是单个bean(层=剩余层、数据层等)。
第二种和第三种方法实际上是相同的,它们是保留仅必要bean的不同方式来“过滤”应用程序上下文。
更新:
由于您已经完成了方法的性能比较:
单元测试=非常快速的测试,其目的是验证您编写的代码(或您的同事之一) 因此,如果您运行Spring,这意味着相对较慢的测试。所以回答您的问题
是否可以使用@ContextConfiguration作为“单元测试”
不可以,它是一个仅在Spring中运行一个类的集成测试。
通常,我们不仅使用Spring Framework运行一个类。如果您只想测试一个类(一个单元),在Spring容器中运行它有什么好处?是的,在某些情况下可能是几个类,但不是十个或几百个。
如果您使用Spring运行一个类,那么无论如何,您都必须模拟其所有依赖项,这也可以使用mockito完成...
现在关于您的问题
@ContextConfiguration与@SpringBootTest的技术差异。
只有在使用Spring Boot应用程序时,@SpringBootTest才是相关的。该框架在底层使用Spring,但简而言之,它提供了许多预定义的编写应用程序"基础设施"的方法/实践:
- 配置管理 - 包结构 - 可插拔性 - 日志记录 - 数据库集成等等
因此,Spring Boot建立了明确定义的处理所有上述项目的过程,如果您想要启动模拟Spring Boot应用程序的测试,那么您可以使用@SpringBootTest注解。否则(或者如果您只有一个Spring驱动的应用程序而不是Spring Boot),根本不要使用它。
然而,@ContextConfiguration是完全不同的。它只是指定在Spring驱动的应用程序中要使用的bean(它也适用于Spring Boot)。
"单元测试"是使用@ContextConfiguration的正确方式吗?还是不是?
就像我说的那样,所有与春季测试相关的东西都只用于集成测试,所以不,这是在单元测试中使用的错误方式。对于单元测试,使用一些完全不使用春季框架的东西(比如mockito用于模拟和一个普通的junit测试,不使用春季运行器)。

3
对于使用Spring上下文的单元测试和集成测试之间的比较,这是最好的答案之一。 - SGB
如果我们需要测试控制器和HTTP响应代码会发生什么?这个答案没有意义。 - RamPrakash
@RamPrakash。原始问题根本不涉及控制器和HTTP... 对于那些考虑使用Spring提供的MockMVC,但再次强调这与原始问题毫无关系。 - Mark Bramnik

14
@MarkBramnik的答案是我在Spring测试方面读过的最简单的答案。根据Spring官方文档,你的测试分为两类:
  • 单元测试:不需要加载Spring上下文。此时,您不需要任何Spring TestContext Framework注解。您可以使用JUnitTestNGMockito等等...

应用程序中的POJO应该可以在JUnit或TestNG测试中进行测试,使用new运算符实例化对象,而不是Spring或任何其他容器。您可以使用模拟对象(与其他有价值的测试技术结合使用)以隔离方式测试代码

  • 集成测试:需要您加载部分或全部上下文,因此必须使用所有所需的Spring注解

然而,对于某些单元测试场景,Spring框架提供了模拟对象和测试支持类,这些都在本章中进行了描述

从Spring 文档中:

依赖注入应使您的代码对容器的依赖性较低,与传统的Java EE开发相比如此。 应用程序中的POJO应该可以在JUnit或TestNG测试中进行测试,使用new运算符实例化对象,而不是Spring或任何其他容器。您可以使用模拟对象(与其他有价值的测试技术结合使用)以隔离方式测试代码。如果遵循Spring的体系结构建议,则代码库的清洁层和组件化会使单元测试更容易。例如,您可以通过存根或模拟DAO或存储库接口来测试服务层对象,而无需在运行单元测试时访问持久数据。

真正的单元测试通常运行非常快,因为没有需要设置的运行时基础设施。将真正的单元测试作为开发方法论的一部分强调可以提高生产力。您可能不需要本测试章节来帮助编写IoC-based应用程序的有效单元测试。然而,对于某些单元测试场景,Spring框架提供了模拟对象和测试支持类,这些都在本章中进行了描述。

如果您的应用程序基于Spring推荐的架构(Repository > Service > Controller >等等...),则应遵循以下规则(在我看来):

  • 测试您的仓库:使用@DataJpaTest注释进行测试。
  • 测试您的服务层:使用JUnitMockito。在这里,您将模拟您的存储库。
  • 测试您的控制器层:使用@WebMvcTest注释进行测试片段,或者使用JUnitMockito。在两种情况下,您都将模拟您的服务。
  • 测试组件,例如第三方库包装器或加载一些特定的bean:使用@ExtendWith(SpringExtension.class)@ContextConfiguration/@Import@SpringJUnitWebConfig,它是两者的结合。
  • 测试与LDAP或任何外部API等的集成:使用@DataLdapTest测试片段或相关注释,或者只需使用WireMock或任何模拟工具进行模拟。
  • 进行集成测试:使用 @SpringBootTest

很好的总结,我不知道@SpringJUnitWebConfig。我看到还有@SpringJUnitConfig,你可能想提到后者。 - mat3e
@mat3e,你可以在spring.io文档中找到更多细节:https://spring.io/projects/spring-framework - Harry Coder

6

就像@MarkBramnik所说的,如果你想编写单元测试,你必须模拟使用特定测试组件的其他组件。 如果您想编写模拟应用程序过程的集成测试,则建议使用@SpringBootTest。 当您在单元测试中使用@Autowired组件并且必须设置该类或创建bean的类的配置时,需要使用@ContextConfiguration


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接