UiAutomation injectInputEvent注入屏幕点击事件

//=========================================================================
//==                        Utility Methods                             ===
//=========================================================================
/**
 * 同步事件注入只可在测试线程中调用,不可在被测试者的主线程中调用,否则会导致被测试主线程阻塞
 * Helper method injects a click event at a point on the active screen via the UiAutomation object.
 * @param x the x position on the screen to inject the click event
 * @param y the y position on the screen to inject the click event
 * @param automation a UiAutomation object received through the current Instrumentation
 */
@WorkerThread
static void injectClickEvent(float x, float y, UiAutomation automation){
    //A MotionEvent is a type of InputEvent.  
    //The event time must be the current uptime.
    final long eventTime = SystemClock.uptimeMillis();

    //A typical click event triggered by a user click on the touchscreen creates two MotionEvents,
    //first one with the action KeyEvent.ACTION_DOWN and the 2nd with the action KeyEvent.ACTION_UP
    MotionEvent motionDown = MotionEvent.obtain(eventTime, eventTime, KeyEvent.ACTION_DOWN,
            x,  y, 0); 
    //We must set the source of the MotionEvent or the click doesn't work.
    motionDown.setSource(InputDevice.SOURCE_TOUCHSCREEN);
    automation.injectInputEvent(motionDown, true);
    MotionEvent motionUp = MotionEvent.obtain(eventTime, eventTime, KeyEvent.ACTION_UP,
            x, y, 0);
    motionUp.setSource(InputDevice.SOURCE_TOUCHSCREEN);
    automation.injectInputEvent(motionUp, true);
    //Recycle our events back to the system pool.
    motionUp.recycle();
    motionDown.recycle();
}

注意:injectInputEvent 的第二个参数 sync 当被设置为 true 的时候,只可在测试框架的子线程中调用,不可在被测试者的主线程(UI线程)中调用,否则会导致被测试UI线程阻塞。原因是调用之后需要在另一个线程中等待执行结果。

参考链接


How to inject click event with Android UiAutomation.injectInputEvent

Jdk 6260652 Bug

最近在看JDK的源码:CopyOnWriteArrayList.javaArrayList.java,这2个类的构造函数,注释中有一句话看不懂。

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
 }

 public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements = c.toArray();
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elements.getClass() != Object[].class)
        elements = Arrays.copyOf(elements, elements.length, Object[].class);
    setArray(elements);
 }

上网查了一下资料,才知道see 6260652 这个编号代表JDK bug库中的编号。可以去官网查看bug详情

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6515694

6260652 和6515694这2个bug,貌似是同一个问题。这个bug是什么意思呢?我们先来看看一些测试代码: 

public static void test1()
{
    SubClass[] subArray = {new SubClass(), new SubClass()};
    System.out.println(subArray.getClass());

    // class [Lcollection.SubClass;
    BaseClass[] baseArray = subArray;
    System.out.println(baseArray.getClass());

    // java.lang.ArrayStoreException
    baseArray[0] = new BaseClass();
}

public static void test2()
{
    List<String> list = Arrays.asList("abc");

    // class java.util.Arrays$ArrayList
    System.out.println(list.getClass());

    // class [Ljava.lang.String;
    Object[] objArray = list.toArray();
    System.out.println(objArray.getClass());

    objArray[0] = new Object(); // cause ArrayStoreException
}

public static void test3()
{
    List<String> dataList = new ArrayList<String>();
    dataList.add("one");
    dataList.add("two");

    Object[] listToArray = dataList.toArray();

    // class [Ljava.lang.Object;返回的是Object数组
    System.out.println(listToArray.getClass());
    listToArray[0] = "";
    listToArray[0] = 123;
    listToArray[0] = new Object();

}

 1、关于test1()

        SubClass 继承自BaseClass,由于SubClass数组中每一个元素都是SubClass对象,所以

BaseClass[] baseArray = subArray;

