基於spring-boot的應用程式的單元+整合測試方案
概述
本文主要介紹單元測試、整合測試相關的概念、技術實現以及最佳實踐。
本文的demo是基於Java語言,Spring Boot構建的web應用。測試框架使用Junit,模擬框架使用mockito。
之前曾經總結過一篇文章: ofollow,noindex" target="_blank">基於spring-boot的應用程式的單元測試方案 ,但是當時只是從技術實現的角度去研究單元測試,很多概念沒有搞清楚。本文在重新梳理脈絡,豐富概念的基礎上,整合了前文的大部分內容,但是有一部分幾乎在實踐中用不到的內容就被刪去了。
在我的個人wiki站點,可以獲得更好的閱讀體驗喔: 基於spring-boot的應用程式的單元+整合測試方案
概念解析
單元測試和整合測試
測試領域有很多場景,比如單元測試,整合測試,系統測試,冒煙測試,迴歸測試,端到端測試,功能測試等。測試的分類方式各有不同,一些測試場景也可能存在重疊。具體這些場景的概念和區別,大家可以閱讀文末給出的參考資料。
這裡主要以程式設計師的視角談一下我理解的單元測試和整合測試。
單元測試是編寫單元測試類,針對類級別的測試。比如使用Junit框架,針對一個類,寫一個測試類,測試目標類的大部分主要方法。
需要注意單元測試的級別是類。專案當中,類之間的依賴呼叫是很常見的事,如果你要測試一個類,而這個目標類又呼叫了另一個類,那麼在測試時就沒有遵守“在一個類範圍內進行測試”,自然算不得單元測試。
如上圖所示,假設A,B,C,D四個類之間存在上述的依賴關係,我們要測試類A,那麼如何遵守“在類A的範圍內測試”?
這就是模擬框架要解決的問題了,通過模擬B和C,我們可以在測試A的時候,呼叫B和C的模擬物件,而不是實際的B和C。下文會有詳細介紹。
如果在測試時超脫一個類的範圍,那就可以稱為整合測試。如上圖所示,你可以測試類A,它會直接或間接呼叫其他三個類,這就可以叫做整合測試。如果你去測試類C,因為它會呼叫D,也可以稱為整合測試。
如果純粹按照單元測試的概念,把這個工作代入到一個大型的專案,成百上千的類需要編寫測試類,而且類之間的依賴需要編寫模擬程式碼。這樣的工作太過龐大,對專案來說應該是得不償失的。
我推薦的做法是識別核心程式碼,或者說是重要的程式碼,只對這些程式碼做精細的單元測試。除此之外,都通過整合測試來覆蓋。整合測試時優先從最頂層開始,讓測試自然流下來。然後根據程式碼測試覆蓋報告,再進行補刀。
Mock和Stub
此處介紹的mock和stub,是作者基於mockito框架的理解,行業內對這兩個概念的定義和此處的理解可能有所出入。作者不追求對概念有“專業的定義”或者“精確的定義”,如果讀者有此追求,可另外查閱其他資料。
上文講到,在做單元測試的時候,需要遮蔽目標類的依賴,mock和stub就是這種操作涉及到的兩個概念。
在專案程式碼中,經常會涉及依賴多個外部資源的情況,比如資料庫、微服務中的其他服務。這表示在測試的時候需要先做很多準備工作,比如準備資料庫環境,比如先把依賴的服務run起來。
另外,還需要考慮消除測試的副作用,以使測試具備冪等性。比如如果測試會修改資料庫,那麼是否會影響二次測試的結果,或者影響整個測試環境?
對外部的資源依賴進行模擬,是一個有效的解決方案。即測試時不是真正的操作外部資源,而是通過自定義的程式碼進行模擬操作。我們可以對任何的依賴進行模擬,從而使測試的行為不需要任何準備工作或者不具備任何副作用。
在這個大環境下,可以解釋mock和stub的含義。當我們在測試時,如果只關心某個操作是否執行過,而不關心這個操作的具體行為,這種技術稱為mock。
比如我們測試的程式碼會執行傳送郵件的操作,我們對這個操作進行mock;測試的時候我們只關心是否呼叫了傳送郵件的操作,而不關心郵件是否確實傳送出去了。
另一種情況,當我們關心操作的具體行為,或者操作的返回結果的時候,我們通過執行預設的操作來代替目標操作,或者返回預設的結果作為目標操作的返回結果。這種對操作的模擬行為稱為stub(打樁)。
比如我們測試程式碼的異常處理機制是否正常,我們可以對某處程式碼進行stub,讓它丟擲異常。再比如我們測試的程式碼需要向資料庫插入一條資料,我們可以對插入資料的程式碼進行stub,讓它始終返回1,表示資料插入成功。
技術實現
單元測試
測試常規的bean
當我們進行單元測試的時候,我們希望在spring容器中只例項化測試目標類的例項。
假設我們的測試目標如下:
@Service public class CityService { @Autowired private CityMapper cityMapper; public List<City> getAllCities() { return cityMapper.selectAllCities(); } public void save(City city) { cityMapper.insert(city); } }
我們可以這樣編寫測試類:
@RunWith(SpringRunner.class) @SpringBootTest public class CityServiceUnitTest { @SpringBootApplication(scanBasePackages = "com.shouzheng.demo.web") static class InnerConfig { } @Autowired private CityService cityService; @MockBean private CityMapper cityMapper; @Test public void testInsert() { City city = new City(); cityMapper.insert(city); Mockito.verify(cityMapper).insert(city); } @Test public void getAllCities() { City city = new City(); city.setId(1L); city.setName("杭州"); city.setState("浙江"); city.setCountry("CN"); Mockito.when(cityMapper.selectAllCities()) .thenReturn(Collections.singletonList(city)); List<City> result = cityService.getAllCities(); Assertions.assertThat(result.size()).isEqualTo(1); Assertions.assertThat(result.get(0).getName()).isEqualTo("杭州"); } }
@RunWith
註解宣告測試是在spring環境下執行的,這樣就可以啟用Spring的相關支援。
@SpringBootTest
註解負責掃描配置來構建測試用的Spring上下文環境。它預設搜尋 @SpringBootConfiguration
類,除非我們通過classes屬性指定配置類,或者通過自定義內嵌的 @Configuration
類來指定配置。如上面的程式碼,就是通過內嵌類來自定義配置。
@SpringBootApplication
擴充套件自 @Configuration
,其scanBasePackages屬性指定了掃描的根路徑。確保測試目標類在這個路徑下,而且需要明白這個路徑下的所有bean都會被例項化。雖然我們已經儘可能的縮小了例項化的範圍,但是我們沒有避免其他無關類的例項化開銷。
即使如此,這種方案依然被我看作是最佳的實踐方案,因為它比較簡單。如果我們追求“只例項化目標類”,那麼可以使用下面的方式宣告內嵌類:
@Configuration @ComponentScan(value = "com.shouzheng.demo.web", useDefaultFilters = false, includeFilters = @ComponentScan.Filter( type = FilterType.REGEX, pattern = {"com.shouzheng.demo.web.CityService"}) ) static class InnerConfig { }
@ComponentScan
負責配置掃描Bean的方案,value屬性指定掃描的根路徑,useDefaultFilters屬性取消預設的過濾器,includeFilters屬性自定義了一個過濾器,這個過濾器設定為要掃描模式匹配的類。
@ComponentScan預設的過濾器會掃描@Component,@Repository,@Service,@Controller;如果不禁用預設過濾器,自定義過濾器的效果是在預設過濾器的基礎上追加更多的bean。即我們要限定只例項化某個特定的bean,就需要把預設的過濾器禁用。
可以看到,這種掃描策略配置,會顯得複雜很多。
@Autowired
負責注入依賴的bean,在這裡注入的是測試目標bean。
@MockBean
負責宣告這是一個模擬的bean。在進行單元測試時,需要將測試目標的所有依賴bean宣告為模擬的bean,這些模擬的bean將被注入測試目標bean。
在testInsert方法中,我們執行了 cityMapper.insert
,這只是模擬的執行了,實際上什麼也沒做。接下來我們呼叫 Mockito.verify
,目的是驗證 cityMapper.insert
執行了。這正對應了上文中對Mock概念的解釋,我們只關心它是否執行了。
需要注意的是,驗證的內容同時包括引數是否一致。如果實際呼叫時的傳參和驗證時指定的引數不一致,則驗證失敗,以至於測試失敗。
在getAllCities方法中,我們使用 Mockito.when
對 cityMapper.selectAllCities
方法進行打樁,設定當方法被呼叫時,直接返回我們預設的資料。這也對應了上文中對Stub概念的解釋。
注意:只能對mock物件進行stub。
測試Controller
Controller是一類特殊的bean,這類bean除了顯式的依賴,還有一些系統元件的依賴。比如訊息轉換元件,負責將方法的返回結果轉換成可以寫的HTTP訊息。所以,我們無法像測試上文那樣對其單獨例項化。
Spring提供了特定的註解,配置用於測試Controller的上下文環境。
例如我們要測試的controller如下:
@RestController public class CityController { @Autowired private CityService cityService; @GetMapping("/cities") public ResponseEntity<?> getAllCities() { List<City> cities = cityService.getAllCities(); return ResponseEntity.ok(cities); } @PostMapping("/city") public ResponseEntity<?> newCity(@RequestBody City city) { cityService.save(city); return ResponseEntity.ok(city); } }
我們可以這樣編寫測試類:
@RunWith(SpringRunner.class) @WebMvcTest(CityController.class) public class CityControllerUnitTest { @Autowired private MockMvc mvc; @MockBean private CityService service; @Test public void getAllCities() throws Exception { City city = new City(); city.setId(1L); city.setName("杭州"); city.setState("浙江"); city.setCountry("中國"); Mockito.when(service.getAllCities()). thenReturn(Collections.singletonList(city)); mvc.perform(MockMvcRequestBuilders.get("/cities")) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("杭州"))); } }
@WebMvcTest
是特定的註解,它的職責和 @SpringBootTest
相同,但它只會例項化Controller。預設例項化所有的Controller,也可以指定只例項化某一到多個Controller。
除此之外, @WebMvcTest
還會例項化一個MockMvc的bean,用於傳送http請求。
我們同樣需要對測試目標的依賴進行模擬,即,將CityService宣告為MockBean。
spring環境問題
@WebMvcTest
就像 @SpringBootTest
一樣,預設搜尋 @SpringBootConfiguration
註解的類作為配置類。一般情況下,基於Spring-Boot的web應用,會建立一個啟動類,並使用 @SpringBootApplication
,這個註解可看作 @SpringBootConfiguration
註解的擴充套件,所以很可能會搜尋到這個啟動類作為配置。
如果專案當中有多個 @SpringBootConfiguration
配置類,比如有些其他的測試類建立了內部配置類,並且使用了這個註解。如果當前測試類沒有使用內部類,也沒有使用classes屬性指定使用哪個配置類,就會因為找到了多個配置類而失敗。這種情況下會有明確的錯誤提示資訊。
思考當前測試類會使用哪一個配置類,是一個很好的習慣。
另外一個可能的問題是:如果配置類上添加了其他的註解,比如Mybatis框架的 @MapperScan
註解,那麼Spring會去嘗試例項化Mapper例項,但是因為我們使用的是 @WebMvcTest
註解,Spring不會去例項化Mapper所依賴的sqlSessionFactory等自動配置的元件,最終導致依賴註解失敗,無法構建Spring上下文環境。
也就是說,雖然 @WebMvcTest
預設只例項化Controller元件,但是它同樣也會遵從配置類的註解去做更多的工作。如果這些工作依賴於某些自動化配置bean,那麼將會出現依賴缺失。
解決這個問題的方法可能有很多種,我這邊提供一個自己的最佳實踐:
@RunWith(SpringRunner.class) @WebMvcTest(CityController.class) public class CityControllerWebLayer { @SpringBootApplication(scanBasePackages = {"com.shouzheng.demo.web"}) static class InnerConfig {} @Autowired private MockMvc mvc; @MockBean private CityService service; }
這個方案,是通過使用內部類來自定義配置。內部類只有一個 @SpringBootApplication
註解,指定了掃描的根路徑,以縮小bean的掃描範圍。
測試持久層
就像測試controller一樣,持久層的單元測試也有專門的註解支援。
持久層的技術有多種,Spring提供了 @JdbcTest
來支援通過spring的JdbcTemplate進行持久化的測試,提供了 @DataJpsTest
支援通過JPA技術進行持久化的測試。
上面的這兩個註解我沒有做過研究,因為專案中使用的是Mybatis,這裡僅介紹Mybatis提供的測試支援: @MybatisTest
。
最簡單的方式是使用記憶體資料庫作為測試資料庫,這樣可以儘量減少測試的環境依賴。
預設的持久層測試是回滾的,即每一個測試方法執行完成之後,會回滾對資料庫的修改;所以也可以使用外部的資料庫進行測試,但多少會有些影響(比如序列的當前值)。
使用記憶體資料庫
首先,新增資料庫依賴:
<!-- pom.xml --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>RELEASE</version> <scope>test</scope> </dependency>
準備資料庫初始化指令碼,比如放在resources/import.sql檔案中:
drop table if exists city; drop table if exists hotel; create table city (id int primary key AUTO_INCREMENT, name varchar, state varchar, country varchar); create table hotel (city int primary key AUTO_INCREMENT, name varchar, address varchar, zip varchar); insert into city (id, name, state, country) values (1, 'San Francisco', 'CA', 'US'); insert into hotel(city, name, address, zip) values (1, 'Conrad Treasury Place', 'William & George Streets', '4001')
需要在配置檔案中指定指令碼檔案的位置:
spring.datasource.schema=classpath:import.sql
例如我們要測試如下的Mapper介面:
@Mapper public interface CityMapper { City selectCityById(int id); List<City> selectAllCities(); int insert(City city); }
我們可以這樣編寫測試類:
@RunWith(SpringRunner.class) @MybatisTest @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class CityMapperUnitTest { @SpringBootApplication(scanBasePackages = {"com.shouzheng.demo.mapper"}) static class InnerConfig {} private static Logger LOG = LoggerFactory.getLogger(CityMapperUnitTest.class); @Autowired private CityMapper cityMapper; @Before @After public void printAllCities() { List<City> cities = cityMapper.selectAllCities(); LOG.info("{}", cities); } @Test //@Rollback(false) // 禁止回滾 public void test1_insert() throws Exception { City city = new City(); city.setName("杭州"); city.setState("浙江"); city.setCountry("CN"); cityMapper.insert(city); LOG.info("insert a city {}", city); } @Test public void test2_doNothing() { } }
@MybatisTest
搜尋配置類的邏輯和 @SpringBootTest
、 @WebMvcTest
相同,為了避免Spring環境問題(上文在測試Controller一節中介紹過),這裡直接使用內部類進行配置。
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
用來指定測試方法的執行順序,這是為了觀察事務回滾的效果。
如果將test1_insert方法上的 @Rollback(false)
註釋放開,事務不會回滾,test2_doNothing方法之後列印輸出的內容會包含test1_insert方法裡插入的資料。
反之,如果註釋掉,事務回滾,test2_doNothing方法之後列印輸出的內容不包含test1_insert方法裡插入的資料。
使用外部資料庫
首先,新增對應的資料庫驅動依賴,以及資料來源配置。比如使用mysql外部資料庫:
<!-- pom.xml --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-jdbc.version}</version> </dependency>
# application.yml spring: datasource: url: jdbc:mysql://localhost:3306/test?autoReconnect=true&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&useSSL=false username: root password: root driver-class-name: com.mysql.jdbc.Driver
然後配置測試類,唯一不同的是,在測試類上要多加一個 @AutoConfigureTestDatabase
註解:
@RunWith(SpringRunner.class) @MybatisTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public class CityMapperTest2 { @SpringBootApplication(scanBasePackages = {"com.shouzheng.demo.mapper"}) static class InnerConfig {} @Autowired private CityMapper cityMapper; // ... }
這樣,測試的時候就會使用我們配置的資料庫進行測試,而不是使用記憶體資料庫。
事務回滾設定
測試持久層時,預設是回滾的。可以在具體的測試方法上新增 @Rollback(false)
來禁止回滾,也可以在測試類上新增。
整合測試
整合測試時會超脫一個類的範圍,我們需要保證自測試目標類及以下的依賴類,都能夠在spring容器中被例項化,最簡單的方式莫過於構建完整的spring上下文。雖然這樣一來,會有很多和測試目標無關的類也會被例項化,但是我們省去了精心設計初始化bean的工夫,而且也間接的達到了“測試構建完整的spring上下文”的目的。
從Controller開始測試
例如我們以上文中介紹到的controller為測試目標,測試newCity請求。測試類如下:
@RunWith(SpringRunner.class) @SpringBootTest(classes = DemoTestSpringBootApplication.class) @AutoConfigureMockMvc @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class CityControllerWithRollbackTest { private static Logger LOG = LoggerFactory.getLogger(CityControllerWithRollbackTest.class); @Autowired private MockMvc mockMvc; @Before @After public void getAllCities() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/cities")) .andDo(result -> { String content = result.getResponse().getContentAsString(); LOG.info("cities = {}", content); }); } @Test @Transactional //@Rollback(false) public void test1_insertCity() throws Exception { LOG.info("insert a city"); mockMvc.perform(MockMvcRequestBuilders.post("/city") .contentType(MediaType.APPLICATION_JSON) .content("{\"name\": \"杭州\", \"state\": \"浙江\", \"country\": \"中國\"}")) .andExpect(MockMvcResultMatchers.status().isOk()); } /** * 為了觀察資料庫是否回滾 */ @Test public void test2_doNothind() { } }
這段程式碼主要測試新增資料記錄的請求,並在測試前後分別請求並列印當前的資料記錄集。我們可以看到,在test1_insertCity方法執行之後列印的資料集,會比在此之前列印的資料集多一條記錄,而這條記錄正是我們申請新增的資料記錄。
test2_doNothind是一個輔助的測試方法,在完成test1_insertCity方法之後,開始執行test2_doNothind測試。而測試前的列印資料記錄集的行為,可以讓我們觀察到test1_insertCity測試中新增的資料是否發生回滾。
整合測試時使用 @SpringBootTest
註解,指定配置類為專案啟動類。如果我們的專案是基於spring-cloud的微服務環境,那麼也可以使用內部配置類來減少服務註冊等相關的配置。
@AutoConfigureMockMvc
是為了例項化MockMvc例項,用來發送http請求。
事務回滾設定
實驗證明, 整合測試依然可以支援資料庫操作回滾 ,方案就是在測試方法上使用 @Transactional
註解,標識事務性操作。同時,我們依然可以使用 @Rollback
來設定是否回滾。
從中間層開始測試
整合測試不是非要從最頂層開始測試,我們也可以從service層開始測試:
@RunWith(SpringRunner.class) @SpringBootTest(classes = {DemoTestSpringBootApplication.class}) @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class CityServiceWithRollbackTest { private static Logger LOG = LoggerFactory.getLogger(CityServiceWithRollbackTest.class); @Autowired private CityService cityService; @Before @After public void printAllCities() { List<City> cities = cityService.getAllCities(); LOG.info("{}", cities); } @Test @Transactional public void test1_insert() { City city = new City(); city.setName("杭州"); city.setState("浙江"); city.setCountry("CN"); cityService.save(city); LOG.info("insert a new city {}", city); } @Test public void test2_doNothind() { } }
這段程式碼的測試方案和上文的controller整合測試方案相同,都是測試新增操作,並在測試前後列印當前資料集,來演示是否支援事務回滾。
Mock
在spring專案的測試類中,我們可以對任意的類進行mock,如下面這樣:
@RunWith(SpringRunner.class) @SpringBootTest public class CityServiceUnitTest { @MockBean private CityMapper cityMapper; ... }
定義一個field,對其新增 @MockBean
註解,就聲明瞭對應型別的一個mock bean。如果spring上下文中已經存在對應型別的bean,將會被mock bean覆蓋掉。
預設的情況下,mock bean的所有方法都是透明的:什麼也不做,直接返回對應型別的預設值。宣告返回引用型別的方法,將直接返回null;宣告返回基本型別的方法,直接返回相應的預設值;宣告無返回的方法,那更是透明的。
mock的作用對靜態方法無效,靜態方法會被實際呼叫。所以建議不要在靜態方法中進行資源相關的處理,否則將無法進行模擬測試。比如,使用靜態方法封裝資料庫操作的行為是不好的。
如上文所述,Mock的使用場景是我們只關注對應的方法是否執行了,而不關心實際的執行效果。實際程式碼中,我們可以按照下面的方式使用:
@Test @Transactional public void test1_insert() { City city = new City(); city.setName("杭州"); city.setState("浙江"); city.setCountry("CN"); cityService.save(city); Mockito.verify(cityMapper).insert(city); LOG.info("insert a new city {}", city); }
Mockito.verify
開始的一行,用來驗證作為mock bean的cityMapper的insert方法會被執行,而且引數為city。如果方法沒有被呼叫,或者實際呼叫時的傳參不一致,都會導致測試失敗。
比如,如果改成 Mockito.verify(cityMapper).insert(new City());
,將會丟擲下面的異常:
Argument(s) are different! Wanted: cityMapper bean.insert(null,null,null,null); -> at com.shouzheng.demo.web.CityServiceWithRollbackTest.test1_insert(CityServiceWithRollbackTest.java:56) Actual invocation has different arguments: cityMapper bean.insert(null,杭州,浙江,CN); -> at com.shouzheng.demo.web.CityService.save(CityService.java:26) Comparison Failure: Expected :cityMapper bean.insert(null,null,null,null); Actual:cityMapper bean.insert(null,杭州,浙江,CN);
Stub
在Mock的基礎上更進一步,如果我們關注方法的返回結果,或者我們希望方法能有預定的行為,使得測試按照我們預期的方向進行,那麼我們需要對mock bean的某些方法進行stub,讓這些方法在引數滿足某個條件的情況下,給我們預設的響應。
實際程式碼中,我們 只能對mock bean的方法進行stub ,否則得到下面的異常:
org.mockito.exceptions.misusing.MissingMethodInvocationException: when() requires an argument which has to be 'a method call on a mock'. For example: when(mock.getArticles()).thenReturn(articles); Also, this error might show up because: 1. you stub either of: final/private/equals()/hashCode() methods. Those methods *cannot* be stubbed/verified. Mocking methods declared on non-public parent classes is not supported. 2. inside when() you don't call method on mock but on some other object.
返回預設的結果
我們可以按照下面的方式,讓它返回預設的結果:
Mockito.when(cityMapper.selectAllCities()) .thenReturn(Collections.singletonList(city));
或者丟擲預設的異常(如果我們檢測異常處理程式碼的話):
Mockito.when(cityMapper.selectAllCities()) .thenThrow(new RuntimeException("test"));
或者去執行實際的方法:
when(mock.someMethod()).thenCallRealMethod();
注意,呼叫真實的方法有違mock的本義,應該儘量避免。如果要呼叫的方法中呼叫了其他的依賴,需要自行注入其他的依賴,否則會空指標。
執行預設的操作
如果我們希望它能夠執行預設的操作,比如列印我們傳入的引數,或者修改我們傳入的引數,我們可以按照下面的方式實現:
Mockito.when(cityMapper.insert(Mockito.any())) .then(invocation -> { LOG.info("arguments are {}", invocation.getArguments()); return 1; });
引數匹配
我們可以指定明確的引數匹配條件,或者使用模式匹配:
@RunWith(SpringRunner.class) @SpringBootTest public class MathServiceTest { @Configuration static class ConfigTest {} @MockBean private MathService mathService; @Test public void testDivide() { Mockito.when(mathService.divide(4, 2)) .thenReturn(2); Mockito.when(mathService.divide(8, 2)) .thenReturn(4); Mockito.when(mathService.divide(Mockito.anyInt(), Mockito.eq(0))) // 必須同時用模式 .thenThrow(new RuntimeException("error")); Assertions.assertThat(mathService.divide(4, 2)) .isEqualTo(2); Assertions.assertThat(mathService.divide(8, 2)) .isEqualTo(4); Assertions.assertThatExceptionOfType(RuntimeException.class) .isThrownBy(() -> { mathService.divide(3, 0); }) .withMessageContaining("error"); } }
上面的測試可能有些奇怪,mock的物件也同時作為測試的目標。這是因為我們的目的在於介紹mock,所以簡化了測試流程。
注意,如果我們對方法的其中一個引數使用了模式,其他的引數都需要使用模式。比如下面這句:
Mockito.when(mathService.divide(Mockito.anyInt(), Mockito.eq(0)))
,我們的本意是 Mockito.when(mathService.divide(Mockito.anyInt(), 0))
,但是我們不得不為第二個引數使用模式。
附錄
相關注解的彙總
註解 | 說明 |
---|---|
@RunWith |
junit的註解,通過這個註解使用 SpringRunner.class ,能夠將junit和spring進行整合。後續的spring相關注解才會起效。 |
@SpringBootTest |
spring的註解,通過掃描應用程式中的配置來構建測試用的Spring上下文。 |
@AutoConfigureMockMvc |
spring的註解,能夠自動配置 MockMvc 物件例項,用來在模擬測試環境中傳送http請求。 |
@WebMvcTest |
spring的註解,切片測試的一種。使之替換 @SpringBootTest 能將構建bean的範圍限定於web層,但是web層的下層依賴bean,需要通過mock來模擬。也可以通過引數指定只例項化web層的某一個到多個controller。具體可參考 Auto-configured Spring MVC Tests 。 |
@RestClientTest |
spring的註解,切片測試的一種。如果應用程式作為客戶端訪問其他Rest服務,可以通過這個註解來測試客戶端的功能。具體參考 Auto-configured REST Clients 。 |
@MybatisTest |
mybatis按照spring的習慣開發的註解,切片測試的一種。使之替換 @SpringBootTest ,能夠將構建bean的返回限定於mybatis-mapper層。具體可參考 mybatis-spring-boot-test-autoconfigure 。 |
@JdbcTest |
spring的註解,切片測試的一種。如果應用程式中使用Jdbc作為持久層(spring的 JdbcTemplate ),那麼可以使用該註解代替 @SpringBootTest ,限定bean的構建範圍。官方參考資料有限,可自行網上查詢資料。 |
@DataJpaTest |
spring的註解,切片測試的一種。如果使用Jpa作為持久層技術,可以使用這個註解,參考 Auto-configured Data JPA Tests 。 |
@DataRedisTest |
spring的註解,切片測試的一種。具體內容參考 Auto-configured Data Redis Tests 。 |