[翻譯]Spring Boot 中的測試

原文地址:https://www.baeldung.com/spring-boot-testing

1 概覽

在這個教程中,我們會帶你看看如果使用 Spring Boot 中的框架編寫測試用例。內容會覆蓋單元測試,也會有在執行測試用例前會啓動 Spring 上下文的集成測試。如果你是使用 Spring Boot 的新手,查看鏈接:Spring Boot 介紹。

擴展閱讀:探索 Spring Boot TestRestTemplate、Spring Boot @RestClientTest快速導航、在Spring Beans中注入 Mockito Mocks

2 項目啓動

我們要使用的應用程序是一個api,這個api會提供一些關於Employee表的基本操作(增刪改查)。這是一個典型的分層框架——API調用從controller層到service層,最後到持久層。

3 Maven 依賴

首先增加測試依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <version>2.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

spring-boot-starter-test包是包含測試所需要的大部分元素的主要依賴項。H2數據庫是一個內存數據庫。它不需要我們配置和啓動一個真正的數據庫,因此在測試場景下方便了開發人員。

3.1 JUnit4

Spring Boot 2.4 中,JUnit 5’s vintage engine 包已經從spring-boot-starter-test中被移除了。如果我們想用  JUnit4 寫測試用例,我們需要添加下述依賴項。

<dependency>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

4 通過 @SpringBootTest 進行集成測試

就像標題所說,集成測試焦點是整合應用程序的不同層(controller層、service層以及持久層)。這也意味着沒有 mocking 參與其中。

理想情況下,我們應該把單元測試和集成測試分開,並且不應該和單元測試一起運行。我們可以通過使用不同的配置文件來實現這個分離。爲什麼要這麼做呢?因爲一般集成測試比較消耗時間並且有可能需要真正的數據庫(不是內存數據庫)來執行。

然而在本文中,我們不關注這個,我們關注的是,使用內存數據庫H2持久化存儲。

集成測試需要啓動一個容器來執行測試用例。因此需要一些額外的設置——這些在 Spring Boot 中都很容易。

@RunWith(SpringRunner.class)
@SpringBootTest(
  SpringBootTest.WebEnvironment.MOCK,
  classes 
= Application.class)
@AutoConfigureMockMvc
@TestPropertySource(
  locations 
"classpath:application-integrationtest.properties")
public class EmployeeRestControllerIntegrationTest {

    @Autowired
    private MockMvc mvc;

    @Autowired
    private EmployeeRepository repository;

    // write test cases here
}

當我們需要啓動整個容器時,@SpringBootTest註解是很有用的。這個註解會創建測試用例中需要的應用上下文(ApplicationContext)。

我們可以@SpringBootTest註解的webEnvironment屬性來配置運行時環境;我們可以在這裏使用WebEnvironment.MOCK,這樣整個容器會以模擬servlet 環境來運行。

然後,@TestPropertySource註解幫助我們配置在測試用例中使用的配置文件地址。需要注意的是,這個註解配置的配置文件會覆蓋存在的application.properties配置文件。

application-integrationtest.properties該配置文件包含持久層存儲的配置細節:

spring.datasource.url = jdbc:h2:mem:test
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.H2Dialect

如果我們想使用MySQL來進行集成測試,我們可以修改上述配置文件(application-integrationtest.properties)的值。集成測試的測試用例看起來像Controller層的單元測試。

