安卓自動化測試入門-5-創建UI

安卓自動化測試入門-5-創建UI

在這個系列的博客中,我們新建了一個叫做Github User Search的Android App範例。在Part1-4的博客中,介紹了爲什麼我們應該寫測試如何爲了測試而配置項目創建API調用創建一個Presenter 。請看一看前面的博客,因爲Part5將是這個系列的延續。

原文Part 1, Part 2, Part 3Part4

在Part5中,我們將會了解如何與Part4創建的Presenter交互,同時我們將會創建一個展示搜索結果列表的UI。

本文翻譯自Riggaroo的《 Introduction to Automated Android Testing – Part 5
注意:以下的測試特指“程序員編寫的自動化代碼測試”
水平有限,歡迎指教。如有錯漏,多多包涵。
作者的項目地址:
https://github.com/riggaroo/GithubUsersSearchApp
請注意:每個分支對應這一系列博客的每一篇文章。

創建UI

關於用戶交互界面,我們想要一個簡單的列表用於顯示每個用戶的頭像,姓名和一些用戶的其它信息。

Part4中,我們定義了一個Activity應該實現的View約定。這就是編寫Android特有代碼的地方(例如某個控件的可見性改變,或者任何UI的變更都應該寫在這裏)。重溫一下View約定的定義:

interface UserSearchContract {

    interface View extends MvpView {
        void showSearchResults(List<User> githubUserList);

        void showError(String message);

        void showLoading();

        void hideLoading();
    }

    interface Presenter extends MvpPresenter<View> {
        void search(String term);
    }
}

讓我們來實現這個View吧!

1 . 創建或導航到UserSearchActivity。這個類將實現UserSearchContract.View約定並繼承AppCompatActivity。定義一個UserSearchContract.Presenter類型的變量userSearchPresenter。這個就是我們用於調用網絡訪問的對象。

public class UserSearchActivity extends AppCompatActivity implements UserSearchContract.View {

    private UserSearchContract.Presenter userSearchPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user_search);
         userSearchPresenter = new UserSearchPresenter(Injection.provideUserRepo(), Schedulers.io(),
                AndroidSchedulers.mainThread());
        userSearchPresenter.attachView(this);

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        userSearchPresenter.detachView();
    }

    @Override
    public void showSearchResults(List<User> githubUserList) {

    }

    @Override
    public void showError(String message) {

    }

    @Override
    public void showLoading() {

    }

    @Override
    public void hideLoading() {
    }
}

onCreate()中,創建presenter對象。將Injection類定義的User Repo作爲第一個參數。(譯者注:有跟我一樣喜歡即時拷代碼進項目看的朋友嗎?我就當有了,我把原文的Injection代碼提前到下面。)傳遞ios()AndroidSchedulers.mainThread()計劃進構造器,這樣RxJava的Subscription就知道應該在哪條線程上面執行自己的代碼了。

在下一行,你可以看到我調用userSearchPresenter.attachView(this)。這個操作將View依附到Presenter,這樣Presenter就可以將變動通知給View。因爲Presenter並不會自動與Activity的生命週期聯動,所以在onDestroy()中我們需要通知Presenter這時View已經不存在了,具體做法是調用userSearchPresenter.detachView()。這樣就可以註銷RxJava的所有訂閱並防止內存泄露。

public class Injection {

    private static final String BASE_URL = "https://api.github.com";
    private static OkHttpClient okHttpClient;
    private static GithubUserRestService userRestService;
    private static Retrofit retrofitInstance;


    public static UserRepository provideUserRepo() {
        return new UserRepositoryImpl(provideGithubUserRestService());
    }

    static GithubUserRestService provideGithubUserRestService() {
        if (userRestService == null) {
            userRestService = getRetrofitInstance().create(GithubUserRestService.class);
        }
        return userRestService;
    }

    static OkHttpClient getOkHttpClient() {
        if (okHttpClient == null) {
            HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
            logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
            okHttpClient = new OkHttpClient.Builder().addInterceptor(logging).build();
        }

        return okHttpClient;
    }

    static Retrofit getRetrofitInstance() {
        if (retrofitInstance == null) {
            Retrofit.Builder retrofit = new Retrofit.Builder().client(Injection.getOkHttpClient()).baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create());
            retrofitInstance = retrofit.build();

        }
        return retrofitInstance;
    }
}

2 . 在layout文件夾創建activity_user_search.xml。這個文件將會包含一個RecyclerView,一個ProgressBar,一個錯誤TextView和一個Toolbar。我準備使用ConstraintLayout來設計我的屏幕,所以我不會很詳細地述說細節,因爲大部分操作都是拖和放。(如果你想要知道更多ConstraintLayout的信息,你可以打開我的 另一篇博客 。)

UserSearchActivity

