对于大多数 Android 商业项目,基本都是处于高速迭代的开发阶段,这个阶段不只仅是对项目的开发效率,也对项目的产品质量提出了更高的要求。java
一般大型项目都是经过黑盒测试等方式来提供质量相关的保障,但同时笔者认为也须要 Android 端的单元测试以及能自动在 Android 平台上运行的 UI 测试,这几种测试有如下几个优点:android
在 Android Studio 中新建新的项目时,它已自动为两种测试类型建立了对应的代码目录:数据库
接下来,笔者将尝试为本身的项目(基于 MVP 架构开发)补充相应的单元测试用例和 UI 测试用例,来初步实践下如何在 Android 平台编写和运行相关的测试用例。网络
若是须要编写一个新的本地单元测试用例,只需打开你想测试的 java 代码文件,而后点击类名 – ⇧⌘T(Windows:Ctrl+Shift+T)– 选择要生成的方法 – 选择 test 文件夹,对应于本地单元测试 – 完成。架构
须要 JUnit 和 Mockito 框架支持,因此在 build.gradle 中增长:app
testImplementation "junit:junit:4.12" testImplementation "org.mockito:mockito-core:2.7.1"
通常来讲,编写一段测试代码须要三个步骤:框架
笔者主要测试的是 MVP 架构中 P 层的代码。在笔者的项目中,P 层是经过 Dagger2 机制,注入一个 DataManager,也就是数据获取源。同时也须要一个 V 层的代理,这样在 P 层经过数据源获取数据以后,就能将数据交给 V 层,由 V 层去展现。异步
代码调用大体逻辑以下:ide
mPresenter = new NewsPresenter(mDataManager); mPresenter.getNews(); mPresenter.attach(mView); --> mView.showProgress(); // 在数据未加载完前加载进度条 --> mView.showNews(news); --> mView.hideProgress(); // 在数据加载完后隐藏进度条
对应着,实际编写 P 层的单元测试用例的时候,并不须要一个真实的数据源,只须要经过 Mockito 框架,mock 出一个测试用的 DataManager 和 V 层代理。单元测试
对应着 Presenter 类,新建立的测试代码以下:
/** * Created by Xu on 2019/04/05. * * @author Xu */ public class NewsPresenterTest { @ClassRule public static final RxImmediateSchedulerRule schedulers = new RxImmediateSchedulerRule(); @Mock private NewsContract.View view; @Mock protected DataManager mMockDataManager; private NewsPresenter newsPresenter; @Before public void setUp() { MockitoAnnotations.initMocks(this); newsPresenter = new NewsPresenter(mMockDataManager); newsPresenter.attach(view); } @Test public void getNewsAndLoadIntoView() { TencentNewsResultBean resultBean = new TencentNewsResultBean(); resultBean.setData(new ArrayList<>()); when(mMockDataManager.getNews()).thenReturn(Flowable.just(resultBean)); newsPresenter.getNews(); // 测试model是否有获取数据 verify(mMockDataManager).getNews(); // 测试view是否调用相应接口 verify(view).showProgress(); verify(view).showNews(anyList()); verify(view).hideProgress(); } @After public void tearDown() { newsPresenter.detach(); } }
在其中:
什么是 @ClassRule 呢?它跟 @Rule 注解几乎相同,能够在全部类方法开始前进行一些相关的初始化调用操做。使用这个注解,能够在执行测试用例的时候加入特有的操做,而不影响原有用例代码,有效减小耦合程度。
这里主要是由于项目中使用了 RxJava2,而 RxJava 是须要 Android 环境支持的,若是直接运行 JUnit 测试用例会报错,因此在此处增长了一个 @ClassRule,具体可参考
https://stackoverflow.com/questions/41121778/junit-rule-and-classrule
/** * Created by Xu on 2019/04/05. * * @author Xu */ public class RxImmediateSchedulerRule implements TestRule { private Scheduler immediate = new Scheduler() { @Override public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) { // this prevents StackOverflowErrors when scheduling with a delay return super.scheduleDirect(run, 0, unit); } @Override public Worker createWorker() { return new ExecutorScheduler.ExecutorWorker(Runnable::run); } }; @Override public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { RxJavaPlugins.setInitIoSchedulerHandler(scheduler -> immediate); RxJavaPlugins.setInitComputationSchedulerHandler(scheduler -> immediate); RxJavaPlugins.setInitNewThreadSchedulerHandler(scheduler -> immediate); RxJavaPlugins.setInitSingleSchedulerHandler(scheduler -> immediate); RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> immediate); try { base.evaluate(); } finally { RxJavaPlugins.reset(); RxAndroidPlugins.reset(); } } }; } }
至此,一个 Android 的单元测试用例编写完成。经过 Android Studio 直接运行此单元测试用例,结果以下:
须要明白一个点:单元测试它只是测试一个方法单元,它不是测试一整个 APP 的功能流程,即单元测试不会涉及到数据库或网络等复杂的外部环境。好比说这里咱们只测试到 NewsPresenter#getNews() 方法,并无测试 NewsFragment 的整个初始化到显示的过程是否正常,数据是否有误。(这样的测试每每称之为集成测试)
若是要编写一个新的本地 UI 测试用例,只需打开你想测试的 java 代码文件,而后点击类名 – ⇧⌘T(Windows:Ctrl+Shift+T)– 选择要生成的方法 – 选择 androidTest 文件夹,对应于本地 UI 测试 – 完成。
须要 Espresso 框架支持,因此在 build.gradle 中增长(注意是 androidTestImplementation):
androidTestImplementation "androidx.test:runner:1.1.0" androidTestImplementation "androidx.test:rules:1.1.0" androidTestImplementation "androidx.test.espresso:espresso-core:3.0.2" androidTestImplementation "androidx.test.espresso:espresso-contrib:3.0.2" androidTestImplementation "androidx.test.espresso:espresso-intents:3.0.2" androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:3.0.2" androidTestImplementation "androidx.test.espresso:espresso-idling-resource:3.0.2"
笔者主要测试的代码为 NewsDetailActivity,主要功能是加载 intent 传递过来的新闻标题和新闻原文地址,而后在 Toolbar 中显示新闻标题,在 Webview 中加载此新闻。
对应着,实际编写测试代码的时候,能够构造一个测试用的 intent,在 intent 中加入须要的测试数据,而后启动这个 activity,检查数据是否正确便可。这里咱们借助 Espresso 框架,它有三个重要的组成部分:ViewMatchers(根据视图 id 或其余属性匹配指定的 View),ViewActions(执行 View 的某些行为,例如点击事件),ViewAssertions(检查 View 的某些状态,例如指定 View 是否显示在屏幕上)。
新建立的 UI 测试代码以下:
/** * Created by Xu on 2019/04/09. */ @RunWith(AndroidJUnit4.class) @LargeTest public class NewsDetailActivityTest { @Rule public ActivityTestRule<NewsDetailActivity> newsDetailActivityActivityTestRule = new ActivityTestRule<>(NewsDetailActivity.class, true, false); @Before public void setUp() { Intent intent = new Intent(InstrumentationRegistry.getInstrumentation().getTargetContext(), NewsDetailActivity.class); intent.putExtra(Constants.NEWS_URL, TestConstants.NEWS_DETAIL_ACTIVITY_TEST_URL); intent.putExtra(Constants.NEWS_IMG, TestConstants.NEWS_DETAIL_ACTIVITY_TEST_IMG); intent.putExtra(Constants.NEWS_TITLE, TestConstants.NEWS_DETAIL_ACTIVITY_TEST_TITLE); newsDetailActivityActivityTestRule.launchActivity(intent); IdlingRegistry.getInstance().register(newsDetailActivityActivityTestRule.getActivity().getCountingIdlingResource()); } @Test public void showNewsDetail() { onView(withId(R.id.toolbar)).check(matches(isDisplayed())); onView(withId(R.id.iv_news_detail_pic)).check(matches(isDisplayed())); onView(withId(R.id.clp_toolbar)).check(matches(isDisplayed())); onView(withId(R.id.clp_toolbar)).check(matches(withCollapsingToolbarLayoutText(is(TestConstants.NEWS_DETAIL_ACTIVITY_TEST_TITLE)))); } @After public void tearDown() { IdlingRegistry.getInstance().unregister(newsDetailActivityActivityTestRule.getActivity().getCountingIdlingResource()); } }
在其中:
@RunWith 注解能够改变 JUnit 测试用例的的默认执行类,因为这里是须要 Android 环境且使用到 Espresso 框架,因此 @RunWith 选择 AndroidJUnit4 类。@LargeTest 表示此测试用例会使用到外部文件系统或者网络,而且运行时间大于 1000 ms。
什么是 IdlingResource 呢?
一般来讲,大多数 APP 在设计业务功能的过程当中,会有不少的异步任务,例如使用 Rxjava 发起网络请求等,可是 Espresso 并不知道你的异步任务何时结束,若是单纯使用 Thread.sleep() 等待异步回调的结果又过于“硬核”,因此须要借助于 IdlingResource 这个类。
它须要在业务代码中添加相关的逻辑。例如在 NewsDetailActivity 中,会接收到 intent 传递过来的新闻图片地址,而后使用 Glide 异步加载此图片,大体代码以下:
public class NewsDetailActivity extends AppCompatActivity { @BindView(R.id.iv_news_detail_pic) private ImageView ivNewsDetailPic; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_news); // 省略部分代码逻辑 // 开始发起异步操做,App开始进入忙碌状态 EspressoIdlingResource.increment(); // 开始加载图片 Glide.with(context).asBitmap().load(imgUrl).into(new GlideDrawableImageViewTarget(mAvatar) { @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { super.onResourceReady(resource, transition); // 异步操做结束,将App设置成空闲状态 if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) { EspressoIdlingResource.decrement(); } } }); } // 省略代码 @VisibleForTesting public IdlingResource getCountingIdlingResource() { return EspressoIdlingResource.getIdlingResource(); } } public class EspressoIdlingResource { private static final String RESOURCE = "GLOBAL"; // Espresso 提供了一个实现好的 CountingIdlingResource 类 // 若是没有特别需求的话,直接使用它便可 private static CountingIdlingResource countingIdlingResource = new CountingIdlingResource(RESOURCE); public static void increment() { countingIdlingResource.increment(); } public static void decrement() { countingIdlingResource.decrement(); } public static IdlingResource getIdlingResource() { if (countingIdlingResource == null) { countingIdlingResource = new CountingIdlingResource(RESOURCE); } return countingIdlingResource; } }
再加上咱们在测试代码中声明的 IdlingRegistry.getInstance().register() 和 IdlingRegistry.getInstance().unregister() 方法,根据 APP 是否处于忙碌状态来判断异步任务是否完成,这样 Espresso 就能作到对异步任务进行相应的测试。
以链式代码的形式编写验证测试结果的代码,例如 onView(withId(R.id.toolbar)).check(matches(isDisplayed())); 意思就是获取 id 为 R.id.toolbar 的 view,检查这个 view 是否正常显示。
若是 Espresso 自带的 View Matchers 不能知足需求的话,咱们也能够自定义一个 matcher,例如 onView(withId(R.id.clp_toolbar)).check(matches(withCollapsingToolbarLayoutText(is(TestConstants.NEWS_DETAIL_ACTIVITY_TEST_TITLE)))); ,咱们获取到的 view 是一个 CollapsingToolbarLayout,是一个特殊样式的 Toolbar,咱们要检查其中的标题是否与测试数据相匹配,咱们能够编写自定义的 Matcher:
public static Matcher<View> withCollapsingToolbarLayoutText(Matcher<String> stringMatcher) { return new BoundedMatcher<View, CollapsingToolbarLayout>(CollapsingToolbarLayout.class) { @Override public void describeTo(Description description) { description.appendText("with CollapsingToolbarLayout title: "); stringMatcher.describeTo(description); } @Override protected boolean matchesSafely(CollapsingToolbarLayout collapsingToolbarLayout) { return stringMatcher.matches(collapsingToolbarLayout.getTitle()); } }; }
这里传入一个 String 类型的匹配器(经过 is() 方法返回),返回一个 CollapsingToolbarLayout title 的 Matcher。
至此,一个 Android 的 UI 测试用例编写完成。经过 Android Studio 直接运行此用例,结果以下:
本文主要从测试的两个不一样粒度:单元测试和 UI 测试入手,综合参考 Google Sample 项目中的测试代码,作一个初步实践,分析编写并运行相关的测试用例。
笔者认为编写 Android 的测试用例的大体流程以下: