面向開發的測試技術(一):Mock

引子:自上世紀末Kent Beck提出TDD(Test-Driven Development)開發理念以來,開發和測試的邊界變的愈來愈模糊,從本來上下游的依賴關係,逐步演變成你中有我、我中有你的互賴關係,甚至不少公司設立了新的QE(Quality Engineer)職位。和傳統的QA(Quality Assurance)不一樣,QE的主要職責是經過工程化的手段保證項目質量,這些手段包括但不只限於編寫單元測試、集成測試,搭建自動化測試流程,設計性能測試等。能夠說,QE身上兼具了QA的質量意識和開發的工程能力。從這篇開始,我會從開發的角度分三期聊聊QE這個亦測試亦開發的角色所需的基本技能。java

1 什麼是Mock?

在軟件測試領域,Mock的意思是模擬,簡單來講,就是經過某種技術手段模擬測試對象的行爲,返回預先設計的結果。這裏的關鍵詞是預先設計,也就是說對於任意被測試的對象,能夠根據具體測試場景的須要,返回特定的結果。打個比方,就像BBC紀錄片裏面的假企鵝,能夠根據拍攝須要做出不一樣的反應。git

2 Mock有什麼用?

理解了什麼是Mock,再來看Mock有哪些用途。首先,Mock能夠用來解除測試對象對外部服務的依賴(好比數據庫,第三方接口等),使得測試用例能夠獨立運行。不論是傳統的單體應用,仍是如今流行的微服務,這點都特別重要,由於任何外部依賴的存在都會極大的限制測試用例的可遷移性和穩定性。可遷移性是指,若是要在一個新的測試環境中運行相同的測試用例,那麼除了要保證測試對象自身可以正常運行,還要保證全部依賴的外部服務也可以被正常調用。穩定性是指,若是外部服務不可用,那麼測試用例也可能會失敗。經過Mock去除外部依賴以後,不論是測試用例的可遷移性仍是穩定性,都可以上一個臺階。github

Mock的第二個好處是替換外部服務調用,提高測試用例的運行速度。任何外部服務調用至少是跨進程級別的消耗,甚至是跨系統、跨網絡的消耗,而Mock能夠把消耗下降到進程內。好比原來一次秒級的網絡請求,經過Mock能夠降至毫秒級,整整3個數量級的差異。web

Mock的第三個好處是提高測試效率。這裏說的測試效率有兩層含義。第一層含義是單位時間運行的測試用例數,這是運行速度提高帶來的直接好處。而第二層含義是一個QE單位時間建立的測試用例數。如何理解這第二層含義呢?以單體應用爲例,隨着業務複雜度的上升,爲了運行一個測試用例可能須要準備不少測試數據,與此同時還要儘可能保證多個測試用例之間的測試數據互不干擾。爲了作到這一點,QE每每須要花費大量的時間來維護一套可運行的測試數據。有了Mock以後,因爲去除了測試用例之間共享的數據庫依賴,QE就能夠針對每個或者每一組測試用例設計一套獨立的測試數據,從而很容易的作到不一樣測試用例之間的數據隔離性。而對於微服務,因爲一個微服務可能級聯依賴不少其餘的微服務,運行一個測試用例甚至須要跨系統準備一套測試數據,若是沒有Mock,基本上能夠說是不可能的。所以,不論是單體應用仍是微服務,有了Mock以後,QE就能夠省去大量的準備測試數據的時間,專一於測試用例自己,天然也就提高了單人的測試效率。spring

3 如何Mock?

說了這麼多Mock的好處,那麼究竟如何在測試中使用Mock呢?針對不一樣的測試場景,能夠選擇不一樣的Mock框架。數據庫

3.1 Mockito

若是測試對象是一個方法,尤爲是涉及數據庫操做的方法,那麼Mockito多是最好的選擇。做爲使用最普遍的Mock框架,Mockito出於EasyMock而勝於EasyMock,乃至被默認集成進Spring Testing。其實現原理是,經過CGLib在運行時爲每個被Mock的類或者對象動態生成一個代理對象,返回預先設計的結果。集成Mockito的基本步驟是:api

  1. 標記被Mock的類或者對象,生成代理對象
  2. 經過Mockito API定製代理對象的行爲
  3. 調用代理對象的方法,得到預先設計的結果