activity_user_search.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_user_search"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="za.co.riggaroo.gus.presentation.search.UserSearchActivity">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="?attr/colorPrimary"
        android:minHeight="?attr/actionBarSize"
        android:theme="?attr/actionBarTheme"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintLeft_toLeftOf="@+id/activity_user_search"
        app:layout_constraintRight_toRightOf="@+id/activity_user_search"
        app:layout_constraintTop_toTopOf="@+id/activity_user_search">

    </android.support.v7.widget.Toolbar>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view_users"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="16dp"
        android:clipToPadding="false"
        android:scrollbars="vertical"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="@+id/activity_user_search"
        app:layout_constraintLeft_toLeftOf="@+id/activity_user_search"
        app:layout_constraintRight_toRightOf="@+id/activity_user_search"
        app:layout_constraintTop_toBottomOf="@+id/toolbar"
        tools:listitem="@layout/list_item_user">

    </android.support.v7.widget.RecyclerView>

    <TextView
        android:id="@+id/text_view_error_msg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="@string/search_for_some_users"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="@+id/recycler_view_users"
        app:layout_constraintLeft_toLeftOf="@+id/toolbar"
        app:layout_constraintRight_toRightOf="@+id/recycler_view_users"
        app:layout_constraintTop_toBottomOf="@+id/toolbar"
        tools:text="No Data has loaded"/>

    <ProgressBar
        android:id="@+id/progress_bar"
        style="@style/Widget.AppCompat.ProgressBar"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_marginBottom="16dp"
        android:layout_marginTop="16dp"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="@+id/activity_user_search"
        app:layout_constraintLeft_toLeftOf="@+id/recycler_view_users"
        app:layout_constraintRight_toRightOf="@+id/recycler_view_users"
        app:layout_constraintTop_toBottomOf="@+id/toolbar"
        tools:visibility="visible"/>

</android.support.constraint.ConstraintLayout>

strings.xml

<resources>
    <string name="app_name">Gus</string>
    <string name="search_users">Search for users on Github...</string>
    <string name="search_icon_title">Search</string>
    <string name="search_for_some_users">Start typing to search</string>
</resources>

3 . 我們還需要添加一個SearchView到ToolBar,這樣我們就有地方鍵入搜索詞。添加一個menu_user_search.xml文件到menu資源文件夾,在文件裏面,我們添加一個SearchView
menu_user_search.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/menu_search"
        android:icon="@drawable/ic_search"
        android:title="@string/search_icon_title"
        app:actionViewClass="android.support.v7.widget.SearchView"
        app:showAsAction="always|collapseActionView" />
</menu>

添加一個ic_search.xml文件到drawable文件夾:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportHeight="24.0"
    android:viewportWidth="24.0">
    <path
        android:fillColor="#FFFFFF"
        android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>

4 . 我們需要爲RecyclerView的每一個項創建一個layout。新建一個list_item_user.xml文件。我使用ConstraintLayout,包含一個顯示頭像的ImageView和兩個TextView。

List_item_user_designmode

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/constraintLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/imageview_userprofilepic"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginLeft="16dp"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        app:layout_constraintLeft_toLeftOf="@+id/constraintLayout"
        app:layout_constraintTop_toTopOf="@+id/constraintLayout"
        app:srcCompat="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/textview_username"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        app:layout_constraintLeft_toRightOf="@+id/imageview_userprofilepic"
        app:layout_constraintTop_toTopOf="@+id/constraintLayout"
        tools:text="Rebecca Franks" />

    <TextView
        android:id="@+id/textview_user_profile_info"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        android:layout_marginStart="16dp"
        android:textAppearance="@style/TextAppearance.AppCompat.Caption"
        app:layout_constraintBottom_toBottomOf="@+id/constraintLayout"
        app:layout_constraintLeft_toRightOf="@+id/imageview_userprofilepic"
        app:layout_constraintRight_toRightOf="@+id/constraintLayout"
        app:layout_constraintTop_toBottomOf="@+id/textview_username"
        tools:text="JHB, South Africa. Lots of code, lots and lots and lots of code." />
</android.support.constraint.ConstraintLayout>

5 . 現在我們已經有了所有需要的layout,讓我們把它們綁定到Activity吧。首先,在onCreate()方法獲取View的引用。

    private UsersAdapter usersAdapter;
    private SearchView searchView;
    private Toolbar toolbar;
    private ProgressBar progressBar;
    private RecyclerView recyclerViewUsers;
    private TextView textViewErrorMessage;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        progressBar = (ProgressBar) findViewById(R.id.progress_bar);
        textViewErrorMessage = (TextView) findViewById(R.id.text_view_error_msg);
        recyclerViewUsers = (RecyclerView) findViewById(R.id.recycler_view_users);
        usersAdapter = new UsersAdapter(null, this);
        recyclerViewUsers.setAdapter(usersAdapter);

    }