@Test
public void givenEmployees_whenGetEmployees_thenStatus200()
  throws Exception 
{

    createTestEmployee("bob");

    mvc.perform(get("/api/employees")
      .contentType(MediaType.APPLICATION_JSON))
      .andExpect(status().isOk())
      .andExpect(content()
      .contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
      .andExpect(jsonPath("$[0].name", is("bob")));
}

區別是Controller層測試用例中,沒有東西是模擬的,並且是執行端到端場景。

5 通過@TestConfiguration進行測試配置

在前文中我們看到,增加了註解@SpringBootTest的類會啓動整個應用上下文,這也意味着我們可以通過@Autowire注入任何通過component掃描的類到我們的測試類中:

@RunWith(SpringRunner.class)
@SpringBootTest
public class EmployeeServiceImplIntegrationTest 
{

    @Autowired
    private EmployeeService employeeService;

    // class code ...
}

然而,我們也許想要避免啓動整個應用程序,而只是啓動一個特殊的測試配置。我們可以通過@TestConfiguration註解實現它。使用這個註解的方式有兩種。一種方式是,我們可以在內部類的地方使用該註解來注入我們想要通過@Autowire注入的類。

@RunWith(SpringRunner.class)
public class EmployeeServiceImplIntegrationTest 
{

    @TestConfiguration
    static class EmployeeServiceImplTestContextConfiguration {
        @Bean
        public EmployeeService employeeService() {
            return new EmployeeService() {
                // implement methods
            };
        }
    }

    @Autowired
    private EmployeeService employeeService;
}

另一種方式是,我們可以創建分開的測試配置類,而不是內部類:

@TestConfiguration
public class EmployeeServiceImplTestContextConfiguration {
    
    @Bean
    public EmployeeService employeeService() {
        return new EmployeeService() { 
            // implement methods 
        };
    }
}

@TestConfiguration註解的配置類會被componet掃描排除在外,因此我們需要在所有我們想要使用@Autowired的測試類中清晰的導入該類。我們可以通過@Import註解來實現:

@RunWith(SpringRunner.class)
@Import(EmployeeServiceImplTestContextConfiguration.class)
public class EmployeeServiceImplIntegrationTest 
{

    @Autowired
    private EmployeeService employeeService;

    // remaining class code
}

6 通過 @MockBean 模擬

Service 層代碼是依賴於持久層代碼的:

@Service
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;

    @Override
    public Employee getEmployeeByName(String name) {
        return employeeRepository.findByName(name);
    }
}

然後,在測試Service層的時候,我們並不需要或者關心持久層是怎麼實現的。理想情況下,我們應該可以在沒有連接完整持久層代碼的情況下,編寫和測試Service層代碼。

爲了實現這樣的解耦,==我們可以使用 Spring Boot Test 提供的 Mocking 支持來做到==。

讓我們瞟一眼測試類的框架先:

@RunWith(SpringRunner.class)
public class EmployeeServiceImplIntegrationTest 
{

    @TestConfiguration
    static class EmployeeServiceImplTestContextConfiguration {
 
        @Bean
        public EmployeeService employeeService() {
            return new EmployeeServiceImpl();
        }
    }

    @Autowired
    private EmployeeService employeeService;

    @MockBean
    private EmployeeRepository employeeRepository;

    // write test cases here
}

爲了檢查該Service類,我們需要有個一已經創建好並且可以通過 @Bean 可獲得的Service類實例,這樣我們纔可以通過@Autowired在測試類中注入該Service類。我們可以通過@TestConfiguration註解來實現。

這裏另一個有趣的事情是使用@MockBean。它會創建一個EmployeeRepository模擬類,它可以被用來替換真正的EmployeeRepository.

@Before
public void setUp() {
    Employee alex = new Employee("alex");

    Mockito.when(employeeRepository.findByName(alex.getName()))
      .thenReturn(alex);
}

啓動完成之後,測試用例就簡單了:

@Test
public void whenValidName_thenEmployeeShouldBeFound() {
    String name = "alex";
    Employee found = employeeService.getEmployeeByName(name);
 
     assertThat(found.getName())
      .isEqualTo(name);
 }

7 通過@DataJpaTest註解集成測試

我們將使用Employee實體,它有兩個屬性:id和name:

@Entity
@Table(name = "person")
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Size(min = 3, max = 20)
    private String name;

    // standard getters and setters, constructors
}

這是使用 Spring Data JPA的持久層類:

@Repository
public interface EmployeeRepository extends JpaRepository<EmployeeLong{

    public Employee findByName(String name);

}

這是持久層代碼。現在讓我們繼續往下編寫測試代碼。首先,我們創建測試類的基本框架:

@RunWith(SpringRunner.class)
@DataJpaTest
public class EmployeeRepositoryIntegrationTest 
{

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private EmployeeRepository employeeRepository;

    // write test cases here

}

@RunWith(SpringRunner.class)註解提供一個Spring Boot Test 特性和JUnit中間的一個橋樑。當我們需要在JUnit測試類中使用Spring Boot 測試的特性的時候,這個註解就有用了。

@DataJpaTest 註解提供了持久層測試類的一些標準設置:

  • 配置H2數據庫,一個內存數據庫
  • 設置Hibernate,SPring Data,和DataSource
  • 執行@EntityScan
  • 打開SQL日誌記錄

