單元測(cè)試-mockito+powermock

單元測(cè)試--Java

使用mockito+powermock進(jìn)行java單元測(cè)試

實(shí)例

如下一個(gè)正常業(yè)務(wù)代碼嫡意,接下來就對(duì)HelloController.say方法進(jìn)行單元測(cè)試用例代碼開發(fā)。

業(yè)務(wù)代碼

public class HelloController{

    @Autowired
    private HelloService helloService;

    @Autowired
    private UserService userService;

    @GetMapping("/say")
    public String say(Integer id){
        HelloUser user=userService.findById(id);
        if(user==null){
            throw new RuntimeException("user not found");
        }
        return helloService.doHello(user);
    }
}

一個(gè)正常用例

在HelloController.say()個(gè)方法體內(nèi),引用了另外兩個(gè)Bean,userService和helloService。
正常服務(wù)啟動(dòng),我們是必須要有這兩個(gè)Bean的實(shí)例。但是在測(cè)試用例代碼中其實(shí)我們是沒必要知道UserService和和HelloService的具體實(shí)現(xiàn)的茬祷,所以這兩個(gè)Bean我們可以通過mock的方式虛擬出來。

@RunWith(MockitoJUnitRunner.class)
public class TestHelloController{
    @InjectMock
    private HelloController helloController;
    @Mock
    private HelloService helloService;
    @Mock
    private UserService userService;
    /**
    * 正常用例
    **/
    @Test
    public void testSay(){
        Integer id = 1;
        HelloUser user = new HlloUser();
        //模擬方法調(diào)用的傳參和返回值
        Mockito.when(userService.findById(id)).thenReturn(user);
        Mockito.when(helloService.doHello(user)).thenReturn("say hi!");
        String resp=helloController.say(id);
        //驗(yàn)證方法調(diào)用次數(shù)
        Mockito.verfiy(userService,times(1)).findById(id);
        Mockito.verfiy(helloService,times(1)).doHello(user);
        //驗(yàn)證結(jié)果
        Assert.assertTrue(resp.equals("say hi!"));
    }
    /**
    * 異常用例
    **/
    @Test(expected=RuntimeException.class)
    public void testSayException(){
        //這里模擬userService調(diào)用findById無論傳入什么參數(shù)并蝗,返回都是null
        Mockito.when(userService.findById(Mockito.anyString())).thenReturn(null);
        Integer id = 1;
        helloController.say(id);
    }
}

靜態(tài)方法模擬

一些方法中有時(shí)候需要調(diào)用靜態(tài)方法(如HelloController.create方法)祭犯,但是靜態(tài)方法內(nèi)部,也不是該用例需要關(guān)心的滚停,這個(gè)使用可以使用powermock對(duì)靜態(tài)方法進(jìn)行mock沃粗。

//業(yè)務(wù)代碼
public class HelloController{
    //...省略部分代碼
    @GetMapping("/create")
    public String create(HelloUser user){
        user.setNo(BusinessNoUtils.generateNo());
        userService.save(user);
        return user.getNo();
    }

}
//用例代碼--因?yàn)檫@里使用了Powermock,所以這里運(yùn)行器一定是要使用PowerMockRunner.class
@Runwith(PowerMockRunner.class) 
@PrepareForTest(BusinessNoUtils.class)
public class TestHelloController(){
    //...省略部分代碼
    @Test
    public void testCreate(){
        HelloUser user= new HelloUser();
        //需要聲明键畴,模擬靜態(tài)方法
        PowerMockito.mockStatic(BusinessNoUtils.class);
        Mockito.when(BusinessNoUtils.generateNo()).thenReturn("111111");
        Mockito.when(userService.save(Mockito.argThat(e->{
            //簡單判斷傳入?yún)?shù)是否正確
            if(e.getNo().equals("111111")){
                return true;
            }
            return false;
        })));
        String resp=helloController.create(user);
        Assert.assertTrue(resp.equals("111111"));
    }
}

new對(duì)象模擬

有一些業(yè)務(wù)代碼中需要?jiǎng)?chuàng)建一個(gè)對(duì)象最盅,這個(gè)對(duì)象中有些操作也沒必要在業(yè)務(wù)代碼中體現(xiàn),而且這個(gè)對(duì)象可能會(huì)依賴其他東西起惕。對(duì)于這種也可以通過Powermock的方式進(jìn)行模擬涡贱。

// 業(yè)務(wù)代碼
public HelloController{
    //...省略其他代碼
   public String copyUser(String id){
        HelloUser helloUser=userService.findById(id);
        HelloUser destUser = new HelloUser();
        String userNo=destUser.copyFrom(helloUser);
        return userNo;
   }
}
//用例代碼
@Runwith(PowerMockRunner.class)
//這里的PrepareForTest引用的類是要   new 對(duì)象所在的類,這里要注意跟模擬靜態(tài)方法不一樣的地方
@PrepareForTest(HelloController.class)
public TestHelloController{
    //...省略其他代碼
    @Test
    public void testCopyUser(){
        HelloUser rawUser = new HelloUser();
        rawUser.setName("123");
        Mockito.when(userService.findById(1)).thenReturn(rawUser);
        HelloUser destUser = Mockito.mock(HelloUser.class);//模擬對(duì)象
        //需要聲明new對(duì)象返回值
        PowerMockito.whenNew(HelloUser.class).withNoArguments().thenReturn(destUser);
        //模擬copyFrom方法
        Mockito.when(destUser.copyFrom(rawUser)).thenReturn("123");
        String taskNo=controller.copyUser(1);
        Mockito.verfiy(taskNo.equals("123"));
    }
}

@Spy的使用