这种强制类型转换不会报错。这其实就是java对象的向上转型,子类数组转换成父类数组是允许的。但是由于数组中元素类型都是SubClass类型的,所以

baseArray[0] = new BaseClass();

会报错

java.lang.ArrayStoreException

这也就是说假如我们有1个Object[]数组,并不代表着我们可以将Object对象存进去,这取决于数组中元素实际的类型。

2、关于test2()

List<String> list = Arrays.asList("abc");

需要注意,可以知道返回的实际类型是

java.util.Arrays$ArrayList

而不是

ArrayList

我们调用

Object[] objArray = list.toArray();

返回是String[]数组,所以我们不能将Object对象,放到objArray数组中。

3、关于test3()

ArrayList对象的toArray()返回就是Object[]数组,所以我们可以将任意对象存放到返回的Object[]数组中。

通过test2和test3可以看出,如果我们有1个

List<String> stringList

对象,当我么调用

Object[] objectArray = stringList.toArray();

的时候,objectArray 并不一定能够放置Object对象。这就是源码中的注释:

//c.toArray might (incorrectly) not return Object[] (see 6260652)

为了考虑这种情况,所以源码中进行了if判断,来防止错误的数组对象导致异常。

Arrays.copyOf(elementData, size, Object[].class);

这个方法就是用来创建1个Object[]数组,这样数组中就可以存放任意对象了。

参考链接


Jdk 6260652 Bug

Android跳转权限设置页面

最近项目上有个需求,读取通讯录。当用户点了拒绝访问通讯录或者其他权限,导致无法使用,这时候我想重新打开权限设置,但是对于很多小白用户不知道怎么设置,这就会导致用户体验不友好的一面。

之前已经有人写过类似的文章,不过都比较分散,经实测将这些方法总结了一下。

要跳转的权限设置界面如图:目前手上只有华为和小米作为测试

继续阅读Android跳转权限设置页面

国产手机Android R(Android 11)系统执行Espresso UI(AndroidX)遇到的问题总结

最近在 Android R(Android 11)系统上执行以前写的Espresso UI(AndroidX)单元测试用例的时候,发现在 联想拯救者电竞手机Pro(128GB) 系统版本 12.5.225上,如果测试用例申请了通讯录写入权限,如下:

<uses-permission android:name="android.permission.WRITE_CONTACTS" />

那么测试用例执行的时候,会报告如下错误:

Test running failed:Process crashed.

详细的logcat日志如下:

D/Zygote: Forked child process 10664
I/ActivityManager: Start proc 10664:xx.test/u0a274 for added application xx.test
I/AutoRun: AutoRunServices: ProcessStateHandler: sendKillAppRequest: [uid = 10274, pid = 10664, package name = xx.test mOtherPids = null, mStartupTime = 0, mDelay = 0, mCount = 0]
I/adbd: jdwp connection from 10664
D/ApplicationLoaders: Returning zygote-cached class loader: /system/framework/android.test.base.jar
D/ApplicationLoaders: Returning zygote-cached class loader: /system/framework/android.test.base.jar
I/yboard.biz.tes: The ClassLoaderContext is a special shared library.
D/ApplicationLoaders: Returning zygote-cached class loader: /system/framework/android.test.base.jar
D/ApplicationLoaders: Returning zygote-cached class loader: /system/framework/android.test.base.jar
I/Perf: Connecting to perf service.
D/NetworkSecurityConfig: No Network Security Config specified, using platform default
D/NetworkSecurityConfig: No Network Security Config specified, using platform default
I/MonitoringInstr: Instrumentation started!
I/AndroidJUnitRunner: Waiting for debugger to connect...
I/System.out: Sending WAIT chunk
I/ActivityManager: Killing 10664:xx.test/u0a274 (adj 0): permissions revoked
I/Zygote: Process 10664 exited due to signal 9 (Killed)
I/libprocessgroup: Successfully killed process cgroup uid 10274 pid 10664 in 44ms

目前测试发现,即使通过GrantPermissionRule授予权限,也是没用的,代码根本来不及执行,应用就一下被系统杀死了。

目前只能是不测试这部分功能。

@Rule
public final GrantPermissionRule rule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_CONTACTS);

OPPO/VIVO/小米等手机上,需要手工开启 后台弹出界面权限 才能执行单元测试。有个思路是通过UI Automator来自动开启这个权限,暂时还需要测试一下。

参考链接


Android R(11) ContentResolver报错java.lang.IllegalArgumentException: Invalid token limit

Android 11执行如下代码:

/**
 * 保存拍照的图片是否已经完成
 *
 * @param context Context 对象
 * @return 查询成功返回 true ,否则返回false
 */
private static boolean isSavePictureComplete(@NonNull final Context context, long takeTime) {
    //扫描图片
    final Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    final ContentResolver resolver = context.getContentResolver();
    final Cursor cursor = resolver.query(uri, new String[]{
            MediaStore.Images.Media.DATE_ADDED, 
            MediaStore.Images.Media._ID, 
            MediaStore.Images.Media.SIZE },
            MediaStore.MediaColumns.SIZE + ">0",
            null,
            MediaStore.Files.FileColumns._ID + " DESC limit 1 offset 0");

    //读取扫描到的图片
    if ((null != cursor) && (cursor.getCount() > 0) && cursor.moveToFirst()) {
        //获取图片时间
        long time = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED));
        int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));
        if (String.valueOf(time).length() < 13) {
            time *= 1000;
        }
        cursor.close();
        // 如果照片的插入时间大于相机的拍照时间,就认为是拍照图片已插入
        return time + 1000 < takeTime; 
    }
    return true;
}

会出现如下错误:

java.lang.IllegalArgumentException: Invalid token limit
        at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:172)
        at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:142)
        at android.content.ContentProviderProxy.query(ContentProviderNative.java:472)
        at android.content.ContentResolver.query(ContentResolver.java:1197)
        at android.content.ContentResolver.query(ContentResolver.java:1128)
        at android.content.ContentResolver.query(ContentResolver.java:1084)

原因为 Android 11 上使用 ContentResolver 的时候,已经不允许进行 SQL语句拼接,主要是为了防止 SQL注入攻击。

代码参考如下:

/**
 * 保存拍照的图片是否已经完成
 *
 * @param context Context 对象
 * @return 查询成功返回 true ,否则返回false
 */
private static boolean isSavePictureComplete(@NonNull final Context context, long takeTime) {
    final Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    final String[] columns = new String[]{
                MediaStore.Images.Media.DATE_ADDED,
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.SIZE};
    //扫描图片
    final ContentResolver contentResolver = context.getContentResolver();
    Cursor cursor = null;
    try {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q/*29*/) {
            final Bundle bundle = new Bundle();
            bundle.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, MediaStore.MediaColumns.SIZE + " > ?");
            bundle.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, new String[]{"0"});
            bundle.putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, new String[]{MediaStore.Files.FileColumns._ID});
            bundle.putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING);
            bundle.putInt(ContentResolver.QUERY_ARG_LIMIT, 1);
            bundle.putInt(ContentResolver.QUERY_ARG_OFFSET, 0);
            cursor = contentResolver.query(uri, columns,
                    bundle, 
                    null);
        } else {
            cursor = contentResolver.query(uri, columns,
                    MediaStore.MediaColumns.SIZE + ">0",
                    null,
                    MediaStore.Files.FileColumns._ID + " DESC limit 1 offset 0");
        }

        //读取扫描到的图片
        if ((null != cursor) && (cursor.getCount() > 0) && cursor.moveToFirst()) {
            //获取图片时间
            long time = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED));
            if (String.valueOf(time).length() < 13) {
                time *= 1000;
            }
            // 如果照片的插入时间大于相机的拍照时间,就认为是拍照图片已插入
            return time + 1000 < takeTime;
        }
    } finally {
        if (null != cursor) {
            cursor.close();
        }
    }
    return true;
}

