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

NO IMAGE

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

1 什麼是Mock?

在軟件測試領域,Mock的意思是模擬,簡單來說,就是通過某種技術手段模擬測試對象的行為,返回預先設計的結果。這裡的關鍵詞是預先設計,也就是說對於任意被測試的對象,可以根據具體測試場景的需要,返回特定的結果。打個比方,就像BBC紀錄片裡面的假企鵝,可以根據拍攝需要作出不同的反應。

2 Mock有什麼用?

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

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

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

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

3 如何Mock?

說了這麼多Mock的好處,那麼究竟如何在測試中使用Mock呢?針對不同的測試場景,可以選擇不同的Mock框架。

3.1 Mockito

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

  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測試所難以實現的。

注意: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 參考

相關文章

響應式編程總覽

【Spring5】響應式Web框架前瞻

面向開發的測試技術(三):Web自動化測試

面向開發的測試技術(二):性能測試