Spring Boot中的單元和整合測試
瞭解如何在Spring Boot環境中編寫單元和整合測試,以及在本教程中為此提供便利的工具,本文還會提供一種工具來幫助我們寫單元和整合測試。
1 概述
在這篇文章中,我們將瞭解如何在Spring Boot環境中編寫測試單元和整合。您可以線上找到大量有關此主題的教程,但很難在一個頁面中找到所需的所有資訊。我經常注意到初級開發人員在單元和整合測試之間混淆,特別是在談到spring生態系統時,我會嘗試解釋說明在不同環境中使用的不同註釋的用法。
2. Unit vs. Integration tests
unit testing維基百科中的解釋:在計算機程式設計中,單元測試是一種軟體測試方法,通過該方法測試各個原始碼單元,一個或多個計算機程式模組的集合以及相關的控制資料,使用程式和操作程式,以確定它們是否適合使用。
Integration testing:整合測試(有時稱為整合和測試,縮寫為I&T)是軟體測試階段,其中各個軟體模組組合並作為一組進行測試。
簡單來說,當我們進行單元測試時,我們只測試一個程式碼單元,一次測試一個方法,排除對測試有影響的所有其他元件。
在另一方面的整合測試中,我們測試元件之間的整合。由於單元測試,我們知道元件的行為與所需的一樣,但我們不知道它們將如何完全工作。這是整合測試的責任。
3. Java Test Unit
所有Java開發人員都知道JUnitas是執行測試單元的主要框架。它提供了許多註釋來對期望做出斷言。
Hamcrest是另一個軟體測試框架。Hamcrest允許使用現有匹配器類檢查程式碼中的條件,還允許您定義自定義匹配器實現。要在JUnit中使用Hamcrest匹配器,您必須使用assertThat語句,後跟一個或多個匹配器。
在這裡,您可以看到使用兩個框架的簡單測試:
package edu.princeton.cs.algs4;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.everyItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;
public class AssertTests {
@Test
public void testAssertArrayEquals() {
byte[] expected = "trial".getBytes();
byte[] actual = "trial".getBytes();
assertArrayEquals("failure - byte arrays not same", expected, actual);
}
@Test
public void testAssertEquals() {
assertEquals("failure - strings are not equal", "text", "text");
}
@Test
public void testAssertFalse() {
assertFalse("failure - should be false", false);
}
@Test
public void testAssertNotNull() {
assertNotNull("should not be null", new Object());
}
@Test
public void testAssertNotSame() {
assertNotSame("should not be same Object", new Object(), new Object());
}
@Test
public void testAssertNull() {
assertNull("should be null", null);
}
@Test
public void testAssertSame() {
Integer aNumber = Integer.valueOf(768);
assertSame("should be same", aNumber, aNumber);
} // JUnit Matchers assertThat
@Test
public void testAssertThatBothContainsString() {
assertThat("albumen", both(containsString("a")).and(containsString("b")));
}
@Test
public void testAssertThatHasItems() {
assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
}
@Test
public void testAssertThatEveryItemContainsString() {
assertThat(Arrays.asList(new String[]{"fun", "ban", "net"}), everyItem(containsString("n")));
} // Core Hamcrest Matchers with assertThat
@Test
public void testAssertThatHamcrestCoreMatchers() {
assertThat("good", allOf(equalTo("good"), startsWith("good")));
assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
assertThat(7, not(CombinableMatcher.either(equalTo(3)).or(equalTo(4))));
assertThat(new Object(), not(sameInstance(new Object())));
}
@Test
public void testAssertTrue() {
assertTrue("failure - should be true", true);
}
}
4. Introducing Our Example
讓我們編寫簡單的應用程式。這個想法是為漫畫提供一個基本的搜尋引擎。
4.1. Maven Dependencies
首先,我們必須給我們的專案新增一些依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
<scope>provided</scope>
</dependency>
4.2. Define the Model
我們的模型非常簡單;它只由兩個類組成:Manga和MangaResult。
4.2.1. Manga Class
Manga類表示由系統檢索的漫畫例項。我使用Lombok來減少樣板程式碼。
package com.mgiglione.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class Manga {
private String title;
private String description;
private Integer volumes;
private Double score;
}
4.2.2. MangaResult
MangaResult是一個包裝類,包含一個漫畫列表。
package com.mgiglione.model;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter @Setter @NoArgsConstructor
public class MangaResult {
private List<Manga> result;
}
4.3. Implementing the Service
為了實現該服務,我們將使用Jikan Moe自由公開的API。
RestTemplate是我用來對API進行REST呼叫的Spring類。
package com.mgiglione.service;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.mgiglione.model.Manga;
import com.mgiglione.model.MangaResult;
@Service
public class MangaService {
Logger logger = LoggerFactory.getLogger(MangaService.class);
private static final String MANGA_SEARCH_URL="http://api.jikan.moe/search/manga/";
@Autowired
RestTemplate restTemplate;
public List<Manga> getMangasByTitle(String title) {
return restTemplate.getForEntity(MANGA_SEARCH_URL+title, MangaResult.class).getBody().getResult();
}
}
4.4. Implementing the Controller
列表的下一步是寫下暴露兩個端點的REST Controller,一個是同步的,一個是非同步的,僅用於測試目的。該控制器使用上面定義的Service。
package com.mgiglione.controller;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;
@RestController
@RequestMapping(value = "/manga")
public class MangaController {
Logger logger = LoggerFactory.getLogger(MangaController.class);
@Autowired
private MangaService mangaService;
@RequestMapping(value = "/async/{title}", method = RequestMethod.GET)
@Async
public CompletableFuture<List<Manga>> searchASync(@PathVariable(name = "title") String title) {
return CompletableFuture.completedFuture(mangaService.getMangasByTitle(title));
}
@RequestMapping(value = "/sync/{title}", method = RequestMethod.GET)
public @ResponseBody <List<Manga>> searchSync(@PathVariable(name = "title") String title) {
return mangaService.getMangasByTitle(title);
}
}
4.5. Launching and Testing the System
mvn spring-boot:run
Then let’s try it:
Example of output:
{
"title":"Rurouni Kenshin: Meiji Kenkaku Romantan",
"description":"Ten years have passed since the end of Bakumatsu, an era of war that saw the uprising of citizens against the Tokugawa shogunate. The revolutionaries wanted to create a time of peace, and a thriving c...",
"volumes":28,
"score":8.69
},
{
"title":"Sun-Ken Rock",
"description":"The story revolves around Ken, a man from an upper-class family that was orphaned young due to his family's involvement with the Yakuza; he became a high school delinquent known for fighting. The only...",
"volumes":25,
"score":8.12
},
{
"title":"Yumekui Kenbun",
"description":"For those who suffer nightmares, help awaits at the Ginseikan Tea House, where patrons can order much more than just Darjeeling. Hiruko is a special kind of a private investigator. He's a dream eater....",
"volumes":9,
"score":7.97
}
5. Unit Testing the Spring Boot Application
Spring boot提供一個很棒的類是測試變得簡單: @SpringBootTest annotation
可以在執行基於Spring Boot的測試的測試類上指定此批註。除常規Spring TestContext框架之外,還提供以下功能:
- 當沒有定義特定的@ContextConfiguration(loader = …)時,使用SpringBootContextLoader作為預設的ContextLoader。
- 在未使用巢狀的@Configuration和未指定顯式類時自動搜尋@SpringBootConfiguration。
- 允許使用properties屬性定義自定義環境屬性。
- 支援不同的web環境模型,包括啟動在定義或隨機埠上偵聽的完全執行的Web伺服器的功能。
- 註冊TestRestTemplate和/或WebTestClient bean,以便在使用完全執行的Web伺服器的Web測試中使用。
我們在這裡測試基本上有兩個元件:MangaService和MangaController
5.1. Unit Testing MangaService
為了測試MangaService,我們需要將其與外部元件隔離。在本例子,我們只需要一個外部元件:RestTemplate,我們用它來呼叫遠端API。
我們需要做的是模擬RestTemplate bean並讓它始終以固定的給定響應進行響應。 Spring Test結合並擴充套件了Mockito庫,通過@MockBean註釋配置模擬bean。
package com.mgiglione.service.test.unit;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import com.mgiglione.model.Manga;
import com.mgiglione.model.MangaResult;
import com.mgiglione.service.MangaService;
import com.mgiglione.utils.JsonUtils;
@RunWith(SpringRunner.class)
@SpringBootTest
public class MangaServiceUnitTest {
@Autowired
private MangaService mangaService;
// MockBean is the annotation provided by Spring that wraps mockito one
// Annotation that can be used to add mocks to a Spring ApplicationContext.
// If any existing single bean of the same type defined in the context will be replaced by the mock, if no existing bean is defined a new one will be added.
@MockBean
private RestTemplate template;
@Test
public void testGetMangasByTitle() throws IOException {
// Parsing mock file
MangaResult mRs = JsonUtils.jsonFile2Object("ken.json", MangaResult.class);
// Mocking remote service
when(template.getForEntity(any(String.class), any(Class.class))).thenReturn(new ResponseEntity(mRs, HttpStatus.OK));
// I search for goku but system will use mocked response containing only ken, so I can check that mock is used.
List<Manga> mangasByTitle = mangaService.getMangasByTitle("goku");
assertThat(mangasByTitle).isNotNull()
.isNotEmpty()
.allMatch(p -> p.getTitle()
.toLowerCase()
.contains("ken"));
}
}
5.2. Unit Testing MangaController
正如在服務的單元測試中所做的那樣,我們需要隔離元件。在這種情況下,我們需要模擬MangaService bean。
然後,我們還有一個問題……Controller部分是管理HttpRequest的系統的一部分,因此我們需要一個系統來模擬這種行為,而無需啟動完整的HTTP伺服器。
MockMvc就是這樣做的Spring類。可以用不同的方式設定它:
- 使用 Standalone Context
- 使用 WebApplication Context
- spring通過在測試類上使用@SpringBootTest @AutoConfigureMockMvc註解自動載入所有上下文來自動配置它
- 讓Spring通過在測試類@WebMvcTest上使用這些註釋載入Web層上下文來自動配置它
package com.mgiglione.service.test.unit;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.context.WebApplicationContext;
import com.mgiglione.controller.MangaController;
import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;
@SpringBootTest
@RunWith(SpringRunner.class)
public class MangaControllerUnitTest {
MockMvc mockMvc;
@Autowired
protected WebApplicationContext wac;
@Autowired
MangaController mangaController;
@MockBean
MangaService mangaService;
/**
* List of samples mangas
*/
private List<Manga> mangas;
@Before
public void setup() throws Exception {
this.mockMvc = standaloneSetup(this.mangaController).build();// Standalone context
// mockMvc = MockMvcBuilders.webAppContextSetup(wac)
// .build();
Manga manga1 = Manga.builder()
.title("Hokuto no ken")
.description("The year is 199X. The Earth has been devastated by nuclear war...")
.build();
Manga manga2 = Manga.builder()
.title("Yumekui Kenbun")
.description("For those who suffer nightmares, help awaits at the Ginseikan Tea House, where patrons can order much more than just Darjeeling. Hiruko is a special kind of a private investigator. He's a dream eater....")
.build();
mangas = new ArrayList<>();
mangas.add(manga1);
mangas.add(manga2);
}
@Test
public void testSearchSync() throws Exception {
// Mocking service
when(mangaService.getMangasByTitle(any(String.class))).thenReturn(mangas);
mockMvc.perform(get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title", is("Hokuto no ken")))
.andExpect(jsonPath("$[1].title", is("Yumekui Kenbun")));
}
@Test
public void testSearchASync() throws Exception {
// Mocking service
when(mangaService.getMangasByTitle(any(String.class))).thenReturn(mangas);
MvcResult result = mockMvc.perform(get("/manga/async/ken").contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(request().asyncStarted())
.andDo(print())
// .andExpect(status().is2xxSuccessful()).andReturn();
.andReturn();
// result.getRequest().getAsyncContext().setTimeout(10000);
mockMvc.perform(asyncDispatch(result))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title", is("Hokuto no ken")));
}
}
正如程式碼中所見,我選擇第一個方法的原因是它是最輕的一個,而且對Spring上下文中載入的內容有最好的管理。
在非同步測試中,我必須通過首先呼叫服務然後啟動asyncDispatch方法來模擬非同步行為。
6. Integration Testing the Spring Boot Application
對於整合測試,我們希望通過下游通訊檢查我們的主要元件。
6.1. Integration Testing of MangaService
這個測試非常簡單。我們不需要模擬任何東西因為我們只需要呼叫遠端的 mangas API.
package com.mgiglione.service.test.integration;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;
@RunWith(SpringRunner.class)
@SpringBootTest
public class MangaServiceIntegrationTest {
@Autowired
private MangaService mangaService;
@Test
public void testGetMangasByTitle() {
List<Manga> mangasByTitle = mangaService.getMangasByTitle("ken");
assertThat(mangasByTitle).isNotNull().isNotEmpty();
}
}
6.2. Integration Testing of MangaController
測試與單元測試非常相似,但在這種情況下,我們不需要沒有模擬服務。
package com.mgiglione.service.test.integration;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.context.WebApplicationContext;
import com.mgiglione.controller.MangaController;
@SpringBootTest
@RunWith(SpringRunner.class)
public class MangaControllerIntegrationTest {
// @Autowired
MockMvc mockMvc;
@Autowired
protected WebApplicationContext wac;
@Autowired
MangaController mangaController;
@Before
public void setup() throws Exception {
this.mockMvc = standaloneSetup(this.mangaController).build();// Standalone context
// mockMvc = MockMvcBuilders.webAppContextSetup(wac)
// .build();
}
@Test
public void testSearchSync() throws Exception {
mockMvc.perform(get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken"))));
}
@Test
public void testSearchASync() throws Exception {
MvcResult result = mockMvc.perform(get("/manga/async/ken").contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(request().asyncStarted())
.andDo(print())
.andReturn();
mockMvc.perform(asyncDispatch(result))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken"))));
}
}
7 結束語
我們已經看到了Spring Boot環境中單元測試和整合測試之間的主要區別,瞭解像Hamcrest這樣簡化測試編寫的框架。當然,您可以在我的GitHub儲存庫中找到所有內容。