對於大多數 Android 商業項目,基本都是處於高速迭代的開發階段,這個階段不只僅是對項目的開發效率,也對項目的產品質量提出了更高的要求。java
一般大型項目都是經過黑盒測試等方式來提供質量相關的保障,但同時筆者認爲也須要 Android 端的單元測試以及能自動在 Android 平臺上運行的 UI 測試,這幾種測試有如下幾個優點:android
在 Android Studio 中新建新的項目時,它已自動爲兩種測試類型建立了對應的代碼目錄:數據庫
接下來,筆者將嘗試爲本身的項目(基於 MVP 架構開發)補充相應的單元測試用例和 UI 測試用例,來初步實踐下如何在 Android 平臺編寫和運行相關的測試用例。微信
若是須要編寫一個新的本地單元測試用例,只需打開你想測試的 java 代碼文件,而後點擊類名 -- ⇧⌘T(Windows:Ctrl+Shift+T)-- 選擇要生成的方法 -- 選擇 test 文件夾,對應於本地單元測試 -- 完成。網絡
須要 JUnit 和 Mockito 框架支持,因此在 build.gradle 中增長:架構
testImplementation "junit:junit:4.12"
testImplementation "org.mockito:mockito-core:2.7.1"
複製代碼
通常來講,編寫一段測試代碼須要三個步驟:app
筆者主要測試的是 MVP 架構中 P 層的代碼。在筆者的項目中,P 層是經過 Dagger2 機制,注入一個 DataManager,也就是數據獲取源。同時也須要一個 V 層的代理,這樣在 P 層經過數據源獲取數據以後,就能將數據交給 V 層,由 V 層去展現。框架
代碼調用大體邏輯以下:異步
mPresenter = new NewsPresenter(mDataManager);
mPresenter.getNews();
mPresenter.attach(mView);
--> mView.showProgress(); // 在數據未加載完前加載進度條
--> mView.showNews(news);
--> mView.hideProgress(); // 在數據加載完後隱藏進度條
複製代碼
對應着,實際編寫 P 層的單元測試用例的時候,並不須要一個真實的數據源,只須要經過 Mockito 框架,mock 出一個測試用的 DataManager 和 V 層代理。ide
對應着 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,具體可參考 stackoverflow.com/questions/4…
/** * 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 的測試用例的大體流程以下:
這篇文章會同步到個人我的日誌,若有問題,請你們踊躍提出,謝謝你們!