Android单元测试利器–Robolectric ShadowDemo

2016/07/07

本文原创作者:Cloud Chou. 欢迎转载,请注明出处和本文链接

Android单元测试系列文章的代码都可以在Github上找到: https://github.com/cloudchou/RobolectricDemo

Shadow测试Demo

其实Robolectric对Android API的支持都是通过为各个API类建立Shadow类实现支持的,比如SystemProperties类,在Robolectric框架中有一个对应的ShadowSystemProperties,在Shadow类中只需要实现想mock的方法即可,不需要实现原始类的所有方法,这样当调用Android API类的方法时,实际上是调用Shadow类中的方法,所以通过这种方式实现了对Android系统API的mock。我们来看一下ShadowSystemProperties的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
 * Shadow for {@link android.os.SystemProperties}.
 */
//下述注解表明是针对SystemProperties类的Shadow类,isInAndroidSdk表示原始类是否是在Android Sdk中暴露的,
//因为SystemProperities类实际上是一个隐藏类,所以这里isInAndroidSdk=false
@Implements(value = SystemProperties.class, isInAndroidSdk = false)
public class ShadowSystemProperties {
  private static final Map<String, Object> VALUES = new HashMap<>();
  private static final Set<String> alreadyWarned = new HashSet<>();

  static {
    VALUES.put("ro.build.version.release", "2.2");
    VALUES.put("ro.build.version.incremental", "0");
    VALUES.put("ro.build.version.sdk", 8);
    VALUES.put("ro.build.date.utc", 1277708400000L);  // Jun 28, 2010
    VALUES.put("ro.debuggable", 0);
    VALUES.put("ro.secure", 1);
    VALUES.put("ro.product.cpu.abilist", "armeabi-v7a");
    VALUES.put("ro.product.cpu.abilist32", "armeabi-v7a,armeabi");
    VALUES.put("ro.product.cpu.abilist64", "armeabi-v7a,armeabi");
    VALUES.put("ro.build.fingerprint", "robolectric");
    VALUES.put("ro.build.version.all_codenames", "REL");
    VALUES.put("log.closeguard.Animation", false);
    // disable vsync for Choreographer
    VALUES.put("debug.choreographer.vsync", false); 
  }

  /** 实现对get方法的mock
  */
  @Implementation
  public static String get(String key) {
    Object o = VALUES.get(key);
    if (o == null) {
      warnUnknown(key);
      return "";
    }
    return o.toString();
  }

  @Implementation
  public static String get(String key, String def) {
    Object value = VALUES.get(key);
    return value == null ? def : value.toString();
  }

  /** 实现对getInt方法的mock
  */
  @Implementation
  public static int getInt(String key, int def) {
    Object value = VALUES.get(key);
    return value == null ? def : (Integer) value;
  }

  /** 实现对getLong方法的mock
  */
  @Implementation
  public static long getLong(String key, long def) {
    Object value = VALUES.get(key);
    return value == null ? def : (Long) value;
  }

  /** 实现对getBoolean方法的mock
  */
  @Implementation
  public static boolean getBoolean(String key, boolean def) {
    Object value = VALUES.get(key);
    return value == null ? def : (Boolean) value;
  }

  synchronized private static void warnUnknown(String key) {
    if (alreadyWarned.add(key)) {
      System.err.println("WARNING: no system properties value for " + key);
    }
  }
}

原始的SystemProperties类本来是从系统的属性服务里读取属性,Robolectric为了实现mock,将事先定义好的部分属性直接hardcode在代码里,应用获取系统属性时会调用到ShadowSystemProperties类的get方法,这时候就可以直接从内存里取值并返回给应用了。我们知道android.os.Build类的很多字段的值其实都是通过SystemProperties类的get方法获得的,比如Build.brand对应的属性key是ro.product.brand,而Build.MODEL对应的属性key是ro.product.model,但是在上述ShadowSystemProperties里并没有这些属性设置值,所以当我们读取Build.brand或者Build.model时得到的值都是unknown.

但是我们可以利用Roblectric创建自己的针对系统Api类的mock类,比如我们也创建一个CloudSystemProperties类,实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@Implements(value = SystemProperties.class, isInAndroidSdk = false)
public class CloudSystemProperties {
    private static final Map<String, Object> VALUES = new HashMap<>();
    private static final Set<String> alreadyWarned = new HashSet<>();

