Categories
android testing

Introduction to Automated Android Testing – Part 6

In the previous 5 blog posts, we covered different aspects of building an Android app from scratch. We focused on including tests in the process. Here are the links to the previous posts:

  • Post #1 – Why should we write tests?
  • Post #2 – Set up your app for testing
  • Post #3 – Creating API calls
  • Post #4 – Creating repositories
  • Post #5 – Following the MVP pattern

In this last post of the series, we will cover creating Espresso tests for the View we created in part 5. The Github repo for this post can be found here.

Testing that a view contains the exact information expected can be tricky if the data is dynamic. This data can change at any time and our tests should not fail because of it. In order for the tests to be reliable and repeatable, we should not call any production APIs.

Mocking out the responses of the API calls will enable us to write tests that depend on the mocked data. There are a couple of ways in which we can mock out our API calls:

  • Option 1 – Use WireMock and run a standalone server which serves up the same static JSON for specific network calls.
  • Option 2 – Use OkHttp’s MockWebServer which runs a webserver on your device and serves up any response you request.
  • Option 3 – Create a custom implementation of the Retrofit REST interface that returns dummy objects.

Obviously, the choice is entirely up to you as to how you would want to go about writing UI tests. In my case, WireMock is extra effort as I would need to ensure I have a standalone server running with a static IP address.

MockWebServer is a lot easier to use than WireMock as you don’t have to set up a standalone web server (The server runs on the device). MockWebServer is also flexible because you can give it different scenarios. Useful features like specifying the failure rate of a certain call or simulating a slow network are possible using MockWebServer. (Read more here).

I am going to use option 3 for the purpose of testing that the UI matches the mock response data. If I wanted to add tests for slow network conditions (or some kind of non-functional test), I would choose option 2. If you are unable to use OkHttp,  I would choose option 1 as Wiremock works with any HTTP client.

Mocking out data using Gradle flavors

By making use of Gradle flavors, we can easily mock out of API responses.  If you read post #2 on Gradle flavors, you should already have a “mock” and a “production” flavor set up.

  1. Make sure you are switched to the mockDebug flavor. Select mockDebug variant
  2. Create a mock folder in the src directory. Then create a package within the mock folder, that mimics the main package name. Make a class called MockGithubUserRestServiceImpl. Your resulting file structure should look like this: Folder structure for mock testing
  3. Create a prod directory. Move the Injection class defined previously into this folder. We will be creating another Injection class in the mock folder. This class will inject the mocked out Github service instead of the production API. Move Injection to prod and create one in mock folderIn the Injection class that is located in the mock folder, we simply return the MockGithubUserServiceImpl that we created. In the prod folder, we return the actual Retrofit Github service. 
    Mock Injection class:

    public class Injection {
    
        private static GithubUserRestService userRestService;
    
        public static UserRepository provideUserRepo() {
            return new UserRepositoryImpl(provideGithubUserRestService());
        }
    
        static GithubUserRestService provideGithubUserRestService() {
            if (userRestService == null) {
                userRestService = new MockGithubUserRestServiceImpl();
            }
            return userRestService;
        }
    
    }

    Prod 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;
        }
    }
  4. The data returned from the mock service is dependant on your specific requirements. Below is my implementation of the MockGithubUserRestServiceImpl:
    public class MockGithubUserRestServiceImpl implements GithubUserRestService {
        
        private final List<User> usersList = new ArrayList<>();
        private User dummyUser1, dummyUser2;
    
        public MockGithubUserRestServiceImpl() {
            dummyUser1 = new User("riggaroo", "Rebecca Franks",
                    "https://riggaroo.co.za/wp-content/uploads/2016/03/rebeccafranks_circle.png", "Android Dev");
            dummyUser2 = new User("riggaroo2", "Rebecca's Alter Ego",
                    "https://s-media-cache-ak0.pinimg.com/564x/e7/cf/f3/e7cff3be614f68782386bfbeecb304b1.jpg", "A unicorn");
            usersList.add(dummyUser1);
            usersList.add(dummyUser2);
        }
    
        @Override
        public Observable<UsersList> searchGithubUsers(final String searchTerm) {
            return Observable.just(new UsersList(usersList));
        }
    
        @Override
        public Observable<User> getUser(final String username) {
            if (username.equals("riggaroo")) {
                return Observable.just(dummyUser1);
            } else if (username.equals("riggaroo2")) {
                return Observable.just(dummyUser2);
            }
            return Observable.just(null);
        }
    }
    

    In this case, I am just returning some dummy data. Let’s run the mock version of the app and we should get the same results no matter what you search.

    Android Test app with mock data

    Cool. Now we have a working dummy app! We can now write Espresso UI tests. 

Basics of Writing an Espresso Test

When writing an Espresso test, the following formula is used for performing functions within your UI:

onView(withId(R.id.menu_search))      // withId(R.id.menu_search) is a ViewMatcher
  .perform(click())               // click() is a ViewAction
  .check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion
  • ViewMatchers – Used to find a view in an activity. There are a bunch of different kinds of matchers. For example: withId(R.id.menu_search), withText(“Search”), withTag(“custom_tag”) .
  • ViewActions – Used to interact with a view. For example: click(), doubleClick(), swipeUp(), typeText().
  • ViewAssertations – Used to make assertions that certain views possess specific properties. For example:  doesNotExist(), isAbove(), isBelow().

There is a great cheat sheet for the different Espresso methods that can be found in pdf form here: android-espresso-testing.pdf. It is worth mentioning that ordinary hamcrest matchers can be used when writing Espresso tests. Methods such as not(), allOf()  and anyOf() are valid.

