Insider's Guide To Udacity Android Developer Nanodegree Part 3 - Making the Baking App
Written by Nikos Vaggalis   
Monday, 03 July 2017
Article Index
Insider's Guide To Udacity Android Developer Nanodegree Part 3 - Making the Baking App
Step 1 Fragments
Step 2 - Libraries & Networking
Step 3 - Adding Exoplayer
Step 4 - Widgets
Step 5 - The Widget Provider
Step 6 - UI Testing
Step 7 - Testing Intents

Step 6 - UI testing with Espresso

It's time to test the UI in an automated way by using the Espresso testing framework. Espresso simulates user interaction which can be summarized in three steps:

  •  find the view with ViewMatcher

  •  perform action on the view such as click on it with ViewActions

  •  check if the view did what we expected as the result of the action performed on it  with ViewAssertion


 
image28

 

Google offers a nice cheatsheet of the Espresso utility methods.

image28a
Before I can run my test, I first need to add the necessary
Gradle dependencies. But the Gradle build was giving 'Conflict with dependency' errors:

image29

 

After some digging, the solution was to exclude:

 
 androidTestCompile (
  'com.android.support.test.espresso:espresso-contrib:2.2.2'){
    exclude group: 'com.android.support', module: 'appcompat-v7'
    exclude group: 'com.android.support', module: 'support-v4'
    exclude module: 'recyclerview-v7'
    }
   


Maybe a tutorial on advanced Gradle use should have beeen included.

Anyway, wanting to test my RecipeActivity, I wrapped it up into an Espresso AcitivityTestRule:

  
 public ActivityTestRule<RecipeActivity>    
  mActivityTestRule =
      new ActivityTestRule<>(RecipeActivity.class);
   


Then I ran two tests :


@Test
    public void checkText_RecipeActivity() {
        onView(ViewMatchers.withId(R.id.recipe_recycler))
                  .perform(RecyclerViewActions.scrollToPosition(1));
        onView(withText("Brownies")).check(matches(isDisplayed()));
    }

@Test
    public void checkPlayerViewIsVisible_RecipeDetailActivity1() {
        onView(ViewMatchers.withId(R.id.recipe_recycler))
               .perform(RecyclerViewActions
                .actionOnItemAtPosition(0,click()));
        onView(ViewMatchers.withId(R.id.recipe_detail_recycler))
            .perform(RecyclerViewActions
                .actionOnItemAtPosition(0,click()));
        onView(withId(R.id.playerView))
                .check(matches(isDisplayed()));
    }


The checkText_RecipeActivity() test  returned the RecyclerView with id 'recipe_recycler'
which is the RecyclerViewer of the RecipeActivity which contains the list with all the initial recipes, Nutella Pie, Brownies, etc. Espresso was then instructed to scroll down to its second element to check if the word 'Brownies' existed at that location. 

In the checkPlayerViewIsVisible_RecipeDetailActivity1() test, scroll to the first RecyclerView item, 'Nutella Pie' and click it. This should cause the RecipeDetailActivity to open up and the RecipeDetail RecyclerView 'recipe_detail_recycler' to appear with the list of steps.

On this RecyclerView clicking on '0.Recipe Introduction' could be expect to display the Exoplayer view:

  
 onView(withId(R.id.playerView)).check(matches(isDisplayed()));

 

However, because the list of the recipes is loaded asynchronously due to Retrofit's enqueue method, we have to use an IdlingResource to tell Espresso to wait for the async work to complete in order for the RecyclerViewer to be loaded with the recipe list before it starts the testing. Otherwise several runtime failure errors could occur as the tests would have run before enqueue was able to complete its work.

An IdlingResource must be registered before the test:

 
@Before
    public void registerIdlingResource() {
        mIdlingResource = mActivityTestRule.getActivity()
                                                           .getIdlingResource();
        // To prove that the test fails, omit this call:
        Espresso.registerIdlingResources(mIdlingResource);
    }

@Test
    public void checkText_RecipeActivity() {
        onView(ViewMatchers.withId(R.id.recipe_recycler))
               .perform(RecyclerViewActions.scrollToPosition(1));
        onView(withText("Brownies")).check(matches(isDisplayed()));
    }

    @Test
    public void checkPlayerViewIsVisible_RecipeDetailActivity1() {
        onView(ViewMatchers.withId(R.id.recipe_recycler))
            .perform(RecyclerViewActions
                        .actionOnItemAtPosition(0,click()));
        onView(ViewMatchers.withId(R.id.recipe_detail_recycler))
            .perform(RecyclerViewActions
                       .actionOnItemAtPosition(0,click()));
        onView(withId(R.id.playerView)).check(matches(isDisplayed()));
    }


and unregistered after running the tests:


@After
    public void unregisterIdlingResource() {
        if (mIdlingResource != null) {
            Espresso.unregisterIdlingResources(mIdlingResource);
        }
    }

 

The IdlingResource initialization should happen inside the activity in question:


@VisibleForTesting
    @NonNull
    public IdlingResource getIdlingResource() {
        if (mIdlingResource == null) {
            mIdlingResource = new SimpleIdlingResource();
        }
        return mIdlingResource;
    }

 

and it is this instance handed over to the fragment that does the async work, that is RecipeFragment

 

    
   SimpleIdlingResource idlingResource =
      (SimpleIdlingResource)((RecipeActivity)getActivity())
                             .getIdlingResource();
       

 

uses it to make the actual network call when the activity is idle, that is all UI code has been run to completion

 


  recipe.enqueue(new Callback<ArrayList<Recipe>>() {
            @Override
            public void onResponse(Call<ArrayList<Recipe>> call,
                          Response<ArrayList<Recipe>> response) {
                Integer statusCode = response.code();
                Log.v("status code: ", statusCode.toString());

                ArrayList<Recipe> recipes = response.body();

                Bundle recipesBundle = new Bundle();
                recipesBundle.putParcelableArrayList(
                                                           ALL_RECIPES, recipes);

                recipesAdapter.setRecipeData(recipes,getContext());
                if (idlingResource != null) {
                    idlingResource.setIdleState(true);
                }

            }

            @Override
            public void onFailure(Call<ArrayList<Recipe>> call,
                                                                         Throwable t) {
                Log.v("http fail: ", t.getMessage());
            }
        });
   

 



Last Updated ( Monday, 20 November 2017 )