持續集成之路——服務層的單元測試

        在完成了數據訪問層的單元之後,接下來看如何編寫服務層(Service)的單元測試。服務層應該是整個系統中得重中之重,嚴密的業務邏輯設計保證了系統穩定運行,所以這一層的單元測試也應該佔很大比重。雖然一般情況下單元測試應該儘量通過mock剝離依賴,但是由於在當前的項目中數據訪問層使用spring-data框架,並沒有包含太多的邏輯,因此我就把服務層和數據訪問層放在做了一個僞單元測試。

        一、一般邏輯的單元測試。

        這裏採用的方式和數據訪問層幾乎是一樣的,主要包含三步:

        1. 通過@DatabaseSetup指定測試用數據集

        2. 執行被測試方法

        3. 通過Dao從數據庫中查詢數據驗證執行結果

        假設要被測試的代碼方法是:

@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) {
        //如果已經存在對應的樓層信息,則拋出已經存在的異常信息
        Floor floor = floorDao.findByBuildingNameAndFloorNum(buildingName, floorNum);
        if (floor != null) {
            throw new OnlineShopException(ExceptionCode.Shop_Floor_Existed);
        }
        
        //如果不存在對應的商場信息,則添加新的商場
        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;
    }
}

其對應的接口是:

public interface ShopService {
    public Floor addFloor(String buildingName, int floorNum, String layout);
}

        這段邏輯代碼的意思十分簡單和直白,那麼要編寫的單元的測試必須要包含所有分支情況:a. 商場和樓層信息都存在的,拋出異常 b. 商場存在,而樓層不存在, 樓層信息都被添加的。 c.  商場和樓層都不存在,全部新增。這裏就以第一種情況爲例,先準備測試數據:

<?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());
        }
    }

}

這個測試和數據訪問層的測試看起來沒有什麼兩樣。

        二、使用Mock對象隔離第三方接口

        軟件開發中一般都存在和第三方集成的情況,比如調用新浪的認證、百度的地圖等等。那麼在編寫測試的時候,基於效率的考慮,一般情況不會真的去調用這些遠程API(當然應該有其他測試可以及時發現第三方接口的變化),而是假定它們一直會返回預期的結果。這個時候就需要用到mock對象,來模擬這些API產生相應的結果。

        在這裏,我是用了mockito,使用十分方便。假如現在用戶登錄時,需要去第三方系統驗證,那麼現在來看如何對這個場景進行測試。還是先來看被測試的方法:

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對象特定操作的返回結果
        when(mockThirdPartyAPI.authenticate("jiml", "jiml")).thenReturn(true);
        //通過Setter用mock對象替換由Spring初始化的第三方依賴
        ((UserServiceImpl)userService).setThirdPartyAPI(mockThirdPartyAPI);
        boolean loginStatus = userService.login("jiml", "jiml");
        assertTrue(loginStatus);
    }
}

      其實服務層的測試並沒有太多的新東西,而最關鍵的問題是如何把邏輯中各個分支都能測試到,使測試真正起到爲軟件質量保駕護航的作用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章