Writing Espresso UI Tests

If you can recall, in post #2 we covered what dependencies will need to be added in order to write espresso tests. Now we will cover writing an Espresso test.

  1. Create a folder androidTestMock. The tests in this folder will only run on the mock variant and not on the production variant. Then make a directory that matches the main package name. In that directory, add a new class called UserSearchActivityTest. Your project should then look like this:androidTestMock folder
  2. We will start by writing a basic test that ensures that when the activity is started, the text “Start typing to search” is displayed:
    public class UserSearchActivityTest {
    
        @Rule
        public ActivityTestRule<UserSearchActivity> testRule = new ActivityTestRule<>(UserSearchActivity.class);
    
        @Test
        public void searchActivity_onLaunch_HintTextDisplayed(){
            //Given activity automatically launched
            //When user doesn't interact with the view
            //Then
            onView(withText("Start typing to search"))
                  .check(matches(isDisplayed()));
        }
    }

    The @Rule  ActivityTestRule specifies which activity this test will run with. In this case this test will run with UserSearchActivity. This will automatically start up the UserSearchActivity. Passing extra parameters will indicate if you want the activity to auto start or not.

    The test searchActivity_onLaunch_HintTextDisplayed() is quite simple. It searches in the view for the text and asserts that the text is visible on the UI.

  3. The next test is slightly more complicated:
     @Test
        public void searchText_ReturnsCorrectlyFromWebService_DisplaysResult() {
            //Given activity is automatically launched
    
            //When
            onView(allOf(withId(R.id.menu_search), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))).perform(
                    click());  // When using a SearchView, there are two views that match the id menu_search - one that represents the icon, and the other the edit text view. We want to click on the visible one.
            onView(withId(R.id.search_src_text)).perform(typeText("riggaroo"), pressKey(KeyEvent.KEYCODE_ENTER));
    
            //Then
            onView(withText("Start typing to search")).check(matches(not(isDisplayed())));
            onView(withText("riggaroo - Rebecca Franks")).check(matches(isDisplayed()));
            onView(withText("Android Dev")).check(matches(isDisplayed()));
            onView(withText("A unicorn")).check(matches(isDisplayed()));
            onView(withText("riggaroo2 - Rebecca's Alter Ego")).check(matches(isDisplayed()));
        }

    After typing into the SearchView and pressing enter, we assert that the dummy results are displayed on the UI.

  4. We have now written tests for the positive scenarios, we should add a test for the negative case too. We will need to adjust the MockGithubUserRestServiceImpl in order to allow it to return custom error observables if required.
      private static Observable dummyGithubSearchResult = null;
    
        public static void setDummySearchGithubCallResult(Observable result) {
            dummyGithubSearchResult = result;
        }
        
        @Override
        public Observable<UsersList> searchGithubUsers(final String searchTerm) {
            if (dummyGithubSearchResult != null) {
                return dummyGithubSearchResult;
            }
            return Observable.just(new UsersList(usersList));
        }

    In the code above, a method was created in order to set a dummy observable for the search results. That observable will be returned if it is not null when searchGithubUsers() is called.

  5. Now we can create a test that checks if the error is displayed on the UI.
      @Test
        public void searchText_ServiceCallFails_DisplayError(){
            String errorMsg = "Server Error";
            MockGithubUserRestServiceImpl.setDummySearchGithubCallResult(Observable.error(new Exception(errorMsg)));
    
            onView(allOf(withId(R.id.menu_search), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))).perform(
                    click());  // When using a SearchView, there are two views that match the id menu_search - one that represents the icon, and the other the edit text view. We want to click on the visible one.
            onView(withId(R.id.search_src_text)).perform(typeText("riggaroo"), pressKey(KeyEvent.KEYCODE_ENTER));
    
           onView(withText(errorMsg)).check(matches(isDisplayed()));
    
        }

    In this test, we first ensure that the service will return an exception. Then we assert that the error message is displayed on the UI.

  6. Let’s run the tests:

Passing_UI_Tests

They all pass!

Code Coverage in Android

In order to know how effective your tests are, it is great to get code coverage metrics.

  1. To enable code coverage on your UI tests, add testCoverageEnabled = true  to your build.gradle:
     buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            }
            debug {
                testCoverageEnabled = true
            }
        }
  2. Run the task createMockDebugCoverageReport. You will then find the HTML report located here: app/build/reports/coverage/mock/debug/index.html.

Code Coverage Report in Android

Yay – we have 82% coverage just with the mock UI test. Taking into consideration the coverage reports we saw in post #4 and this post, it gives us a good indication of the test coverage of our entire app. Now we can iteratively go back and try cover more areas of our code.

PS – Code coverage currently doesn’t work with the Jack compiler. I switched to use Retrolambda in order to get the code coverage report to work. If you are interested in learning more, check out this branch.

Conclusion

We have finished writing our feature. Whew! 6 blog posts later. There is obviously a lot more testing that can be completed in this app. Non-functional tests such as testing how your app behaves on devices with low memory or with poor network connectivity can also be added.
That concludes the series on an “Introduction to Automated Android Testing”. I hope you enjoyed reading this series. If you enjoyed it, be sure to subscribe to the blog to receive future updates and share the post with your friends.

Further Reading

Subscribe to Blog via Email

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


If you would like to see more posts from me, please consider buying me a cupcake to keep them coming.

[buy_cupcake]


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

Ty for sharing this. But i think your series have stopped on the most interesting place, because it definitely lacks a guide about best practices of dagger 2 usage for android functional testing, doesn’t it? 🙂

Comments are closed.