引子:自上世紀末Kent Beck提出TDD(Test-Driven Development)開發(fā)理念以來,開發(fā)和測試的邊界變的越來越模糊础废,從原本上下游的依賴關(guān)系饥伊,逐步演變成你中有我、我中有你的互賴關(guān)系智亮,甚至很多公司設(shè)立了新的QE(Quality Engineer)職位俄删。和傳統(tǒng)的QA(Quality Assurance)不同宏怔,QE的主要職責是通過工程化的手段保證項目質(zhì)量,這些手段包括但不僅限于編寫單元測試畴椰、集成測試臊诊,搭建自動化測試流程,設(shè)計性能測試等迅矛》列桑可以說,QE身上兼具了QA的質(zhì)量意識和開發(fā)的工程能力秽褒。從這篇開始壶硅,我會從開發(fā)的角度分三期聊聊QE這個亦測試亦開發(fā)的角色所需的基本技能。
1 什么是Mock销斟?
在軟件測試領(lǐng)域庐椒,Mock的意思是模擬,簡單來說蚂踊,就是通過某種技術(shù)手段模擬測試對象的行為约谈,返回預(yù)先設(shè)計的結(jié)果。這里的關(guān)鍵詞是預(yù)先設(shè)計犁钟,也就是說對于任意被測試的對象棱诱,可以根據(jù)具體測試場景的需要,返回特定的結(jié)果涝动。打個比方迈勋,就像BBC紀錄片里面的假企鵝,可以根據(jù)拍攝需要作出不同的反應(yīng)醋粟。
2 Mock有什么用靡菇?
理解了什么是Mock,再來看Mock有哪些用途米愿。首先厦凤,Mock可以用來解除測試對象對外部服務(wù)的依賴(比如數(shù)據(jù)庫,第三方接口等)育苟,使得測試用例可以獨立運行较鼓。不管是傳統(tǒng)的單體應(yīng)用,還是現(xiàn)在流行的微服務(wù)违柏,這點都特別重要博烂,因為任何外部依賴的存在都會極大的限制測試用例的可遷移性和穩(wěn)定性拓哺。可遷移性是指脖母,如果要在一個新的測試環(huán)境中運行相同的測試用例,那么除了要保證測試對象自身能夠正常運行闲孤,還要保證所有依賴的外部服務(wù)也能夠被正常調(diào)用谆级。穩(wěn)定性是指,如果外部服務(wù)不可用讼积,那么測試用例也可能會失敗肥照。通過Mock去除外部依賴之后,不管是測試用例的可遷移性還是穩(wěn)定性勤众,都能夠上一個臺階舆绎。
Mock的第二個好處是替換外部服務(wù)調(diào)用,提升測試用例的運行速度们颜。任何外部服務(wù)調(diào)用至少是跨進程級別的消耗吕朵,甚至是跨系統(tǒng)、跨網(wǎng)絡(luò)的消耗窥突,而Mock可以把消耗降低到進程內(nèi)努溃。比如原來一次秒級的網(wǎng)絡(luò)請求,通過Mock可以降至毫秒級阻问,整整3個數(shù)量級的差別梧税。
Mock的第三個好處是提升測試效率。這里說的測試效率有兩層含義称近。第一層含義是單位時間運行的測試用例數(shù)第队,這是運行速度提升帶來的直接好處。而第二層含義是一個QE單位時間創(chuàng)建的測試用例數(shù)刨秆。如何理解這第二層含義呢凳谦?以單體應(yīng)用為例,隨著業(yè)務(wù)復(fù)雜度的上升坛善,為了運行一個測試用例可能需要準備很多測試數(shù)據(jù)晾蜘,與此同時還要盡量保證多個測試用例之間的測試數(shù)據(jù)互不干擾。為了做到這一點眠屎,QE往往需要花費大量的時間來維護一套可運行的測試數(shù)據(jù)剔交。有了Mock之后,由于去除了測試用例之間共享的數(shù)據(jù)庫依賴改衩,QE就可以針對每一個或者每一組測試用例設(shè)計一套獨立的測試數(shù)據(jù)岖常,從而很容易的做到不同測試用例之間的數(shù)據(jù)隔離性。而對于微服務(wù)葫督,由于一個微服務(wù)可能級聯(lián)依賴很多其他的微服務(wù)竭鞍,運行一個測試用例甚至需要跨系統(tǒng)準備一套測試數(shù)據(jù)板惑,如果沒有Mock,基本上可以說是不可能的偎快。因此冯乘,不管是單體應(yīng)用還是微服務(wù),有了Mock之后晒夹,QE就可以省去大量的準備測試數(shù)據(jù)的時間裆馒,專注于測試用例本身,自然也就提升了單人的測試效率丐怯。
3 如何Mock喷好?
說了這么多Mock的好處,那么究竟如何在測試中使用Mock呢读跷?針對不同的測試場景梗搅,可以選擇不同的Mock框架。
3.1 Mockito
如果測試對象是一個方法效览,尤其是涉及數(shù)據(jù)庫操作的方法无切,那么Mockito可能是最好的選擇。作為使用最廣泛的Mock框架钦铺,Mockito出于EasyMock而勝于EasyMock订雾,乃至被默認集成進Spring Testing。其實現(xiàn)原理是矛洞,通過CGLib在運行時為每一個被Mock的類或者對象動態(tài)生成一個代理對象洼哎,返回預(yù)先設(shè)計的結(jié)果。集成Mockito的基本步驟是:
- 標記被Mock的類或者對象沼本,生成代理對象
- 通過Mockito API定制代理對象的行為
- 調(diào)用代理對象的方法噩峦,獲得預(yù)先設(shè)計的結(jié)果
下面是我GitHub上的示例工程里的一個例子,
@RunWith(SpringRunner.class)
@SpringBootTest
public class SignonServiceTests {
// 測試對象抽兆,一個服務(wù)類
@Autowired
private SignonService signonService;
// 被Mock的類识补,被服務(wù)類所依賴的一個DAO類
@MockBean
private SignonDao dao;
@Test
public void testFindAll() {
// SignonService#findAll()內(nèi)部會調(diào)用SignonDao#findAll()
// 如果不做定制,所有被Mock的類默認返回空
List<Signon> signons = signonService.findAll();
assertTrue(CollectionUtils.isEmpty(signons));
// 定制返回結(jié)果
Signon signon = new Signon();
signon.setUsername("foo");
when(dao.findAll()).thenReturn(Lists.newArrayList(signon));
signons = signonService.findAll();
// 驗證返回結(jié)果和預(yù)先設(shè)計的結(jié)果一致
assertEquals(1, signons.size());
assertEquals("foo", signons.get(0).getUsername());
}
}
從上面的測試用例可以看到辫红,通過Mock服務(wù)類所依賴的DAO類凭涂,我們可以跳過所有的數(shù)據(jù)庫操作,任意定制返回結(jié)果贴妻,從而專注于測試服務(wù)類內(nèi)部的業(yè)務(wù)邏輯切油。這是傳統(tǒng)的非Mock測試所難以實現(xiàn)的。
注意:Mockito不支持Mock私有方法或者靜態(tài)方法名惩,如果要Mock這類方法澎胡,可以使用PowerMock。
3.2 WireMock
如果說Mocketo是瑞士軍刀,可以Mock Everything攻谁,那么WireMock就是為微服務(wù)而生的倚天劍稚伍。和處在對象層的Mockito不同,WireMock針對的是API戚宦。假設(shè)有兩個微服務(wù)个曙,Service-A和Service-B,Service-A里的一個API(姑且稱為API-1)受楼,依賴于Service-B困檩,那么使用傳統(tǒng)的測試方法,測試API-1時必然需要同時啟動Service-B那槽。如果使用WireMock,那么就可以在Service-A端Mock所有依賴的Service-B的API等舔,從而去掉Service-B這個外部依賴骚灸。
同樣看一個我GitHub上的示例工程里的一個例子,
@RunWith(SpringRunner.class)
@WebMvcTest(VacationController.class)
public class VacationControllerTests {
// Mock被依賴的另一個微服務(wù)
@Rule
public WireMockRule wireMockRule = new WireMockRule(3001);
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Before
public void before() throws JsonProcessingException {
// 定制返回結(jié)果
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 {
// 構(gòu)造請求參數(shù)
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)
// 驗證返回結(jié)果和預(yù)先設(shè)計的結(jié)果一致
.andExpect(status().isOk())
.andExpect(content().contentType(APPLICATION_JSON_UTF8))
.andExpect(content().string(objectMapper.writeValueAsString(expected)));
}
}
和Mockito類似慌植,在測試用例中集成WireMock的基本步驟是:
- 聲明代理服務(wù)甚牲,以替代被Mock的微服務(wù)
- 通過WireMock API定制代理服務(wù)的返回結(jié)果
- 調(diào)用代理服務(wù),獲得預(yù)先設(shè)計的結(jié)果
值得一提的是蝶柿,除了API方式的集成丈钙,WireMock還支持以Jar包的形式獨立運行,從配置文件中加載預(yù)先設(shè)計的響應(yīng)結(jié)果交汤,以替代被Mock的微服務(wù)雏赦。更多信息可以參閱官方文檔。
其他類似的Mock API的框架還有OkHttp的mockwebserver芙扎,moco和mockserver星岗。mockwebserver也屬于嵌入式Mock框架的范疇,但功能過于簡單戒洼。moco俏橘,mockserver雖然功能完善,但需要獨立部署圈浇,和WireMock相比不具有優(yōu)勢寥掐。
4 小結(jié)
以上就是我對Mock技術(shù)的一些見解,歡迎你到我的留言板分享磷蜀,和大家一起過過招召耘。最后還要說一句,Mock技術(shù)雖然強大蠕搜,但主要還是適用于單元測試怎茫,在集成測試,性能測試,自動化測試等其他測試領(lǐng)域使用并不多轨蛤。