Android 单元测试实战(2)—— 基于`Powermock`的常用方法指南

2,644 阅读2分钟

上一篇中,基于调研和分析,决定使用Powermock完成单元测试的编写。

关于Powermock的使用方式,网上有很多的文章进行解释,下面仅仅介绍一些在Android上的常用姿势。

随着时间推移,该文章会不断完善。

Mock vs Spy

Powermock提供了mockspy两种方式,对于Activity的私有方法的调用验证通常需要做方法模拟。mockspy都可以实现,mock是默认对有方法都模拟。spy是默认对所有方法都不模拟。

个人建议是使用mock,因为activity里面的方法逻辑很多,而对于一个单元测试,我们往往只是测试一个方法,对其它方法都需要mock。用以验证调用或者模拟方法返回值等。

findViewById

activity中最不缺的就是控件查找,那么直接调用findViewById()肯定是会报错的Stub。那么通常的做法是mock一个activity,但是mock的方法的findViewById()返回值为null。

举例:验证activityonCreate中是否对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()

finalprivate字段的赋值。

 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()时,父类的AppCompatActivityonCreate()就不会再执行。

PowerMockito.whenNew()

如何验证一个页面跳转呢?

现在常用的方式是通过对于意图的验证,判断是否构造了对应包名的意图。

声明当new Intent()时,返回mockintent对象。

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

如何验证setPositiveButtonOnClickListener的回调逻辑中的内容是否正确。

  @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");