面向開發的測試技術(一):Mock
引子:自上世紀末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可以降至毫秒級,整整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的基本步驟是:
- 標記被Mock的類或者物件,生成代理物件
- 通過Mockito API定製代理物件的行為
- 呼叫代理物件的方法,獲得預先設計的結果
下面是我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的基本步驟是:
- 宣告代理服務,以替代被Mock的微服務
- 通過WireMock API定製代理服務的返回結果
- 呼叫代理服務,獲得預先設計的結果
值得一提的是,除了API方式的整合,WireMock還支援以Jar包的形式獨立執行,從配置檔案中載入預先設計的響應結果,以替代被Mock的微服務。更多資訊可以參閱官方文件。
其他類似的Mock API的框架還有OkHttp的mockwebserver,moco和mockserver。mockwebserver也屬於嵌入式Mock框架的範疇,但功能過於簡單。moco,mockserver雖然功能完善,但需要獨立部署,和WireMock相比不具有優勢。
4 小結
以上就是我對Mock技術的一些見解,歡迎你到我的留言板分享,和大家一起過過招。最後還要說一句,Mock技術雖然強大,但主要還是適用於單元測試,在整合測試,效能測試,自動化測試等其他測試領域使用並不多。