爲了繼續數據庫操作,我們需要在數據庫中添加一些記錄。爲了設置這些數據,我們可以使用TestEntityManager

Spring Boot TestEntityManager 是標準JPA EntityManager的替代方案,標準JPA EntityManager提供了編寫測試時常用的方法。

EmployeeRepository是我們要進行測試的組件。現在我們編寫我們第一個測試用例;

@Test
public void whenFindByName_thenReturnEmployee() {
    // given
    Employee alex = new Employee("alex");
    entityManager.persist(alex);
    entityManager.flush();

    // when
    Employee found = employeeRepository.findByName(alex.getName());

    // then
    assertThat(found.getName())
      .isEqualTo(alex.getName());
}

在上述測試用例中,我們通過TestEntityManager往數據庫中插入一條Employee記錄,然後就通過命名API讀取這條記錄。assertThat來自於Assertj庫,它與Spring Boot捆綁在一起。

8 通過@WebMvcTest進行單元測試

Controller層依賴Service層;簡單起見,我們添加一個簡單的方法:

@RestController
@RequestMapping("/api")
public class EmployeeRestController {

    @Autowired
    private EmployeeService employeeService;

    @GetMapping("/employees")
    public List<Employee> getAllEmployees() {
        return employeeService.getAllEmployees();
    }
}

由於我們只關注Controller層代碼,自然地,我們可以在單元測試中模擬Service層:

@RunWith(SpringRunner.class)
@WebMvcTest(EmployeeRestController.class)
public class EmployeeRestControllerIntegrationTest 
{

    @Autowired
    private MockMvc mvc;

    @MockBean
    private EmployeeService service;

    // write test cases here
}

要測試Controller層,我們可以使用在大部分情況下,@WebMvcTest只會啓動單個Controller類。我們可以和@MockBean註解一起使用來提供任何需要依賴的模擬實現。。它將爲我們的單元測試自動配置Spring MVC基礎結構。

在大部分情況下,@WebMvcTest只會啓動單個Controller類。我們可以和@MockBean註解一起使用來提供任何需要依賴的模擬實現。

@WebMvcTest會自動配置MockMvc,它提供了一種強力的方式來簡化測試MVC controller層的方式,而不需要啓動一個完整的 HTTP 服務器。

測試類如下:

@Test
public void givenEmployees_whenGetEmployees_thenReturnJsonArray()
  throws Exception 
{
    
    Employee alex = new Employee("alex");

    List<Employee> allEmployees = Arrays.asList(alex);

    given(service.getAllEmployees()).willReturn(allEmployees);

    mvc.perform(get("/api/employees")
      .contentType(MediaType.APPLICATION_JSON))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$", hasSize(1)))
      .andExpect(jsonPath("$[0].name", is(alex.getName())));
}

get()方法調用可以被其他與HTTP相對應的方法替換,如put()、 post()等。請注意,我們還在請求中設置內容類型。MockMvc是很靈活的,我們可以用它創建任何請求。

9 自動配置測試

Spring Boot的自動配置註釋的一個驚人特性是,它有助於加載完整應用程序的某些部分和代碼庫的特定測試層。

除了上述提供的註解,這裏還有一些被廣泛使用的註解列表:

  • @WebFluxTest:我們可以使用 @WebFluxTest註解來測試Spring WebFlux控制器。它經常與 @MockBean一起使用,爲所需的依賴項提供模擬實現。
  • @JdbcTest:我們可以使用 @JdbcTest註釋來測試JPA應用程序,但它只用於只需要數據源的測試。該註釋配置一個內存內嵌入式數據庫和一個 JdbcTemplate
  • @JooqTest
  • @DataMongoTest

...

你可以讀到關於這些註解的更多文章,並繼續優化集成測試,優化Spring集成測試。

10 結論

在本文中,我們深入探討了在Spring Boot中進行測試,並展示了怎麼更有效的編寫測試用例。

所有本文的源碼都可以在這裏找到,github。源碼包含很多其他示例和不同的測試用例。

其他閱讀:【Guide to Testing With the Spring Boot Starter Test】https://rieckpil.de/guide-to-testing-with-spring-boot-starter-test/


本文分享自微信公衆號 - 哥妞(gh_d18ec82f19ea)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章