對于大多數 Android 商業項目,基本都是處于高速迭代的開發階段,這個階段不僅僅是對項目的開發效率,也對項目的產品質量提出了更高的要求。
通常大型項目都是通過黑盒測試等方式來提供質量相關的保障,但同時筆者認為也需要 Android 端的單元測試以及能自動在 Android 平臺上運行的 UI 測試,這幾種測試有以下幾個優勢:
在 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"
一般來說,編寫一段測試代碼需要三個步驟:
筆者主要測試的是 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 層代理。
對應著 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 的測試用例的大致流程如下:
原文轉自:https://dev-xu.cn/posts/1e6d7596.html