参考链接


limiting number of rows in a ContentResolver.query() function

Robolectric/PowerMock测试AIDL相关类

在编写单元测试用例的时候,如果被测试类是与AIDL相关的类,不管是直接调用,还是被测试类继承了AIDL生成的类,由于AIDL类是在编译期间动态生成的,并且每次构建都会重新生成一次(类名相同,但是Hash可能会变,文件修改日期必然变更)。

然而,由于PowerMock调用Objenesis库为了加快编译速度,会缓存同名的类,这样导致在运行时进行类型转换的时候无法通过校验,报告如下错误:

org.mockito.exceptions.base.MockitoException: 
    ClassCastException occurred while creating the mockito proxy :
      class to mock : 'com.squareup.otto.Bus', loaded by classloader : 'org.robolectric.internal.bytecode.InstrumentingClassLoader@1593948d'
      created class : 'com.squareup.otto.Bus$$EnhancerByMockitoWithCGLIB$$82a3b196', loaded by classloader : 'org.robolectric.internal.bytecode.InstrumentingClassLoader@1593948d'
      proxy instance class : 'com.squareup.otto.Bus$$EnhancerByMockitoWithCGLIB$$82a3b196', loaded by classloader : 'org.mockito.internal.creation.util.SearchingClassLoader@618ff5c2'
      instance creation by : ObjenesisInstantiator

    You might experience classloading issues, disabling the Objenesis cache *might* help (see MockitoConfiguration)
        at org.powermock.api.mockito.repackaged.ClassImposterizer.imposterise(ClassImposterizer.java:61)
        at org.powermock.api.mockito.repackaged.ClassImposterizer.imposterise(ClassImposterizer.java:49)
        at org.powermock.api.mockito.repackaged.CglibMockMaker.createMock(CglibMockMaker.java:24)
        at org.powermock.api.mockito.internal.mockmaker.PowerMockMaker.createMock(PowerMockMaker.java:45)
        at com.acme.android.myapp.services.gcm.handlers.RequestLogoutHandlerTest.setup(RequestLogoutHandlerTest.java:39)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
        at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:24)
        at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:251)
        at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:188)
        at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:54)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:152)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at org.junit.runners.Suite.runChild(Suite.java:128)
        at org.junit.runners.Suite.runChild(Suite.java:27)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
        at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78)
        at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212)
        at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
    Caused by: java.lang.ClassCastException: Cannot cast com.squareup.otto.Bus$$EnhancerByMockitoWithCGLIB$$82a3b196 to com.squareup.otto.Bus
        at java.lang.Class.cast(Class.java:3369)
        at org.powermock.api.mockito.repackaged.ClassImposterizer.imposterise(ClassImposterizer.java:59)
        at org.powermock.api.mockito.repackaged.ClassImposterizer.imposterise(ClassImposterizer.java:49)
        at org.powermock.api.mockito.repackaged.CglibMockMaker.createMock(CglibMockMaker.java:24)
        at org.powermock.api.mockito.internal.mockmaker.PowerMockMaker.createMock(PowerMockMaker.java:45)
        at org.mockito.internal.util.MockUtil.createMock(MockUtil.java:33)
        at org.mockito.internal.MockitoCore.mock(MockitoCore.java:59)
        at org.mockito.Mockito.mock(Mockito.java:1285)
        at org.mockito.Mockito.mock(Mockito.java:1163)
        ... 36 more


    org.mockito.exceptions.base.MockitoException: 
    ClassCastException occurred while creating the mockito proxy :
      class to mock : 'com.squareup.otto.Bus', loaded by classloader : 'org.robolectric.internal.bytecode.InstrumentingClassLoader@1593948d'
      created class : 'com.squareup.otto.Bus$$EnhancerByMockitoWithCGLIB$$82a3b196', loaded by classloader : 'org.robolectric.internal.bytecode.InstrumentingClassLoader@1593948d'
      proxy instance class : 'com.squareup.otto.Bus$$EnhancerByMockitoWithCGLIB$$82a3b196', loaded by classloader : 'org.mockito.internal.creation.util.SearchingClassLoader@618ff5c2'
      instance creation by : ObjenesisInstantiator

    You might experience classloading issues, disabling the Objenesis cache *might* help (see MockitoConfiguration)
        at org.powermock.api.mockito.repackaged.ClassImposterizer.imposterise(ClassImposterizer.java:61)
        at org.powermock.api.mockito.repackaged.ClassImposterizer.imposterise(ClassImposterizer.java:49)
        at org.powermock.api.mockito.repackaged.CglibMockMaker.createMock(CglibMockMaker.java:24)
        at org.powermock.api.mockito.internal.mockmaker.PowerMockMaker.createMock(PowerMockMaker.java:45)
        at com.acme.android.myapp.services.gcm.handlers.RequestLogoutHandlerTest.setup(RequestLogoutHandlerTest.java:39)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
        at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:24)
        at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:251)
        at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:188)
        at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:54)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:152)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at org.junit.runners.Suite.runChild(Suite.java:128)
        at org.junit.runners.Suite.runChild(Suite.java:27)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
        at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78)
        at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212)
        at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:6