上面的代碼是因?yàn)橐故居美窍耄院芏鄻I(yè)務(wù)邏輯是寫在Controller層的问词。但實(shí)際業(yè)務(wù)開發(fā)中,Controller層只是薄薄的一層嘀粱,沒有什么業(yè)務(wù)邏輯激挪,但Controller層又是整個(gè)用例的入口。這個(gè)時(shí)候锋叨,我們不希望UserService在該用例中被模擬垄分,而是希望這個(gè)用例下來,整條鏈路上我們自己寫的代碼都能覆蓋到娃磺。這個(gè)時(shí)候我們就需要@Spy注解了锋喜。

//業(yè)務(wù)代碼
public class HelloContrller{
    @Autowired
    UserService userService;

    @GetMapping("/delete/{id}")
    public boolean deleteUser(@PathVariable("id")String id){
        return userService.deleteById(id);
    }
}
public class UserService{
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;
    public boolean deleteById(String id){
        HelloUser userInfo=Optional.ofNullable(userMapper.findById(id)).orElseThrow(()->new RuntionException("not found"));
        //空方法
        userMapper.deleteByNo(userInfo.getNo);
        roleService.deleteByUserId(id);
        return true;
    }

}
public class RoleService{
    @Autowired
    private RoleMapper roleMapper;
    public boolean deleteByUserId(String userId){
        return roleMapper.deleteByUserId(userId);
    }
}

//用例代碼
@RunWith(MockitoJUnitRunner.class) 
public class TestHelloController{
    @InjectMocks HelloController helloController;
    @Spy UserService userService ;
    @Spy RoleService roleService ;
    //dao層繼續(xù)模擬
    @Mock RoleMapper roleMapper;
    @Mock UserMapper userMapper;
    @Before
    public void setUp(){
        //由于沒有自動(dòng)注入,所以手動(dòng)set進(jìn)去
        WhiteBox.setInternalState(userService,"userMapper",userMapper);
        WhiteBox.setInternalState(userService,"roleService",roleService);
        WhiteBox.setInternalState(roleService,"roleMapper",roleMapper);
    }

    public void testDelete(){
        String userId = "1";
        UserInfo userInfo = new UserInfo();
        userInfo.setNo("123");
        Mockito.when(userMapper.findById(userId)).thenReturn(userInfo);
        //對(duì)于空方法調(diào)用處理
        Mockito.doNothing(userMapper).when(deleteByNo("123"));
        Mockito.when(roleMapper.deleteByUserId(uesrId)).thenReturn(true);
        Assert.assertTrue(helloController.deleteUser(userId));
    }

}

注解作用

  • @RunWith(MockitoJUnitRunner.class) :@Runwith是一個(gè)運(yùn)行器,@RunWith(MockitoJUnitRunner.class)嘿般,表示使用MockitoJUnitRunner來運(yùn)行。
  • @Mock: 聲明的對(duì)象涯冠,對(duì)函數(shù)調(diào)用均執(zhí)行mock炉奴,不執(zhí)行真正部分
  • @Spy :聲明的對(duì)象,對(duì)函數(shù)調(diào)用均執(zhí)行真正部分
  • @InjectMocks: 自動(dòng)講模擬對(duì)象或偵查域注入到被測(cè)試對(duì)象中蛇更。
  • @PrepareForTest:表示聲明哪些類需要被修改

總結(jié)

寫好的單元測(cè)試用例要比實(shí)際寫代碼的工作量要多瞻赶,這部分工作量沒有太多技術(shù)性的東西(從另外的角度來看,可以對(duì)你的代碼有更深入的理解)派任,但確是比較重要的一部分砸逊。
如果項(xiàng)目是一錘子買賣,沒有后續(xù)迭代掌逛,你可以不需要寫測(cè)試用例师逸,否則都需要寫測(cè)試用例。
當(dāng)需要進(jìn)行代碼重構(gòu)的時(shí)候豆混,這些測(cè)試用例會(huì)給你很大幫助篓像。
當(dāng)你改了一段祖?zhèn)鞔a,如果有測(cè)試用例皿伺,會(huì)給你提供不少思路上的幫助员辩。當(dāng)然改完代碼之后,記得同時(shí)修改測(cè)試用例鸵鸥。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末奠滑,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子妒穴,更是在濱河造成了極大的恐慌宋税,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宰翅,死亡現(xiàn)場(chǎng)離奇詭異弃甥,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)汁讼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門淆攻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嘿架,你說我怎么就攤上這事瓶珊。” “怎么了耸彪?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵伞芹,是天一觀的道長。 經(jīng)常有香客問我,道長唱较,這世上最難降的妖魔是什么扎唾? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮南缓,結(jié)果婚禮上胸遇,老公的妹妹穿的比我還像新娘。我一直安慰自己汉形,他們只是感情好纸镊,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著概疆,像睡著了一般逗威。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上岔冀,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天凯旭,我揣著相機(jī)與錄音,去河邊找鬼楣颠。 笑死尽纽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的童漩。 我是一名探鬼主播弄贿,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼矫膨!你這毒婦竟也來了差凹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤侧馅,失蹤者是張志新(化名)和其女友劉穎危尿,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體馁痴,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡谊娇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了罗晕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片济欢。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖小渊,靈堂內(nèi)的尸體忽然破棺而出法褥,到底是詐尸還是另有隱情,我是刑警寧澤酬屉,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布半等,位于F島的核電站揍愁,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏杀饵。R本人自食惡果不足惜莽囤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望凹髓。 院中可真熱鬧烁登,春花似錦、人聲如沸蔚舀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赌躺。三九已至,卻和暖如春羡儿,著一層夾襖步出監(jiān)牢的瞬間礼患,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國打工掠归, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缅叠,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓虏冻,卻偏偏與公主長得像肤粱,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子厨相,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容