[譯] 經過測試來解耦 Activity

經過測試來解耦Activity

ActivityFragment,多是由於一些奇怪的歷史巧合,從 Android 推出之時起就被視爲構建 Android 應用的最佳構件。咱們把ActivityFragment 是應用的最佳構件這種想法稱爲「android-centric」架構。javascript

本系列博文是關於 android-centric 架構的可測試性和其它問題之間的聯繫的,而這些問題正致使 Android 開發者們排斥這種架構。這些博文也涉及單元測試怎樣試圖告訴咱們:ActivityFragment 不是應用的最佳構件,由於它們迫使咱們寫出高耦合低內聚的代碼。前端

上次,咱們發現ActivityFragment有低內聚的傾向。此次,經過測試咱們將會發現 Activity 是高耦合的。咱們還會發現如何經過測試來驅使實現一個耦合度更低的設計,這樣咱們就能輕易地改變應用和有更多的機會來減去重複代碼。像本系列博文中的其餘文章同樣,咱們依然以 Google I/O 應用爲例子進行探討。java

目標代碼

咱們想要測試的「目標代碼」,作了如下工做:當用戶進入展現全部 Google I/O session 的地圖界面時,app 會請求當前位置。若是用戶拒絕提供定位權限,咱們會彈出一個 toast 來提示用戶已禁用此權限。這是其中的截圖:react

拒絕請求的 toast

這是實現代碼:android

@Override
public void onRequestPermissionsResult(final int requestCode,
        @NonNull final String[] permissions,
        @NonNull final int[] grantResults) {

    if (requestCode != REQUEST_LOCATION_PERMISSION) {
        return;
    }

    if (permissions.length == 1 &&
            LOCATION_PERMISSION.equals(permissions[0]) &&
            grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        // Permission has been granted.
        if (mMapFragment != null) {
            mMapFragment.setMyLocationEnabled(true);
        }
    } else {
        // Permission was denied. Display error message.
        Toast.makeText(this, R.string.map_permission_denied,
                Toast.LENGTH_SHORT).show();
    }
    super.onRequestPermissionsResult(requestCode, permissions,
            grantResults);
}複製代碼

測試代碼

讓咱們嘗試測試下這些代碼,咱們的測試代碼看起來是這樣的:ios

@Test
public void showsToastIfPermissionIsRejected()
        throws Exception {
    MapActivity mapActivity = new MapActivity();

    mapActivity.onRequestPermissionsResult(
            MapActivity.REQUEST_LOCATION_PERMISSION,
            new String[]{MapActivity.LOCATION_PERMISSION}, new int[]{
                    PackageManager.PERMISSION_DENIED});

    assertToastDisplayed();
}複製代碼

固然你很但願能知道 assertToastDisplayed() 是怎麼實現的。重點來了:咱們不會直接實現該方法。爲了不實現後再重構咱們的代碼,咱們須要使用 Roboelectric 和 Powermock。(譯者注:Roboelectric 和 Powermock 均爲測試框架)git

不過,既然咱們更但願根據測試來改變咱們寫代碼的方式,而不是僅僅改變寫測試的方式,咱們要停一會來想想這些測試想要告訴咱們什麼事情:github

咱們在 MapActivity 裏面的代碼邏輯和 Toast 緊密地耦合在一塊兒。後端

這之間的耦合驅使咱們使用 Roboelectric 來模擬 android 行爲和 powermock 來模擬靜態的 Toast.makeText 方法。做爲替換,讓咱們以測試爲驅動來去除耦合。session

爲了讓咱們重構有個方向,咱們先寫測試。這將確保咱們的類已經解耦。爲了不使用 Roboelectric 框架,咱們須要在這特殊狀況下建立一個新類,可是一般來講,咱們只需重構已存在的類來解耦。

@Test
public void displaysErrorWhenPermissionRejected() throws Exception {

    OnPermissionResultListener onPermissionResultListener =
            new OnPermissionResultListener(mPermittedView);

    onPermissionResultListener.onPermissionResult(
            MapActivity.REQUEST_LOCATION_PERMISSION,
            new String[]{MapActivity.LOCATION_PERMISSION},
            new int[]{PackageManager.PERMISSION_DENIED});

    verify(mPermittedView).displayPermissionDenied();
}複製代碼

咱們已經介紹過 OnPermissionResultListener,它的工做就是處理用戶對 app 請求權限的反應。代碼以下:

void onPermissionResult(final int requestCode,
            final String[] permissions, final int[] grantResults) {
    if (requestCode != MapActivity.REQUEST_LOCATION_PERMISSION) {
        return;
    }

    if (permissions.length == 1 &&
            MapActivity.LOCATION_PERMISSION.equals(permissions[0]) &&
            grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        // Permission has been granted.
        mPermittedView.displayPermittedView();

    } else {
        // Permission was denied. Display error message.
        mPermittedView.displayPermissionDenied();
    }
}複製代碼

咱們把對 MapFragmentToast 的調用替換爲對 PermittedView 裏面方法的調用,這個對象經過構造函數來傳遞。PermittedView 是一個接口:

interface PermittedView {
    void displayPermissionDenied();

    void displayPermittedView();
}複製代碼

它在 MapActivity 裏實現:

public class MapActivity extends BaseActivity implements SlideableInfoFragment.Callback, MapFragment.Callbacks, ActivityCompat.OnRequestPermissionsResultCallback, OnPermissionResultListener.PermittedView {
    @Override
    public void displayPermissionDenied() {
        Toast.makeText(MapActivity.this, R.string.map_permission_denied,
                Toast.LENGTH_SHORT).show();
    }
}複製代碼

這也許不是最好的解決方案,可是這能讓咱們抓住能夠在哪裏測試這一重心。這要求 OnPermissionResultListener 下降和 PermittedView 的耦合度。解耦 == 顯而易見的進步。

有必要麼?

對於這一點,一些讀者可能會有所懷疑。「這樣真的算優化代碼嗎?」他們會大惑不解。有兩點理由能夠確認爲何這樣設計更好

(不管我給出哪個理由,你都會發現其解釋是「由於它的可測試性更好,因此它設計得更好」,這是一個很重要的緣由。)

更容易改變

首先,由於所組成的內容耦合度低,從而可以更容易地改變代碼,並且更精彩的是:咱們剛剛測試 Google I/O 應用的代碼實際上已經改變了,經過咱們的測試,能讓其改代碼變得更容易。所測試的代碼來自一個較舊的 commit。以後,寫 I/O 應用的人們決定把 Toast 替換爲 Snackbar

snackbar 拒絕請求

這是一個小改變,可是由於咱們已經把 OnPermissionResultListenerPermittedView 中分離出來,咱們能夠只專一於改變 PermittedViewMapActivity 裏面的實現,而無需擔憂 OnPermissionResultListener

這是咱們改變代碼後的樣子,使用他們的 PermissionUtils 類來顯示 SnackBar

@Override
public void displayPermissionDenied() {
    PermissionsUtils.displayConditionalPermissionDenialSnackbar(this,
            R.string.map_permission_denied, new String[]{LOCATION_PERMISSION},
            REQUEST_LOCATION_PERMISSION);
}複製代碼

請再留意,咱們能夠不用考慮 OnPermissionResultListener 就直接改變其內容。這實際就是 Larry Constantine 在 70 年代提出對耦合這一律唸的定義:

咱們盡力讓系統解耦。。。這樣咱們就能研究(或者調試、維護)其中一個模塊而無需考慮系統中的其餘模塊

–Edward Yourdon and Larry Constantine, Structured Design

去重

另外一個「爲何實際上經過咱們的測試來迫使咱們解耦是一件好事」的有趣緣由是:耦合一般會致使重複。Kent Beck 曾對此有相關見解:

依賴是任意規模的軟件開發的重點問題。。。若是依賴成爲了問題,這就會體如今重複上。

-Kent Beck, TDD By Example, pg 7.

若是這是對的,當咱們解耦,咱們將會發現更多的去重機會。的確,在咱們此次案例中這個觀點顯得很準確。事實上有另一個類的 onRequestPermissionsResultMapActivity 的幾乎同樣:AccountFragment。咱們的測試指引咱們來建立 OnPermissionResultListenerPermittedView 這兩個接口,所以無需任何修改就能夠在其餘類中複用。

結論

因此,當咱們難以測試 ActivityFragment時,一般是由於咱們的測試嘗試告訴咱們所寫的代碼耦合度過高。測試對耦合度的警告一般以咱們沒法對代碼作出斷言的形式表現出來。

當咱們遵從咱們的測試時,與其經過 Roboelectric 和 powermock 替換測試代碼,不如改變被測代碼,讓其耦合度下降,這樣咱們就能更容易改代碼和有更多的機會去重。

注意

  1. 這也可能表現爲沒法讓你的被測代碼在測試中以一個正確的狀態表現出來。例如咱們在本篇中所看到的。

咱們在 Unikey 招聘中級 Android 開發者。若是你想要在 Orlando 智能鎖定空間裏的一間初創公司工做,請發郵件給我。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索