解决方法就是阻止Objenesis的缓存功能。在测试用例的根目录下新建文件,如下:

package org.mockito.configuration;

public class MockitoConfiguration extends DefaultMockitoConfiguration {

    @Override
    public boolean enableClassCache() {
        return false;
    }
}

参考链接


Robolectric 3.8/PowerMock无法Mock由Executors线程运行的类

参照 Android Studio 3.6.3/4.0/4.1/4.2配置Robolectric-3.8/4.3.1/4.5.1/4.6.1 Powermock-1.6.6单元测试环境 进行单元测试,不过,由于工程的限制(不能依赖 AndroidX),我们只能在 Robolectric 3.8

Android Studio 4.1.3,JDK使用 Java 1.8

在执行如下测试用例的时候,发现当被测试代码使用 Executors 在子线程执行的时候,如果使用 PowermockWhenNew 对被测试对象进行仿真的时候,无法正确的被 Mock

被测试代码如下:

import android.support.annotation.NonNull;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CustomExecutor {
    private final static String TAG = "CustomExecutor";
    private static volatile CustomExecutor instance;
    private CustomMocker custom;
    private final ExecutorService executor = Executors.newSingleThreadExecutor();

    private CustomExecutor() {
        executor.submit(new Runnable() {
            @Override
            public void run() {
                custom = new CustomMocker();
            }
        });
    }

    @NonNull
    public static CustomExecutor getInstance() {
        if (null == instance) {
            synchronized (CustomExecutor.class) {
                if (null == instance) {
                    instance = new CustomExecutor();
                }
            }
        }
        return instance;
    }

    public String getMessage() throws Exception {
        final Future<String> future = executor.submit(new Callable<String>() {
            @Override
            public String call() {
                return custom.getMessage();
            }
        });
        return future.get();
    }
}
public class CustomMocker {
    public String getMessage() {
        return "I am not mocked";
    }
}

单元测试代码如下:

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;


@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
@PrepareForTest({CustomMocker.class, CustomExecutor.class})
@Config(manifest = Config.NONE, constants = BuildConfig.class, sdk = 26)
public class CustomExecutorTest {
    @Rule
    public final PowerMockRule rule = new PowerMockRule();

    @Before
    public void setup() throws Exception {
        final CustomMocker clz = PowerMockito.mock(CustomMocker.class);
        PowerMockito.when(clz.getMessage()).thenReturn("I am mocked!");
        PowerMockito.whenNew(CustomMocker.class).withAnyArguments().thenReturn(clz);
    }

