基於spring-boot的應用程序的單元+集成測試方案

概述

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

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

以前曾經總結過一篇文章:基於spring-boot的應用程序的單元測試方案,可是當時只是從技術實現的角度去研究單元測試,不少概念沒有搞清楚。本文在從新梳理脈絡,豐富概念的基礎上,整合了前文的大部份內容,可是有一部分幾乎在實踐中用不到的內容就被刪去了。git

在個人我的wiki站點,能夠得到更好的閱讀體驗喔:基於spring-boot的應用程序的單元+集成測試方案程序員

概念解析

單元測試和集成測試

測試領域有不少場景,好比單元測試,集成測試,系統測試,冒煙測試,迴歸測試,端到端測試,功能測試等。測試的分類方式各有不一樣,一些測試場景也可能存在重疊。具體這些場景的概念和區別,你們能夠閱讀文末給出的參考資料。github

這裏主要以程序員的視角談一下我理解的單元測試和集成測試。web

單元測試是編寫單元測試類,針對類級別的測試。好比使用Junit框架,針對一個類,寫一個測試類,測試目標類的大部分主要方法。redis

須要注意單元測試的級別是類。項目當中,類之間的依賴調用是很常見的事,若是你要測試一個類,而這個目標類又調用了另外一個類,那麼在測試時就沒有遵照「在一個類範圍內進行測試」,天然算不得單元測試。spring

如上圖所示,假設A,B,C,D四個類之間存在上述的依賴關係,咱們要測試類A,那麼如何遵照「在類A的範圍內測試」?sql

這就是模擬框架要解決的問題了,經過模擬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

參考資料

相關文章
相關標籤/搜索