安卓自动化测试入门-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开发工程师工作,邮箱hengzhechenjay@163.com 电话:13580579413 陈捷尉 2016.11.30

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值