    @Test
    public void testCustomExecutors() {
        final CustomExecutor executor = CustomExecutor.getInstance();
        Assert.assertNotNull(executor);
        Assert.assertEquals(expStr, executor.getMessage());
    }
}

上述的测试代码在执行的时候,完全不会生效。

网上查询了一下,根据 Unable to get mocked instance of Executor in separate class 的介绍,我们需要在测试用例里面的`@PrepareForTest`里面增加 `Executors.class`,于是修改测试用例为如下:

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.modules.junit4.PowerMockRunnerDelegate;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(RobolectricTestRunner.class)
@PowerMockIgnore({"jdk.internal.reflect.*", "org.apache.commons.*", "org.mockito.*", "org.robolectric.*", "android.*"})
@PrepareForTest({ExecutorService.class, Executors.class, CustomMocker.class, CustomExecutor.class})
@Config(sdk = 26)
public class CustomExecutorTest {
    private final static String expStr = "I am mocked!";
    @Rule
    public final PowerMockRule rule = new PowerMockRule();

    @Before
    public void setup() throws Exception {
        PowerMockito.mockStatic(Executors.class);
        PowerMockito.mockStatic(ExecutorService.class);
        PowerMockito.when(Executors.newSingleThreadExecutor()).thenCallRealMethod();

        PowerMockito.mockStatic(CustomExecutor.class);
        final CustomMocker clz = PowerMockito.mock(CustomMocker.class);
        PowerMockito.when(clz.getMessage()).thenReturn(expStr);
        PowerMockito.whenNew(CustomMocker.class).withAnyArguments().thenReturn(clz);

        PowerMockito.mockStatic(CustomExecutor.class);
        PowerMockito.when(CustomExecutor.getInstance()).thenCallRealMethod();
    }

    @Test
    public void testExecutorsMock() throws Exception {
        final CustomExecutor executor = CustomExecutor.getInstance();
        Assert.assertNotNull(executor);
        Assert.assertEquals(expStr, executor.getMessage());
    }
}

结果出现一个诡异的现象,如果在创建对象的地方设置断点进行调试跟踪,就是正确的,去掉断点,就会失败。

这个问题卡住很久,结果突然想起以前写的一个差不多功能的被测试类,是能正常工作的,于是进行了代码对比,结果发现一个奇怪的现象,只要内部定义一个类来中继一下,就可以完美解决问题。于是修改被测试类,代码如下:

import android.support.annotation.NonNull;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CustomExecutor {
    private static volatile CustomExecutor instance;
    @NonNull
    private final ExecutorService executor = Executors.newSingleThreadExecutor();

    private Delegate delegate;

    private CustomExecutor() {
        executor.submit(new Runnable() {
            @Override
            public void run() {
                delegate = new Delegate();
            }
        });
    }

    @NonNull
    public static CustomExecutor getInstance() {
        if (null == instance) {
            synchronized (CustomExecutor.class) {
                if (null == instance) {
                    instance = new CustomExecutor();
                }
            }
        }
        return instance;
    }

    public String getMessage() throws Exception {
        final Future<String> future = executor.submit(new Callable<String>() {
            @Override
            public String call() {
                return delegate.getMessage();
            }
        });
        return future.get();
    }

    private static class Delegate {
        private final CustomMocker mocker;

        public Delegate() {
            mocker = new CustomMocker();
        }

        public String getMessage() {
            return mocker.getMessage();
        }
    }
}

也就是内部定义来一个代理子类,初始化的时候,通过内部子类进行初始化。

这个现象比较奇怪,目前能解决问题,具体原因还是不详。

参考链接


Android与JavaScript交互数据上限(Base64图片传输问题)

使用`webview.loadUrl` 给`JavaScript`传`Base64`格式的较大图片时,有的Android版本上不反馈任何错误信息,只是页面接收不到任何信息,传入的脚本不执行任何操作。

有的则报错,如下:

Refusing to load URL as it exceeds 2097152 characters

继续阅读Android与JavaScript交互数据上限(Base64图片传输问题)