添加RecyclerView的依賴,版本與compileSdkVersion匹配就好

    compile 'com.android.support:recyclerview-v7:25.0.1'

添加UsersAdapter,用於RecyclerView。


public class UsersAdapter extends RecyclerView.Adapter<UserViewHolder> {
    private final Context context;
    private List<User> items;

    UsersAdapter(List<User> items, Context context) {
        this.items = items;
        this.context = context;
    }

    @Override
    public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_user, parent, false);
        return new UserViewHolder(v);
    }

    @Override
    public void onBindViewHolder(UserViewHolder holder, int position) {
        User item = items.get(position);

        holder.textViewBio.setText(item.getBio());
        if (item.getName() != null) {
            holder.textViewName.setText(item.getLogin() + " - " + item.getName());
        } else {
            holder.textViewName.setText(item.getLogin());
        }
        Picasso.with(context).load(item.getAvatarUrl()).into(holder.imageViewAvatar);
    }

    @Override
    public int getItemCount() {
        if (items == null) {
            return 0;
        }
        return items.size();
    }

    void setItems(List<User> githubUserList) {
        this.items = githubUserList;
        notifyDataSetChanged();
    }
}


class UserViewHolder extends RecyclerView.ViewHolder {
    final TextView textViewBio;
    final TextView textViewName;
    final ImageView imageViewAvatar;

    UserViewHolder(View v) {
        super(v);
        imageViewAvatar = (ImageView) v.findViewById(R.id.imageview_userprofilepic);
        textViewName = (TextView) v.findViewById(R.id.textview_username);
        textViewBio = (TextView) v.findViewById(R.id.textview_user_profile_info);
    }
}

6 . 我們需要將SearchView勾到Activity裏面並讓它觸發presenter的search()方法。在onCreateOptionsMenu()裏面,加上以下代碼:

@Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.menu_user_search, menu);
        final MenuItem searchActionMenuItem = menu.findItem(R.id.menu_search);
        searchView = (SearchView) searchActionMenuItem.getActionView();
        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                if (!searchView.isIconified()) {
                    searchView.setIconified(true);
                }
                userSearchPresenter.search(query);
                toolbar.setTitle(query);
                searchActionMenuItem.collapseActionView();
                return false; 
            }

            @Override
            public boolean onQueryTextChange(String s) {
                return false;
            }
        });
        searchActionMenuItem.expandActionView();
        return true;
    }

這段代碼將會填充正確的菜單,找到搜索視圖並設置一個文本查詢監聽器。在這種情形下,只有當用戶點擊鍵盤上的提交按鈕,我們纔會做出反應,調用presenter的搜索方法。我們也可以在onQueryTextChange方法裏面做這件事,不過考慮到Github API的調用頻率限制,我還是建議用onQueryTextSubmit。正常情況下,搜索結果將會展現。

7 . 接下來,我們實現presenter將會在數據加載完成後調用的回調。

    @Override
    public void showSearchResults(List<User> githubUserList) {
        recyclerViewUsers.setVisibility(View.VISIBLE);
        textViewErrorMessage.setVisibility(View.GONE);
        usersAdapter.setItems(githubUserList);
    }

    @Override
    public void showError(String message) {
        textViewErrorMessage.setVisibility(View.VISIBLE);
        recyclerViewUsers.setVisibility(View.GONE);
        textViewErrorMessage.setText(message);
    }

    @Override
    public void showLoading() {
        progressBar.setVisibility(View.VISIBLE);
        recyclerViewUsers.setVisibility(View.GONE);
        textViewErrorMessage.setVisibility(View.GONE);
    }

    @Override
    public void hideLoading() {
        progressBar.setVisibility(View.GONE);
        recyclerViewUsers.setVisibility(View.VISIBLE);
        textViewErrorMessage.setVisibility(View.GONE);

    }

我們基本上只是反轉視圖的可見性並給userAdapter設置網絡服務返回的新數據。

譯者注:如果遇到Toolbar相關的報錯This Activity already has an action bar supplied by the window decor. Do not request Window.FEATURE_SUPPORT_ACTION_BAR and set windowActionBar to false in your theme to use a Toolbar instead.,應該是Theme的設置沒有關閉默認的action bar。Theme的代碼如下:

<resources>

    <!-- Base application theme. -->
    <style name="MyTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>

</resources>

譯者注:記得添加網絡訪問權限

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="za.co.riggaroo.gus">

    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/MyTheme">

        <activity android:name=".presentation.search.UserSearchActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

8 . 現在你可以跑一下這個App了。你應該可以搜索一個Github的用戶名並看到結果。
github_user_search

Yay!我們有了一個能工作的App了。這篇博客的代碼可以在 這裏 找到。在下一篇,我們將會介紹如何編寫UI的測試。

尋找廣州Android開發工程師工作,郵箱[email protected] 電話:13580579413 陳捷尉 2016.11.30

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章