上一篇中,基於調研和分析,決定使用Powermock
完成單元測試的編寫。
關於Powermock
的使用方式,網上有很多的文章進行解釋,下面僅僅介紹一些在Android
上的常用姿勢。
隨着時間推移,該文章會不斷完善。
Mock vs Spy
Powermock提供了mock
和spy
兩種方式,對於Activity
的私有方法的調用驗證通常需要做方法模擬。mock
和spy
都可以實現,mock
是默認對有方法都模擬。spy
是默認對所有方法都不模擬。
個人建議是使用mock
,因爲activity
裏面的方法邏輯很多,而對於一個單元測試,我們往往只是測試一個方法,對其它方法都需要mock
。用以驗證調用或者模擬方法返回值等。
findViewById
activity
中最不缺的就是控件查找,那麼直接調用findViewById()
肯定是會報錯的Stub
。那麼通常的做法是mock
一個activity
,但是mock
的方法的findViewById()
返回值爲null。
舉例:驗證activity
的onCreate
中是否對View
設置了點擊監聽。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_launcher);
mSkipView = findViewById(R.id.skip);
mSkipView.setOnClickListener(this);
}
測試代碼:
@PrepareForTest({LauncherActivity.class, Build.VERSION.class})
public class LauncherActivityTest extends PowerMockTest {
@Mock
LauncherActivity activity;
@Mock
View mSkipView;
@Test
public void onCreateSdk19() throws Exception {
PowerMockito.doCallRealMethod().when(activity, "onCreate", ArgumentMatchers.any());
// 當調用findViewById(R.id.skip)時返回mock的View對象
PowerMockito.doReturn(mSkipView).when(activity).findViewById(R.id.skip);
activity.onCreate(null);
// 是否設置監聽
Mockito.verify(mSkipView).setOnClickListener(ArgumentMatchers.any());
}
Whitebox.setInternalState()
final
和private
字段的賦值。
Whitebox.setInternalState(Build.VERSION.class, "SDK_INT", Integer.valueOf(18));
同理,還有獲取方法,省略…
需要添加加@PrepareForTest
PowerMockito.supress()
Activity
的聲明週期方法中通常會有super.onCreate()
方法,但是super
方法又不能執行,一旦執行就會報錯Stub
。
PowerMockito.suppress(Whitebox.getMethod(AppCompatActivity.class, "onCreate", Bundle.class));
通過上面的方式,在測試Activity.onCreate()
時,父類的AppCompatActivity
的onCreate()
就不會再執行。
PowerMockito.whenNew()
如何驗證一個頁面跳轉呢?
現在常用的方式是通過對於意圖的驗證,判斷是否構造了對應包名的意圖。
聲明當new Intent()
時,返回mock
的intent
對象。
Intent intent = PowerMockito.mock(Intent.class);
PowerMockito.whenNew(Intent.class).withNoArguments().thenReturn(intent);
PowerMockito.whenNew(Intent.class)
.withParameterTypes(Context.class, Class.class)
.withArguments(ArgumentMatchers.any(Context.class), ArgumentMatchers.any(Class.class))
.thenReturn(intent);
驗證
PowerMockito.verifyNew(Intent.class).withArguments(activity, MainActivity.class);
Mockito.verify(intent).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
是否構造了MainActivity
的意圖,是否添加了flags
。
PowerMockito.thenAnswer()
當構造一個dialog
時,通常會有如下代碼:
private void showExpiredDialog() {
if (mAlertDialog == null || !mAlertDialog.isShowing()) {
mAlertDialog = new AlertDialog.Builder(this)
.setMessage("您的登錄狀態已經過期,請重新登錄")
.setPositiveButton(R.string.confirm, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//
startActivity(AppHelper.makeMainIntent(LogoutDialogActivity.this));
}
})
.create();
mAlertDialog.setCancelable(false);
mAlertDialog.show();
}
}
如何驗證setPositiveButton
的OnClickListener
的回調邏輯中的內容是否正確。
@Test
public void logout() throws Exception {
PowerMockito.doCallRealMethod().when(activity, "showExpiredDialog");
// 測試回調邏輯
PowerMockito
.when(builder.setPositiveButton(ArgumentMatchers.anyInt(), ArgumentMatchers.any(DialogInterface.OnClickListener.class)))
.thenAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
// 獲取到第二個參數對象
DialogInterface.OnClickListener arg = (DialogInterface.OnClickListener) args[1];
// 直接執行回調
arg.onClick(null, 0);
return invocation.getMock();
}
});
Whitebox.invokeMethod(activity, "showExpiredDialog");
PowerMockito.verifyStatic(AppHelper.class);
AppHelper.makeMainIntent(ArgumentMatchers.any(Context.class));
}
當執行setPositiveButton()
時,會執行Answer
中的回調,此時會直接運行回調方法,然後在驗證對應的方法是否執行。
Whitebox.invokeMethod()
通常一些私有方法需要運行,自己寫反射還需要改一些東西。
Whitebox.invokeMethod(activity, "showExpiredDialog");