持續(xù)集成之路—服務(wù)層的單元測試
在完成了數(shù)據(jù)訪問層的單元之后,接下來看如何編寫服務(wù)層(Service)的單元測試。服務(wù)層應(yīng)該是整個系統(tǒng)中得重中之重,嚴密的業(yè)務(wù)邏輯設(shè)計保證了系統(tǒng)穩(wěn)定運行,所以這一層的單元測試也應(yīng)該占很大比重。雖然一般情況下單元測試應(yīng)該盡量通過mock剝離依賴,但是由于在當前的項目中數(shù)據(jù)訪問層使用spring-data框架,并沒有包含太多的邏輯,因此我就把服務(wù)層和數(shù)據(jù)訪問層放在做了一個偽單元測試。
一、一般邏輯的單元測試。
這里采用的方式和數(shù)據(jù)訪問層幾乎是一樣的,主要包含三步:
1. 通過@DatabaseSetup指定測試用數(shù)據(jù)集
2. 執(zhí)行被測試方法
3. 通過Dao從數(shù)據(jù)庫中查詢數(shù)據(jù)驗證執(zhí)行結(jié)果
假設(shè)要被測試的代碼方法是:
@Service @Transactional(readOnly = true) public class ShopServiceImpl extends BaseService implements ShopService{ private Logger logger = LoggerFactory.getLogger(ShopServiceImpl.class); @Transactional(readOnly = false) public Floor addFloor(String buildingName, int floorNum, String layout) { //如果已經(jīng)存在對應(yīng)的樓層信息,則拋出已經(jīng)存在的異常信息 Floor floor = floorDao.findByBuildingNameAndFloorNum(buildingName, floorNum); if (floor != null) { throw new OnlineShopException(ExceptionCode.Shop_Floor_Existed); } //如果不存在對應(yīng)的商場信息,則添加新的商場 Building building = buildingDao.findByName(buildingName); if (building == null) { building = new Building(); building.setName(buildingName); buildingDao.save(building); } //添加并返回樓層信息 floor = new Floor(); floor.setBuilding(building); floor.setFloorNum(floorNum); floor.setMap(layout); floorDao.save(floor); return floor; } } |
其對應(yīng)的接口是:
public interface ShopService {
public Floor addFloor(String buildingName, int floorNum, String layout);
}
這段邏輯代碼的意思十分簡單和直白,那么要編寫的單元的測試必須要包含所有分支情況:a. 商場和樓層信息都存在的,拋出異常 b. 商場存在,而樓層不存在, 樓層信息都被添加的。 c. 商場和樓層都不存在,全部新增。這里就以第一種情況為例,先準備測試數(shù)據(jù):
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<building id="1" name="New House"/>
<floor id="1" building="1" floor_num="2"/>
</dataset>
接著編寫測試用例,注意要必須得注解不能忘掉:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext-test.xml") @Transactional @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, CustomTransactionDbUnitTestExecutionListener.class, ForeignKeyDisabling.class}) public class ShopServiceTest { @Autowired private ShopService shopService; @Test @DatabaseSetup("shop/ShopService-addFloorExistException-dataset.xml") public void testAddFloorExistException(){ try { shopService.addFloor("New House", 2, ""); fail(); } catch(Exception e){ assertTrue(e instanceof OnlineShopException); assertEquals(ExceptionCode.Shop_Floor_Existed.code(), ((OnlineShopException)e).getCode()); } } } |
這個測試和數(shù)據(jù)訪問層的測試看起來沒有什么兩樣。
二、使用Mock對象隔離第三方接口
軟件開發(fā)中一般都存在和第三方集成的情況,比如調(diào)用新浪的認證、百度的地圖等等。那么在編寫測試的時候,基于效率的考慮,一般情況不會真的去調(diào)用這些遠程API(當然應(yīng)該有其他測試可以及時發(fā)現(xiàn)第三方接口的變化),而是假定它們一直會返回預(yù)期的結(jié)果。這個時候就需要用到mock對象,來模擬這些API產(chǎn)生相應(yīng)的結(jié)果。
在這里,我是用了mockito,使用十分方便。假如現(xiàn)在用戶登錄時,需要去第三方系統(tǒng)驗證,那么現(xiàn)在來看如何對這個場景進行測試。還是先來看被測試的方法:
private boolean validateUser(String inputName, String inputPassword) {
return thirdPartyAPI.authenticate(inputName, inputPassword);
}
其中thirdPartyAPI就是第三方用來認證的API。下面來看測試代碼:
public class UserServiceTest { @Autowired private UserService userService; private ThirdPartyAPI mockThirdPartyAPI = mock(ThirdPartyAPI.class); @Test public void testLogin(){ //指定mock對象特定操作的返回結(jié)果 when(mockThirdPartyAPI.authenticate("jiml", "jiml")).thenReturn(true); //通過Setter用mock對象替換由Spring初始化的第三方依賴 ((UserServiceImpl)userService).setThirdPartyAPI(mockThirdPartyAPI); boolean loginStatus = userService.login("jiml", "jiml"); assertTrue(loginStatus); } } |
其實服務(wù)層的測試并沒有太多的新東西,而最關(guān)鍵的問題是如何把邏輯中各個分支都能測試到,使測試真正起到為軟件質(zhì)量保駕護航的作用。
posted on 2014-06-25 11:26 順其自然EVO 閱讀(224) 評論(0) 編輯 收藏 所屬分類: 測試學(xué)習(xí)專欄