下面是我GitHub上的示例工程裏的一個例子,網絡

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

    // 測試對象,一個服務類
    @Autowired
    private SignonService signonService;

    // 被Mock的類,被服務類所依賴的一個DAO類
    @MockBean
    private SignonDao dao;

    @Test
    public void testFindAll() {
        // SignonService#findAll()內部會調用SignonDao#findAll()
        // 若是不作定製,全部被Mock的類默認返回空
        List<Signon> signons = signonService.findAll();
        assertTrue(CollectionUtils.isEmpty(signons));

        // 定製返回結果
        Signon signon = new Signon();
        signon.setUsername("foo");
        when(dao.findAll()).thenReturn(Lists.newArrayList(signon));

        signons = signonService.findAll();
        // 驗證返回結果和預先設計的結果一致
        assertEquals(1, signons.size());
        assertEquals("foo", signons.get(0).getUsername());
    }
}複製代碼

從上面的測試用例能夠看到,經過Mock服務類所依賴的DAO類,咱們能夠跳過全部的數據庫操做,任意定製返回結果,從而專一於測試服務類內部的業務邏輯。這是傳統的非Mock測試所難以實現的。app

注意:Mockito不支持Mock私有方法或者靜態方法,若是要Mock這類方法,可使用PowerMock框架

3.2 WireMock

若是說Mocketo是瑞士軍刀,能夠Mock Everything,那麼WireMock就是爲微服務而生的倚天劍。和處在對象層的Mockito不一樣,WireMock針對的是API。假設有兩個微服務,Service-A和Service-B,Service-A裏的一個API(姑且稱爲API-1),依賴於Service-B,那麼使用傳統的測試方法,測試API-1時必然須要同時啓動Service-B。若是使用WireMock,那麼就能夠在Service-A端Mock全部依賴的Service-B的API,從而去掉Service-B這個外部依賴。

一樣看一個我GitHub上的示例工程裏的一個例子,

@RunWith(SpringRunner.class)
@WebMvcTest(VacationController.class)
public class VacationControllerTests {

    // Mock被依賴的另外一個微服務
    @Rule
    public WireMockRule wireMockRule = new WireMockRule(3001);

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Before
    public void before() throws JsonProcessingException {
        // 定製返回結果
        JsonResult<Boolean> expected = JsonResult.ok(true);
        stubFor(get(urlPathEqualTo("/api/vacation/isWeekend"))
                .willReturn(aResponse()
                        .withStatus(OK.value())
                        .withHeader(CONTENT_TYPE, APPLICATION_JSON_UTF8_VALUE)
                        .withBody(objectMapper.writeValueAsString(expected))));
    }

    @Test
    public void testIsWeekendProxy() throws Exception {
        // 構造請求參數
        VacationRequest request = new VacationRequest();
        request.setType(PERSONAL);
        OffsetDateTime lastSunday = OffsetDateTime.now().with(TemporalAdjusters.previous(SUNDAY));
        request.setStart(lastSunday);
        request.setEnd(lastSunday.plusDays(1));

        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/vacation/isWeekend");
        request.toMap().forEach((k, v) -> builder.param(k, v));
        JsonResult<Boolean> expected = JsonResult.ok(true);

        mockMvc.perform(builder)
                // 驗證返回結果和預先設計的結果一致
                .andExpect(status().isOk())
                .andExpect(content().contentType(APPLICATION_JSON_UTF8))
                .andExpect(content().string(objectMapper.writeValueAsString(expected)));
    }
}複製代碼

和Mockito相似,在測試用例中集成WireMock的基本步驟是:

  1. 聲明代理服務,以替代被Mock的微服務
  2. 經過WireMock API定製代理服務的返回結果
  3. 調用代理服務,得到預先設計的結果

值得一提的是,除了API方式的集成,WireMock還支持以Jar包的形式獨立運行,從配置文件中加載預先設計的響應結果,以替代被Mock的微服務。更多信息能夠參閱官方文檔

其餘相似的Mock API的框架還有OkHttp的mockwebservermocomockserver。mockwebserver也屬於嵌入式Mock框架的範疇,但功能過於簡單。moco,mockserver雖然功能完善,但須要獨立部署,和WireMock相比不具備優點。

4 小結

以上就是我對Mock技術的一些看法,歡迎你到個人留言板分享,和你們一塊兒過過招。最後還要說一句,Mock技術雖然強大,但主要仍是適用於單元測試,在集成測試,性能測試,自動化測試等其餘測試領域使用並很少。

5 參考

相關文章
相關標籤/搜索