Shadow
Robolectric 的本质是在 Java 运行环境下,采用 Shadow 的方式对 Android 中的组件进行模拟测试,从而实现Android 单元测试。对于一些 Robolectirc 暂不支持的组件,可以采用自定义 Shadow 的方式扩展 Robolectric 的功能。
Shadow是通过对真实的 Android 对象进行函数重载、初始化等方式对 Android 对象进行扩展,Shadow 出来的对象的功能接近 Android 对象,可以看成是对 Android 对象一种修复。自定义的 Shadow 需要在 config 中声明,声明写法是 @Config(shadows=ShadowPoint.class)。
@Implements(Point.class)
public class ShadowPoint {
@RealObject private Point realPoint;
...
public void __constructor__(int x, int y) {
realPoint.x = x;
realPoint.y = y;
}
}
上述实例中,@Implements 是声明 Shadow 的对象,@RealObject 是获取一个 Android 对象,**constructor **则是该 Shadow 的构造函数,Shadow 还可以修改一些函数的功能,只需要在重载该函数的时候添加@Implementation,这种方式可以有效扩展 Robolectric 的功能。
Mock写法介绍
对于一些依赖关系复杂的测试对象,可以采用Mock框架解除依赖,常用的有Mockito。
例如Mock一个List类型的对象实例,可以采用如下方式:
List list = mock(List.class); //mock得到一个对象,也可以用 @mock 注入一个对象
所得到的 list 对象实例便是 List 类型的实例,如果不采用 mock,List 其实只是个接口,我们需要构造或者借助ArrayList 才能进行实例化。与 Shadow 不同,Mock 构造的是一个虚拟的对象,用于解耦真实对象所需要的依赖。Mock得到的对象仅仅是具备测试对象的类型,并不是真实的对象,也就是并没有执行过真实对象的逻辑。
List list = mock(List.class); // 也可以使用 @Mock 注解注入一个对象
list.add(1);
verify(list).add(1); //验证这个函数的执行
verify(list,time(3)).add(1); //验证这个函数的执行次数
when(list.get(1)).thenReturn(10);//指定当执行了这个方法的时候,返回 thenReturn 的值
assertEquals(list[1], 10)
常用 Rule
-
ActivityTestRule
使用 ActivityTestRule,测试框架会在带有 @Test 注解的每个测试方法运行之前以及带有 @Before 注解的所有方法运行之前启动被测 Activity,并在测试完成并且带有 @After 注解的所有方法都运行后关闭该 Activity;也可以手动调用 ActivityTestRule.launchActivity() 和 ActivityTestRule.finishActivity()。
-
ActivityScenarioRule
作为 ActivityTestRule 的替代,在测试方法之前启动指定的 Activity,并在测试方法之后结束该 Activity。同时可以在测试方法中获得 ActivityScenario,可切换 Activity 的生命周期;ActivityScenario 是一种跨平台 API,可用于本地单元测试和设备端集成测试等;在真实或虚拟设备上,ActivityScenario 可提供线程安全,在测试的插桩线程和运行被测 Activity 的线程之间同步事件。
-
IntentsTestRule
是 ActivityTestRule 的子类,IntentsTestRule 类会在每次测试前初始化 Espresso Intents。
-
ServiceTestRule
调用 ServiceTestRule.startService() 或者 ServiceTestRule.bindService() 在测试方法中建立Service连接,在测试结束后会自动关闭 Service。不适用于 IntentService,可以对其他 Service 进行测试。
-
GrantPermissionRule
帮助在 Android API 23及以上的环境申请运行时权限。申请权限时可以避免用户交互弹窗占用UI测试焦点。最终会调用 PermissionRequester.requestPermissions() 方法,通过执行UiAutomationShellCommand 直接在 shell 中为当前 target 申请权限。
Intent
-
启动 Espresso Intents
-
使用 ActivityTestRule 启动需要手动在测试方法运行先初始化 Espresso Intents;
@Rule public ActivityTestRule activityTestRule = new ActivityTestRule<>(MainActivity.class); @Before public void setUp() { Intents.init(); } @After public void release() { Intents.release(); } -
使用 IntentsTestRule 启动,框架会自动初始化 Espresso Intents
@Rule public IntentsTestRule intentsTestRule = new IntentsTestRule<>(MainActivity.class);
-
-
测试 Intent 逻辑
-
主动跳转
通过测试代码触发点击并判断跳转目标
onView(withId(R.id.btn)).perform(click()); intended(hasComponent(SecondActivity.class.getName())); -
自动跳转
如果是在 Activity 的 onCreate 或 onResume 中自动跳转,直接通过 intended 判断会失败,需要使用 ActivityTestRule 并且在 intended 判断之前手动调用 activityTestRule.launchActivity(Intent()),不能使用 IntentsTestRule。
activityTestRule.launchActivity(new Intent()); intended(hasComponent(SecondActivity.class.getName()));
-
多线程
Handler、AsyncTask 和 runOnUiThread 等
使用 Handler、AsyncTask 和 runOnUiThread 执行的异步操作,在运行测试时通过 ShadowLooper 的 runUiThreadTasks() 或 runUiThreadTasksIncludingDelayedTasks() 方法让在排队的任务立即执行。
// 业务代码
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
...
}
}, 1000);
// 测试代码
ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
intended(hasComponent(MainActivity::class.java.name))
RxJava
使用 RxJava 完成异步的代码,测试时需要利用 RxJava 的 TestScheduler 来调度异步任务,可通过自定义 TestRule 方式来实现。
public class RxJavaTestSchedulerRule implements TestRule {
private final TestScheduler mTestScheduler = new TestScheduler();
public TestScheduler getTestScheduler() {
return mTestScheduler;
}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
RxJavaPlugins.setIoSchedulerHandler(scheduler -> mTestScheduler);
RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> mTestScheduler);
RxJavaPlugins.setComputationSchedulerHandler(scheduler -> mTestScheduler);
RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> mTestScheduler);
try {
base.evaluate();
} finally {
RxJavaPlugins.reset();
RxAndroidPlugins.reset();
}
}
};
}
}
通过自定义 TestRule,在运行测试方法之前将 RxJava 或 RxAndroid 提供的 Scheduler 全部设置为 TestScheduler,并对外提供该 TestScheduler,在测试方法中获取该 TestScheduler 并通过 advanceTimeTo() 或 advanceTimeBy() 完成调度。
- advanceTimeTo(): 将 Scheduler 的时钟移到特定时间
- advanceTimeBy(): 将 Scheduler 的时钟在当前的基础上向前移动指定的时间量
@Rule
public RxJavaTestSchedulerRule rxJavaTestSchedulerRule = new RxJavaTestSchedulerRule();
rxJavaTestSchedulerRule.getTestScheduler().advanceTimeTo(10, TimeUnit.SECONDS);
Coroutine
在 gradle 中添加依赖
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.7'
参考Kotlin Coroutine 测试官方wiki
网络
在对具有网络请求的模块编写测试代码时,我们需要能够自定义网络响应内容,不能依赖真实网络请求。
HttpURLConnection
需要在 gradle 中添加依赖
testCompile 'org.robolectric:shadows-httpclient:4.3.1'
使用 HttpURLConnection 进行网络请求的项目,可以使用 Robolectric 框架自带的 FakeHttp 来完成模拟网络请求
//设置拦截真实请求
FakeHttp.getFakeHttpLayer().interceptHttpRequests(true);
//模拟返回
ProtocolVersion version = new ProtocolVersion("HTTP", 1, 1);
HttpResponse httpResponse = new BasicHttpResponse(version, 400, "OK");
//设置默认返回,所有请求返回这个
FakeHttp.setDefaultHttpResponse(httpResponse);
//添加一个返回规则,指定一个请求的期望返回
FakeHttp.addHttpResponseRule("http://www.baidu.com", httpResponse);
接下来当发起网络请求 www.baidu.com 时,就会返回我们制定的响应内容。
OkHttp
使用 OkHttp 完成网络请求,可以使用 OkHttp 提供的 MockWebServer 来搭建本地的模拟服务器,自定义网络请求响应内容。
private MockWebServer mockWebServer = new MockWebServer();
@Before
public void setUp() throws IOException {
mockWebServer.start(55555);
}
@After
public void release() throws IOException {
mockWebServer.shutdown();
}
@Test
public void test() {
mockWebServer.enqueue(
new MockResponse()
.addHeader("key", "value")
.setBody("body")
);
}
使用 MockWebServer 的 start() 方法启动模拟服务器,可以指定 host 或 port,host 默认为:localhost,port 默认自动生成;在构建 Retrofit 的时候将 baseUrl 设置为模拟服务器的地址。
new Retrofit.Builder()
.baseUrl("http://localhost:55555/")
.build()
.create();
}
通过 MockWebServer 的 enqueue() 方法往响应队列里添加自定义响应,先添加的先响应。添加完成后,最近的一次请求将会返回我们自定义的响应内容。
Fragment Test
添加依赖 【这里有个坑,debugImlementation 不能替换为 testImplementation】
debugImlementation 'androidx.fragment:fragment-testing:1.2.3'
AndroidX 提供了一个 FragmentScenario 库,便于您创建 Fragment 并更改其状态。
这些方法还支持以下类型的 Fragment:
- 包含界面的图形 Fragment。要启动此类 Fragment,请调用
launchFragmentInContainer()。FragmentScenario将 Fragment 附加到 Activity 的根视图控制器。此托管 Activity 原本为空。 - 非图形 Fragment(有时称为“无头 Fragment”),用于存储若干 Activity 中包含的信息或对这些信息进行短期处理。要启动此类 Fragment,请调用
launchFragment()。FragmentScenario将此类 Fragment 附加到一个完全为空的 Activity,即没有根视图的 Activity。
启动Fragment 后,FragmentScenario 将被测 Fragment 推动到 RESUMED 状态。此状态表示 Fragment 正在运行。
在更精细的单元测试中,您还可以评估 Fragment 从一个生命周期状态转换到另一个生命周期状态时的行为。要将 Fragment 推动到其他生命周期状态,请调用 moveToState()。此方法支持以下状态作为参数:CREATED、STARTED、RESUMED 和 DESTROYED
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
val scenario = launchFragmentInContainer<MyFragment>()
scenario.recreate()
}
}
更多详情可查看官方文档测试应用的 Fragment
ViewModel Test
ViewModel 的测试以验证逻辑为主,通过 mock 出依赖的环境和返回内容,模拟业务逻辑。一般情况下,ViewModel 是与 LiveData 协同作业的,这时候我们重点要验证 ViewModel 中方法被调用后,与之相关的 LiveData 的执行情况。
public class NewsViewModel extends ViewModel {
private CompositeDisposable disposable;
private final NewsApiClient apiClient;
private final RxSingleSchedulers rxSingleSchedulers;
private final MutableLiveData<NewsListViewState> newsListState = new MutableLiveData<>();
public MutableLiveData<NewsListViewState> getNewsListState() {
return newsListState;
}
@Inject
public NewsViewModel(NewsApiClient apiClient, RxSingleSchedulers rxSingleSchedulers) {
this.apiClient = apiClient;
this.rxSingleSchedulers = rxSingleSchedulers;
disposable = new CompositeDisposable();
}
public void fetchNews() {
disposable.add(apiClient.fetchNews()
.doOnEvent((newsList, throwable) -> onLoading())
.compose(rxSingleSchedulers.applySchedulers())
.subscribe(this::onSuccess,
this::onError));
}
private void onSuccess(NewsList newsList) {
NewsListViewState.SUCCESS_STATE.setData(newsList);
newsListState.postValue(NewsListViewState.SUCCESS_STATE);
}
private void onError(Throwable error) {
NewsListViewState.ERROR_STATE.setError(error);
newsListState.postValue(NewsListViewState.ERROR_STATE);
}
private void onLoading() {
newsListState.postValue(NewsListViewState.LOADING_STATE);
}
@Override
protected void onCleared() {
super.onCleared();
if (disposable != null) {
disposable.clear();
disposable = null;
}
}
}
Test 如下:
@RunWith(JUnit4.class)
public class NewsViewModelTest {
@Rule
public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();
@Mock
ApiEndPoint apiEndPoint;
@Mock
NewsApiClient apiClient;
private NewsViewModel viewModel;
@Mock
Observer<NewsListViewState> observer;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
viewModel = new NewsViewModel(apiClient, RxSingleSchedulers.TEST_SCHEDULER);
viewModel.getNewsListState().observeForever(observer);
}
@Test
public void testNull() {
when(apiClient.fetchNews()).thenReturn(null);
assertNotNull(viewModel.getNewsListState());
assertTrue(viewModel.getNewsListState().hasObservers());
}
@Test
public void testApiFetchDataSuccess() {
// Mock API response
when(apiClient.fetchNews()).thenReturn(Single.just(new NewsList()));
viewModel.fetchNews();
verify(observer).onChanged(NewsListViewState.LOADING_STATE);
verify(observer).onChanged(NewsListViewState.SUCCESS_STATE);
}
@Test
public void testApiFetchDataError() {
when(apiClient.fetchNews()).thenReturn(Single.error(new Throwable("Api error")));
viewModel.fetchNews();
verify(observer).onChanged(NewsListViewState.LOADING_STATE);
verify(observer).onChanged(NewsListViewState.ERROR_STATE);
}
@After
public void tearDown() throws Exception {
apiClient = null;
viewModel = null;
}
ListView & RecyclerView 测试
ListView 使用Espresso.onData(Matcher dataMatcher),您将提供一个Matcher,它将尝试匹配Adapter中的一行。 如果成功匹配,则Espresso会将该行带到屏幕上并进入视图层次结构,以便您可以照常执行操作并检查其视图上的断言:
onData(Matcher dataMatcher)
.perform(ViewAction action)
.check(ViewAssertion assert)
RecyclerView 通过 RecyclerViewActions 滑动到指定 item
onView(withId(R.id.myRecyclerView))
.perform(
RecyclerViewActions.actionOnItemAtPosition(0, click())
);
更多信息可以参考AdapterViews and Espresso
获取 Context 的几种姿势
ApplicationProvider.getApplicationContext()
InstrumentationRegistry.getInstrumentation().targetContext
RuntimeEnvironment.application
Gradle 依赖集合
def espresso = [:]
espresso.core = "androidx.test.espresso:espresso-core:$versions.espresso"
espresso.contrib = "androidx.test.espresso:espresso-contrib:$versions.espresso"
espresso.intents = "androidx.test.espresso:espresso-intents:$versions.espresso"
deps.espresso = espresso
def robolectric = [:]
robolectric.multidex = "org.robolectric:shadows-multidex:4.0.1"
robolectric.robolectric = "org.robolectric:robolectric:4.3.1"
robolectric.shadows_supportv4 = "org.robolectric:shadows-supportv4:3.8"
robolectric.httpclient = 'org.robolectric:shadows-httpclient:4.3.1'
deps.robolectric = robolectric
def powermock = [:]
powermock.junit4 = "org.powermock:powermock-module-junit4:$versions.powermock"
powermock.junit4rule = "org.powermock:powermock-module-junit4-rule:$versions.powermock"
powermock.mockito = "org.powermock:powermock-api-mockito:$versions.powermock"
powermock.xstream = "org.powermock:powermock-classloading-xstream:$versions.powermock"
deps.powermock = powermock
def coroutines = [:]
coroutines.core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.coroutines"
coroutines.coroutinestest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$versions.coroutines"
deps.coroutines = coroutines
def squareup = [:]
squareup.mockwebserver="com.squareup.okhttp3:mockwebserver:4.7.2"
deps.squareup=squareup
deps.fragmenttest = "androidx.fragment:fragment-testing:$versions.fragment"
deps.assertj = "org.assertj:assertj-core:3.15.0"
testImplementation deps.espresso.core
testImplementation deps.espresso.contrib
testImplementation 'androidx.test.ext:junit:1.1.1'
testImplementation deps.espresso.core
testImplementation deps.espresso.intents
testImplementation deps.assertj
testImplementation deps.powermock.junit4
testImplementation deps.powermock.junit4rule
testImplementation deps.powermock.mockito
testImplementation deps.powermock.xstream
testImplementation deps.robolectric.robolectric
testImplementation deps.robolectric.multidex
testImplementation deps.robolectric.shadows_supportv4
testImplementation deps.robolectric.httpclient
debugImplementation deps.fragmenttest
testImplementation coroutines.coroutinestest
testImplementation deps.squareup.mockwebserver
参考内容: