如何在Spring Boot中實現整合測試?

banq發表於2018-12-19

整合測試可以驗證程式碼庫中的整個呼叫路徑,不幸的是,這種測試方法很難在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方法。如果在每次測試後未清除對資料的更改,則最終會出現意外故障,因為不滿足給定測試的前提條件。在實踐中,由於您的測試執行員選擇您的單元測試,這些失敗可能會隨機結束。


 

相關文章