    static {
        VALUES.put("ro.build.version.release", "2.2");
        VALUES.put("ro.build.version.incremental", "0");
        VALUES.put("ro.build.version.sdk", 8);
        VALUES.put("ro.build.date.utc", 1277708400000L);  // Jun 28, 2010
        VALUES.put("ro.debuggable", 0);
        VALUES.put("ro.secure", 1);
        VALUES.put("ro.product.cpu.abilist", "armeabi-v7a");
        VALUES.put("ro.product.cpu.abilist32", "armeabi-v7a,armeabi");
        VALUES.put("ro.product.cpu.abilist64", "armeabi-v7a,armeabi");
        VALUES.put("ro.build.fingerprint", "robolectric");
        VALUES.put("ro.build.version.all_codenames", "REL");
        VALUES.put("log.closeguard.Animation", false);
        // disable vsync for Choreographer 
        VALUES.put("debug.choreographer.vsync", false); 
        //添加了如下属性
        VALUES.put("persist.radio.multisim.config", "DSDS"); 
        VALUES.put("ro.product.device", "GT-I9100G");
        VALUES.put("ro.product.board", "t1");
        VALUES.put("ro.build.product", "GT-I9100G");
        VALUES.put("ro.product.brand", "samsung");
        VALUES.put("ro.product.model", "GT-I9100G");
        VALUES.put("ro.build.fingerprint", 
        "samsung/GT-I9100G/GT-I9100G:4.1.2/JZO54K/I9100GXXLSR:user/release-keys");
    }

    @Implementation
    public static String get(String key) {
        Object o = VALUES.get(key);
        if (o == null) {
            warnUnknown(key);
            return "";
        }
        return o.toString();
    }

    @Implementation
    public static String get(String key, String def) {
        Object value = VALUES.get(key);
        return value == null ? def : value.toString();
    }

    @Implementation
    public static int getInt(String key, int def) {
        Object value = VALUES.get(key);
        return value == null ? def : (Integer) value;
    }

    @Implementation
    public static long getLong(String key, long def) {
        Object value = VALUES.get(key);
        return value == null ? def : (Long) value;
    }

    @Implementation
    public static boolean getBoolean(String key, boolean def) {
        Object value = VALUES.get(key);
        return value == null ? def : (Boolean) value;
    }

    synchronized private static void warnUnknown(String key) {
        if (alreadyWarned.add(key)) {
            System.err.println("WARNING: no system properties value for " + key);
        }
    }
}

这样我们就可以获取Build.brand和Build.model的值了,在编写测试的时候指定Shadow类即可测试了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
public class ShadowTest {

  //指定使用Roblectric框架的ShadowSystemProperties来对SystemProperties类mock
    @Config(shadows = {ShadowSystemProperties.class})
    @Test
    public void testEvn() throws IllegalAccessException, NoSuchFieldException {
        System.out.println(Build.VERSION.RELEASE);
        Context ctx = RuntimeEnvironment.application;
        System.out.println(Build.VERSION.SDK_INT);
        System.out.println(Build.DEVICE);
        System.out.println(Build.FINGERPRINT);
        System.out.println(Build.BRAND);
        System.out.println(Build.BOARD);
        System.out.println(Build.MODEL); 
    }

   //指定使用Roblectric框架的CloudSystemProperties来对SystemProperties类mock
    @Config(shadows = {CloudSystemProperties.class})
    @Test
    public void testEvn2() throws IllegalAccessException, NoSuchFieldException {
        System.out.println(Build.VERSION.RELEASE);
        Context ctx = RuntimeEnvironment.application;
        System.out.println(Build.VERSION.SDK_INT);
        System.out.println(Build.DEVICE);
        System.out.println(Build.FINGERPRINT);
        System.out.println(Build.BRAND);
        System.out.println(Build.BOARD);
        System.out.println(Build.MODEL); 
    }

}

testEvn执行的测试结果如下所示:

ShadowSysProp1

testEvn2执行的测试结果如下所示:

ShadowSysProp2

可以看出来CloudSystemProperties对SystemProperties类的mock是有效的.

加载代码中的资源文件Demo

我们还可以将某个机型的system/build.prop文件作为资源保存在代码里,然后实现SystemProperties的mock时,直接从该该资源文件里读取更加方便,这样可以实现对某个机型的属性的模拟。

