如何在Spring Boot中实现集成测试?


集成测试可以验证代码库中的整个调用路径,不幸的是,这种测试方法很难在Spring Boot应用程序中使用。本案例探索如何克服这种困难:

我们将使用一个简单的REST服务示例,它具有我们连接使用的单个SQL数据库依赖项spring-boot-starter-data-jpa,一个预定义的Spring组件包,可以通过JPA轻松访问SQL数据,以及h2一个用Java编写的免费SQL数据库。

案例源码这里

下面是它包含的组件,所有示例代码都是使用Java 10编写的:

SpringBootApplication
class SlalomiteApplication

@RestController
class SlalomiteController // depends on SlalomiteService

@Service
class SlalomiteService
// depends on SlalomiteRepository

interface SlalomiteRepository extends CrudRepository<Slalomite, Long>

问题
看看CrudRepository存储库,虽然代码非常简单,但是如何模拟Spring提供的组件并不明显。这使得很难验证依赖于此存储库类的应用程序逻辑是否正确。

import org.springframework.data.repository.CrudRepository;

public interface SlalomiteRepository extends CrudRepository<Slalomite, Long> {
}

使用Spring Boot @DataJpaTest和@SpringBootTest(webEnvironment = ...)注释,可以实现模拟数据库并为此应用程序编写集成测试。该集成测试看起来像这样:


@RunWith(SpringRunner.class)
@SpringBootTest(classes = SlalomiteApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DataJpaTest
public class SlalomiteIntegrationTestBroken {
    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    public void getSlalomites_ShouldReturnAdam() {
        var slalomite = new Slalomite("Adam", Date.from(Instant.now()));
        this.entityManager.persist(slalomite);

        var response = restTemplate.getForEntity(
"/api/v1/slalomites", String.class);

        assertTrue(response.getBody().contains(
"Adam"));
    }

}

通过结合Spring教程,Spring指南和许多博客中的想法,这看起来可能是正确的,但不幸的是,在运行它时会遇到以下异常:

java.lang.IllegalStateException: Failed to load ApplicationContext... Caused by: org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.

这里的问题是@DataJpaTest的。来自javadoc解释:
使用此批注将禁用完全自动配置,而只应用与JPA测试相关的配置。

问题发生原因:@DataJpaTest只启用运行持久层所需的组件并禁用所有其他组件。这包括Spring用于启动servlet容器的组件。由于@SpringBootTest尝试启动servlet容器,因此在无法找到启动servlet所需的bean时会产生上述异常。

解决方案
解决方案非常简单,实际上在同一个@DataJpaTestjavadoc中暗示过,即使它没有在Spring文档或在线的许多地方提到过:

如果您要加载完整的应用程序配置,且使用了嵌入式数据库,则应考虑将@SpringBootTest与@AutoConfigureTestDatabase结合使用而不是使用此注释。

使用@AutoConfigureTestDatabase 时,我们没有提供@DataJpaTest提供的许多便利,例如,前面的例子中使用的TestEntityManager是不可用的;我们也没有得到事务测试的好处,这意味着我们必须更加谨慎地清理自己。
但是,还是保留了关键优势 - 在运行集成测试时,应用程序将连接到Spring创建的内存数据库,而不是我们的实际数据库实例。这使我们能够从数据库中读取和写入,而无需担心预先存在的数据或担心创建会影响其他应用程序或用户的数据,从而使测试更安全地作为CI / CD管道的一部分运行并且更具可重复性。
由于我们无法访问TestEntityManager此处,因此我们需要直接编写SQL来设置和清理数据库,或者我们需要在实际调用控制器方法之前利用我们的SlalomiteRepository bean来执行写操作。
这里演示我们将使用第二个选项。虽然它不像直接编写SQL那样具有“黑盒子”的方法,但它更简单,更不易碎。新的、通过测试的代码如下:


@RunWith(SpringRunner.class)
@SpringBootTest(classes = SlalomiteApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase
public class SlalomiteIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private SlalomiteRepository repo;

    @After
    public void cleanup() {
        repo.deleteAll();
    }

    @Test
    public void getSlalomites_ShouldReturnAdam() {
        var slalomite = new Slalomite("Adam", Date.from(Instant.now()));
        repo.save(slalomite);

        var response = restTemplate.getForEntity(
"/api/v1/slalomites", String.class);

        assertTrue(response.getBody().contains(
"Adam"));
    }
}

请注意添加cleanup方法。如果在每次测试后未清除对数据的更改,则最终会出现意外故障,因为不满足给定测试的前提条件。在实践中,由于您的测试运行员选择您的单元测试,这些失败可能会随机结束。