Categories
android testing

Introduction to Automated Android Testing – Part 5

In this series of blog posts, we are working through a sample app called Github User Search. Parts 1 – 4 covered why we should test, getting set up with testing, creating API calls and creating a presenter. Take a look at the previous posts as part 5 is a continuation of the series.

In part 5, we will take a look at interacting with the Presenter  created in part 4 and we will create the UI to display the list of search results.

Creating the UI

For the User Interface we want a simple list that displays the avatar, name and other user information in the list.

In part 4, we defined a View contract which the Activity should implement. This is where the Android specific code will be located (things such as visibility changes or any UI changes will be located here). To refresh your memory, this is the View contract we created in this last post:

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);
    }
}

Let’s implement the View!

  1. Create a class called UserSearchActivity. This class will implement the UserSearchContract.View contract and extend AppCompatActivity. Define a variable called userSearchPresenter of type UserSearchContract.Presenter. This is the object that we will interact with in order to perform our network calls.
    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() {
        }
    }

    In onCreate(), create the presenter object. Provide it with the User repo defined in the Injection class. Pass the io() scheduler and the AndroidSchedulers.mainThread() scheduler so that the RxJava subscriptions know which threads they should perform their work on.

    On the next line, you can see I call userSearchPresenter.attachView(this) . This attaches the view to the presenter, so that the presenter can notify the view of any changes. Because the presenter isn’t aware of the activity’s lifecycle, in onDestroy() we need to inform the presenter that the view is no longer in existence, so we should then call userSearchPresenter.detachView(). This will unregister any RxJava subscriptions and prevent memory leaks from occurring.

  2. Create activity_user_search.xml in the layout folder. This will contain a RecyclerView, a ProgressBar, an error TextView and a Toolbar. I am using ConstraintLayout to design my screen, so I won’t go into too much detail as it is mostly drag and drop. (If you want to read more about ConstraintLayout check out my blog post about it here)UserSearchActivity
    <?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>
    
  3. We also need to add a SearchView to the toolbar so we have somewhere to type. Add a menu_user_search.xml file to the menu resource folder. Inside it, you will need to add a SearchView:
    <?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"
            app:showAsAction="always|collapseActionView"
            android:title="@string/search_icon_title"
            app:actionViewClass="android.support.v7.widget.SearchView"/>
    </menu>
    
  4. We need to create a layout that will be used for each item in the RecyclerView. Create a file named list_item_user.xml in the layout folder. I used ConstraintLayout with one ImageView for the avatar and two TextViews.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. Now that we have all the layouts we need, let’s tie the XML to the Activity. First, in onCreate() we will get references to the views we need.
        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);
    
        }
  6. We need to hook the SearchView up into our activity to make it trigger the presenters search() method. In the onCreateOptionsMenu(), add the following code:
    @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;
        }

    This will inflate the correct menu, find the search view and set a query text listener. In this case, only when someone presses submit on the keyboard, we will respond by calling the search presenter with the query. We could do it in onQueryTextChange too but due to rate limiting on the Github API I will stick to onQueryTextSubmit. By default, the item will be expanded.

  7. Next, we will implement the callbacks that the presenter will call when the items are finished loading.
        @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);
    
        }

    We are basically just toggling visibility of views here and setting the usersAdapter  to the new items that the service returned.

  8. For completeness, here is the UserSearchAdapter class which is used for the RecyclerView on the activity:
    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);
        }
    }
    

     

    Injection class

    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;
        }
    }
    
  9. Now if you run the app, you should be able to search for a username on Github and see results.github_user_search

Yay! We have a working app. Code for this post can be found here. In the next part, we will look at writing UI tests for the app. Make sure you subscribe so you don’t miss a post!

Subscribe to Blog via Email

Enter your email address to subscribe to this blog and receive notifications of new posts by email.


If you enjoyed this blog post, please consider buying me a cupcake to support future blog posts.

[buy_cupcake]

3 replies on “Introduction to Automated Android Testing – Part 5”

Hello Rebecca, thanks for sharing this amazing project! I just miss a key part which is handling change of states in the phone like rotation. What do you use or recommend to keep the presenter in those changes? Thanks and keep it up!

Hi Riggaroo, thanks for sharing this good series articles. I am a Chinese android developer, and I have learned a lot from these articles. Here I wonder if I can translate your articles to chinese to share them in china, and help more people ?

Be, yours

Lovexiaov

Leave a Reply