1. 程式人生 > >基於spring-boot的應用程式的單元+整合測試方案

基於spring-boot的應用程式的單元+整合測試方案

概述

本文主要介紹單元測試、整合測試相關的概念、技術實現以及最佳實踐。

本文的demo是基於Java語言,Spring Boot構建的web應用。測試框架使用Junit,模擬框架使用mockito。

之前曾經總結過一篇文章:基於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.whencityMapper.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

參考資料