Android 单元测试入门

Unit Test

Start

简单了解一下,如何依赖 Junit 进行 Java 代码的单元测试。依旧如何借助 Robolectric 进行 Android 方面的单元测试,主要是 Context 的获取。最后就网络请求的单元测试,简单叙述一下。

JUnit

  • 依赖
1
testImplementation 'junit:junit:4.12'
  • example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

public class Tools {
private static final String TAG = "Tools";

public static String getCurrentTime() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss", Locale.CHINA);
Date curDate = new Date(System.currentTimeMillis());
return simpleDateFormat.format(curDate);
}

public static String getCurrentTime(long tempStap) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年", Locale.CHINA);
Date curDate = new Date(tempStap);
return simpleDateFormat.format(curDate);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ToolsUnitTest {

@BeforeClass
public static void setUp() {
System.out.println( ToolsUnitTest.class.getSimpleName()+ "=====单元测试开始");
}

@AfterClass
public static void end() {
System.out.println( ToolsUnitTest.class.getSimpleName()+ "=====单元测结束");
}

@Test(expected = NullPointerException.class)
public void getCurrentTimeTest() {
assertNotEquals("1111",Tools.getCurrentTime());
// 起码在 2019 年,这条测试是可以通过的
assertEquals("2019年",Tools.getCurrentTime(System.currentTimeMillis()));
}
}

执行顺序: @BeforeClass –> @Before –> @Test –> @After –> @AfterClass

  • 可用 API

都是 Assert 的静态方法,对有返回值的方法,用断言非常好用,你甚至可以测试异常

  • 高级用法
    • @RunWith(Parameterized.class) 参数化
    • assertThat用法
    • @Rule用法
  • 问题来了
1
public static String getAppVersion(Context mContext) { ... }

Android 中的 context 怎么搞 ?

Robolectric

  • 配置

    • 依赖
      1
      2
      3
      4
      testImplementation 'androidx.test:core:1.2.0'
      testImplementation 'androidx.test:rules:1.2.0'
      testImplementation 'androidx.test.espresso:espresso-core:3.2.0'
      testImplementation 'org.robolectric:robolectric:4.3'
    • 允许 robolectric 读取 assets、resources 和 manifests,在 build.gradle 中添加

    一定要添加以下配置,否则将导致单元测试运行异常

    一定要添加以下配置,否则将导致单元测试运行异常

    一定要添加以下配置,否则将导致单元测试运行异常

    1
    2
    3
    4
    5
    testOptions {
    unitTests {
    includeAndroidResources = true
    }
    }
    • 在 gradle.properties 中添加

      1
      android.enableUnitTestBinaryResources=true
  • example

    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

    @RunWith(RobolectricTestRunner::class)
    @Config(sdk = [27])
    class RobolectricUnitTest {

    @Test
    fun assertContext() {
    val context = ApplicationProvider.getApplicationContext<Context>()

    val version = AppUtils.getAppVersion(context)
    // 单元测试,也可以打印日志
    println("version ==$version")

    assertEquals("1.0", version)
    }

    companion object {

    @BeforeClass
    fun setup() {
    // for Fresco
    SoLoader.setInTestMode()
    }
    }
    }

    单元测试也可以用 Kotlin 写,😬😬😬

Context 是什么 ?

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
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 28, manifest = Config.NONE)
public class ContextTest {

@Test
public void useContextTest() throws Utils.SystemUtilsException {
Context context = ApplicationProvider.getApplicationContext();

System.out.println("context ===" + context.getClass().getName());

assertEquals(Constants.PACKAGE_NAME, Utils.getPackageName(context));
assertEquals(Constants.PACKAGE_VERSION, Utils.getPackageVersionName(context));
}

@Test
public void screenInfoTest() {
Context context = ApplicationProvider.getApplicationContext();
Resources resources = context.getResources();
DisplayMetrics displayMetrics = resources.getDisplayMetrics();

float density = displayMetrics.density;
float width = displayMetrics.widthPixels;
float height = displayMetrics.heightPixels;

System.out.println("density==" + density);
System.out.println("width ==" + width);
System.out.println("height ==" + height);
}
}

-output

1
2
3
density==1.0
width ==320.0
height ==470.0
  • 可能遇到的问题

    Application 中某些方法无法被初始化,比如 Fresco,详见

    GitHub issue

    Fresco issue

    及解决方案。

Robolectric 很强大,可以在不使用真机及模拟器的情况下,进行 UI 测试

Robolectric 可以认为是一个虚拟的模拟器

网络测试

网络测试有以下困难

  • 请求结果是异步返回的,无法直接进行断言,需要进行同步转换
  • 后端会做 cookie 和 headers 的校验,Robolectric 只是模拟器,请求时没有这些信息。
    对 okhttp 添加 intercept ,拦截返回值,进行任意返回结果,测试后续逻辑。
  • simple
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void netTest() {

final Retrofit mRetrofit = initRetrofit();

GankApi mGankApi = mRetrofit.create(GankApi.class);
Observable<GankAndroid> mAndroidObservable = mGankApi.getData("10/1");
mAndroidObservable.subscribe(gankAndroid -> {
doAssert(gankAndroid.getResults().get(0));
}, throwable -> System.out.println("fail"));
}

private void doAssert(GankAndroid.ResultsEntity entity) {
assertEquals("5d423ff19d2122031ea52264", entity.get_id());
assertEquals("web", entity.getSource());
assertEquals("Android", entity.getType());
assertEquals("潇湘剑雨", entity.getWho());
}

这个就可以对网络请求回来的数据,进行各种业务逻辑的测试了。

但是也可以添加 interceptor ,创建特定的返回结果

  • 添加 Interceptor 拦截返回结果,方便测试后续逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    @Test
public void netWithInterceptTest() {
String json = "404";
SimpleIntercept errorIntercept = new SimpleIntercept(json, 404);

// SimpleIntercept successIntercept = new SimpleIntercept(json, HTTP_OK);

final Retrofit mRetrofit = initRetrofit(errorIntercept);

GankApi mGankApi = mRetrofit.create(GankApi.class);
Observable<GankAndroid> mAndroidObservable = mGankApi.getData("10/1");
mAndroidObservable.subscribe(gankAndroid -> {
},
throwable -> {
// for example
assertEquals("HTTP 404 404", throwable.getMessage());
});
}
SimpleIntercept
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
public class SimpleIntercept implements Interceptor {

private static final String APPLICATION_JSON = "application/json";

private String expectedResult;
private int code;

public SimpleIntercept(@NonNull String expectedResult, int code) {
this.expectedResult = expectedResult;
this.code = code;
}

@Override
public Response intercept(Chain chain) {
Response response = new Response.Builder()
.code(code)
.message(expectedResult)
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.body(ResponseBody.create(MediaType.parse(APPLICATION_JSON), expectedResult))
.addHeader("content-type", APPLICATION_JSON)
.build();
return response;
}
}

参考资料

JUnit 入门

Android单元测试(四):Robolectric框架的使用

加个鸡腿呗