测试代码如下所示,CloudSystemProperties2实现另一个对SystemProperites的Shadow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Implements(value = SystemProperties.class, isInAndroidSdk = false)
public class CloudSystemProperties2 {

    @Implementation
    public static String get(String key) {
        Properties prop = new Properties();
        String name = "i9100g.properties";
        InputStream is = CloudSystemProperties2.class.getClassLoader().getResourceAsStream(name);
        try {
            prop.load(is);
            return prop.getProperty(key);
        } catch (IOException e) {
            return null;
        } finally {
            try {
                is.close();
            } catch (IOException e) {
            }
        }
    }

    @Implementation
    public static String get(String key, String def) {
        Object value = get(key);
        return value == null ? def : value.toString();
    }

    @Implementation
    public static int getInt(String key, int def) {
        String value = get(key);
        return value == null ? def : Integer.parseInt(value);
    }

    @Implementation
    public static long getLong(String key, long def) {
        String value = get(key);
        return value == null ? def : (Long) Long.parseLong(value);
    }

    @Implementation
    public static boolean getBoolean(String key, boolean def) {
        String value = get(key);
        return value == null ? def : Boolean.parseBoolean(value);
    }

}

i9100g.properties内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ro.build.version.release=2.2
ro.build.version.incremental=0
ro.build.version.sdk=8
ro.build.date.utc=1277708400000
ro.debuggable=0
ro.secure=1
ro.product.cpu.abilist=armeabi-v7a
ro.product.cpu.abilist32=armeabi-v7a,armeabi
ro.product.cpu.abilist64=armeabi-v7a,armeabi
ro.build.fingerprint=robolectric
ro.build.version.all_codenames=REL
log.closeguard.Animation=false
debug.choreographer.vsync=false
debug.choreographer.vsync=false
persist.radio.multisim.config=DSDS
ro.product.device=GT-I9100G
ro.product.board=t1
ro.build.product=GT-I9100G
ro.product.brand=samsung
ro.product.model=GT-I9100G
ro.build.fingerprint=samsung/GT-I9100G/GT-I9100G:4.1.2/JZO54K/I9100GXXLSR:user/release-keys

测试用例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
**
 * Created by Cloud on 2016/6/27.
 */
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
public class ShadowTest {
    // ...
    @Config(shadows = {CloudSystemProperties2.class})
    @Test
    public void testShadow2() throws IllegalAccessException, NoSuchFieldException {
        System.out.println(Build.VERSION.RELEASE);
        System.out.println(Build.VERSION.SDK_INT);
        System.out.println(Build.DEVICE);
        System.out.println(Build.FINGERPRINT);
        System.out.println(Build.BRAND);
        System.out.println(Build.BOARD);
        System.out.println(Build.MODEL);
    }

}

如果仅仅是这样做,是不够的,在AndroidStudio中执行这个单元测试,会在运行getClassLoader().getResourceAsStream(name)时因为找不到资源而抛出NullPointerException,如下所示:

resource-exceptions

出现该问题的根本原因是getResourceAsStream函数在查找资源时,默认是在编译生成的类所在的文件夹里进行查找,而不是在源代码文件所在的文件夹查找,所以我们需要将i9100g.prop文件默认就拷贝到CloudSystemProperties生成的类文件所在的目录中,因此我们需要修改build.gradle,让它在执行测试前就将i9100g自动拷贝到它需要在的位置。 修改后的build.gradle如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
android{
  //...
  testOptions {
        unitTests.all {
            beforeTest {
                def testClassDir = buildDir.getAbsolutePath() + "/intermediates/classes/test/debug"
                copy {
                    from(android.sourceSets.test.java.srcDirs) {
                        exclude "**/*.java"
                    }
                    into(testClassDir)
                }
            }
        }
    }	
  //...
}

在代码文件里直接执行testShadow2之前,需先通过gralde执行testDebugUnitTest目标,否则也不会自动拷贝i9100g.prop文件到它需要的位置,执行这个命令之后就可以在代码中直接运行testShadow2测试了,通过gralde执行testDebugUnitTest目标可在命令行下执行如下命令:

1
gradlew.bat :app:testDebugUnitTest

执行结果如下所示:

resource_load_result

¥打赏5毛

取消

感谢您的支持,我会继续努力的!

扫码支持
赏个5毛,支持我把

打开支付宝扫一扫,即可进行扫码打赏哦

本篇目录