Categories
android testing

Introduction to Android Testing – Part 3

In the previous two blog posts I covered how to get setup with testing in Android and we created a sample app that we will be continuing to develop in this blog post. If you missed those two posts, I suggest reading part 1 and part 2.

In this post, we will look at getting a list of users from the Github API and writing unit tests for it. We will be starting from the following repo at this checkpoint.

Create Web Service Calls

To consume the Github API we will be using Retrofit and RxJava. I am not going to explain RxJava or Retrofit in this series. If you aren’t familiar with RxJava, I suggest reading these articles. If you haven’t used Retrofit before I suggest reading this.

In order to get a list of users for a search term, we will need to use the following endpoint (I am also applying a page size of 2 for this demo because of API call limiting):

https://api.github.com/search/users?per_page=2&q=rebecca

To get more user information (such as a user’s bio and location), we need to make a subsequent call:

https://api.github.com/users/riggaroo
  1.  To start consuming these endpoints, we should create the JSON objects that they are returning and include them in our project. I normally generate them online here. Let’s create the following classes: User class and UsersList class:
    package za.co.riggaroo.gus.data.remote.model;
    
    import com.google.gson.annotations.Expose;
    import com.google.gson.annotations.SerializedName;
    
    public class User {
    
        @SerializedName("login")
        @Expose
        private String login;
        @SerializedName("id")
        @Expose
        private Integer id;
        @SerializedName("avatar_url")
        @Expose
        private String avatarUrl;
        @SerializedName("gravatar_id")
        @Expose
        private String gravatarId;
        @SerializedName("url")
        @Expose
        @SerializedName("type")
        @Expose
        private String type;
        @SerializedName("name")
        @Expose
        private String name;
        @SerializedName("location")
        @Expose
        private String location;
        @SerializedName("email")
        @SerializedName("bio")
        @Expose
        private String bio;
        @SerializedName("followers")
        @Expose
        private Integer followers;
        @SerializedName("following")
        @Expose
        private Integer following;
        @SerializedName("created_at")
        @Expose
        private String createdAt;
        @SerializedName("updated_at")
        @Expose
        private String updatedAt;
    
       ... //see more at https://github.com/riggaroo/GithubUsersSearchApp/blob/testing-tutorial-part3-complete/app/src/main/java/za/co/riggaroo/gus/data/remote/model/User.java
    
    }
    package za.co.riggaroo.gus.data.remote.model;
    
    import com.google.gson.annotations.Expose;
    import com.google.gson.annotations.SerializedName;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class UsersList {
    
        @SerializedName("total_count")
        @Expose
        private Integer totalCount;
        @SerializedName("items")
        @Expose
        private List<User> items = new ArrayList<User>();
    
        public Integer getTotalCount() {
            return totalCount;
        }
    
        public void setTotalCount(Integer totalCount) {
            this.totalCount = totalCount;
        }
    
        public List<User> getItems() {
            return items;
        }
    
        public void setItems(List<User> items) {
            this.items = items;
        }
    
    }
    

    Once the models are created, navigate to the GithubUserRestService. This is where we will create our Retrofit calls.

    import retrofit2.http.GET;
    import retrofit2.http.Path;
    import retrofit2.http.Query;
    import rx.Observable;
    import za.co.riggaroo.gus.data.remote.model.User;
    import za.co.riggaroo.gus.data.remote.model.UsersList;
    
    public interface GithubUserRestService {
    
        @GET("/search/users?per_page=2")
        Observable<UsersList> searchGithubUsers(@Query("q") String searchTerm);
    
        @GET("/users/{username}")
        Observable<User> getUser(@Path("username") String username);
    }

    The first network call will perform a search to get a list of users and the second network call will get more details about a user.

  2. Navigate to UserRepositoryImpl. This is where we will combine the two network calls and transform the data into a view that will be used in the front end. This is using RxJava to first get a list of users for the search term and then for each user it does another network call to find out more of the user’s information. (If you had implemented this API by yourself, I would try make one network call return all the required info – as discussed in my Reducing Mobile Data Usage Talk).
    import java.io.IOException;
    import java.util.List;
    
    import rx.Observable;
    import za.co.riggaroo.gus.data.remote.GithubUserRestService;
    import za.co.riggaroo.gus.data.remote.model.User;
    
    public class UserRepositoryImpl implements UserRepository {
    
       private GithubUserRestService githubUserRestService;
    
        public UserRepositoryImpl(GithubUserRestService githubUserRestService) {
            this.githubUserRestService = githubUserRestService;
        }
    
        @Override
        public Observable<List<User>> searchUsers(final String searchTerm) {
            return Observable.defer(() -> githubUserRestService.searchGithubUsers(searchTerm).concatMap(
                    usersList -> Observable.from(usersList.getItems())
                            .concatMap(user -> githubUserRestService.getUser(user.getLogin())).toList()))
                    .retryWhen(observable -> observable.flatMap(o -> {
                        if (o instanceof IOException) {
                            return Observable.just(null);
                        }
                        return Observable.error(o);
                    }));
        }
    
    }
    

    In the above code, I am creating an observable by using Observable.defer() , this means that the observables code will only run once it has a subscriber (Not like Observable.create() which run when it is created). As corrected from the comments below, Observable.create() is an unsafe RxJava API and it shouldn’t be used.

    When there is a subscriber, the githubUserRestService  is called to search with the searchTerm  provided. From there, I use concatMap  to take the list of users, emit them one by one into a new observable that then calls githubUserRestService.getUser() for each user in that list. That observable is then transformed into a list of users.

    A retry mechanism has also been defined on these network calls. retryWhen() will retry the observable when an IOException is thrown. An IOException is thrown by Retrofit when a user has no internet (you might want to add a terminating condition such as only retrying a certain number of times).

    You might notice that I am using lambda expressions in the code, you can do this by building your app with the new Jack toolchain. Read about how to enable building for Java 8 on Android here.

    Now we have a repository and two network calls to get a list of users! We should write tests for the code we have just written.

Unit Testing – What is Mockito?

In order to unit test our repository object, we are going to make use of Mockito. What is Mockito? Mockito is an open source testing framework for Java released under the MIT License. The framework allows the creation of test double objects (mock objects) in automated unit tests. (Wikipedia).

Mockito allows you to stub method calls and verify interactions with objects.

When we write unit tests, we need to think of testing a certain component in isolation. We should not test anything beyond what that class’ responsibility is. Mockito helps us achieve this separation.

Okay, let’s write some tests!

Writing Unit Tests for UserRepositoryImpl

  1. Select the UserRepositoryImpl class name and press “ALT + ENTER”. A dialog will pop up with the options to “Create Test”. Select that option and a new dialog will appear:Create Test Dialog
  2. You can select to generate methods but I generally leave the options unselected. It will then ask you to select a directory where the test should be placed. Select the “app/src/test” directory as we are writing a JUnit test that does not require an Android ContextSelect Test Directory - Automated Testing Android
  3. Now we are ready to set up our unit test. To do this, create a UserRepository object. We will also need to create a mock instance of GithubUserRestService as we won’t be directly hitting the API in this test. This test will just confirm that the transformations are done correctly within the UserRepository.  Below is the code to set up our unit tests:
        @Mock
        GithubUserRestService githubUserRestService;
    
        private UserRepository userRepository;
    
        @Before
        public void setUp() throws Exception {
            MockitoAnnotations.initMocks(this);
            userRepository = new UserRepositoryImpl(githubUserRestService);
        }

    The method that is annotated @Before will run before any unit test and it ensures that the Mock objects are setup before trying to use them.  We call MockitoAnnotations.initMocks()  in the setUp()  method and then create an instance of the UserRepository  using the mocked out github service.

  4. The first test we will will write will test that the GithubUserRestService  is called with the correct parameters. It will also test that it returns the expected result. Below is the example test I have written:
    @Test
        public void searchUsers_200OkResponse_InvokesCorrectApiCalls() {
            //Given
            when(githubUserRestService.searchGithubUsers(anyString())).thenReturn(Observable.just(githubUserList()));
            when(githubUserRestService.getUser(anyString()))
                    .thenReturn(Observable.just(user1FullDetails()), Observable.just(user2FullDetails()));
    
            //When
            TestSubscriber<List<User>> subscriber = new TestSubscriber<>();
            userRepository.searchUsers(USER_LOGIN_RIGGAROO).subscribe(subscriber);
    
            //Then
            subscriber.awaitTerminalEvent();
            subscriber.assertNoErrors();
    
            List<List<User>> onNextEvents = subscriber.getOnNextEvents();
            List<User> users = onNextEvents.get(0);
            Assert.assertEquals(USER_LOGIN_RIGGAROO, users.get(0).getLogin());
            Assert.assertEquals(USER_LOGIN_2_REBECCA, users.get(1).getLogin());
            verify(githubUserRestService).searchGithubUsers(USER_LOGIN_RIGGAROO);
            verify(githubUserRestService).getUser(USER_LOGIN_RIGGAROO);
            verify(githubUserRestService).getUser(USER_LOGIN_2_REBECCA);
        }
    
        private UsersList githubUserList() {
            User user = new User();
            user.setLogin(USER_LOGIN_RIGGAROO);
    
            User user2 = new User();
            user2.setLogin(USER_LOGIN_2_REBECCA);
    
            List<User> githubUsers = new ArrayList<>();
            githubUsers.add(user);
            githubUsers.add(user2);
            UsersList usersList = new UsersList();
            usersList.setItems(githubUsers);
            return usersList;
        }
    
        private User user1FullDetails() {
            User user = new User();
            user.setLogin(USER_LOGIN_RIGGAROO);
            user.setName("Rigs Franks");
            user.setAvatarUrl("avatar_url");
            user.setBio("Bio1");
            return user;
        }
    
        private User user2FullDetails() {
            User user = new User();
            user.setLogin(USER_LOGIN_2_REBECCA);
            user.setName("Rebecca Franks");
            user.setAvatarUrl("avatar_url2");
            user.setBio("Bio2");
            return user;
        }
    

    This test is split up into three sections namely: given, when, then. I separate my tests like this because it ensures your tests are structured and gets you thinking about the specific functionality you are testing. In this test, I am testing the following: Given the Github service returns certain users, when I search for users, the results should return and transform correctly.

    I find naming of tests is also quite important. The naming structure I like to follow is the following:

    [Name of method under test]_[Conditions of test case]_[Expected Result]

    So in this example, the name of the method is searchUsers_200OkResponse_InvokesCorrectApiCalls() . In this test case, a TestSubscriber is subscribed to the search query observable. Assertions are then done on the TestSubscriber  to ensure it has the expected results.

  5. The next unit test will test if an IOException  is thrown by the search service call then the network call will be retried.
     @Test
        public void searchUsers_IOExceptionThenSuccess_SearchUsersRetried() {
            //Given
            when(githubUserRestService.searchGithubUsers(anyString()))
                    .thenReturn(getIOExceptionError(), Observable.just(githubUserList()));
            when(githubUserRestService.getUser(anyString()))
                    .thenReturn(Observable.just(user1FullDetails()), Observable.just(user2FullDetails()));
    
            //When
            TestSubscriber<List<User>> subscriber = new TestSubscriber<>();
            userRepository.searchUsers(USER_LOGIN_RIGGAROO).subscribe(subscriber);
    
            //Then
            subscriber.awaitTerminalEvent();
            subscriber.assertNoErrors();
    
            verify(githubUserRestService, times(2)).searchGithubUsers(USER_LOGIN_RIGGAROO);
    
            verify(githubUserRestService).getUser(USER_LOGIN_RIGGAROO);
            verify(githubUserRestService).getUser(USER_LOGIN_2_REBECCA);
        }

    In this test, we are asserting that the githubUserRestService  was called twice and the other network calls were called once. We also assert that there were no terminating errors on the subscriber.

    Final Unit Test Code for UserRepositoryImpl

    I have added a few more tests than the ones described above. They test different cases but they follow the same concepts as described in the previous section. Below is the full test class for UserRepositoryImpl :

    public class UserRepositoryImplTest {
    
        private static final String USER_LOGIN_RIGGAROO = "riggaroo";
        private static final String USER_LOGIN_2_REBECCA = "rebecca";
        @Mock
        GithubUserRestService githubUserRestService;
    
        private UserRepository userRepository;
    
        @Before
        public void setUp() {
            MockitoAnnotations.initMocks(this);
            userRepository = new UserRepositoryImpl(githubUserRestService);
        }
    
        @Test
        public void searchUsers_200OkResponse_InvokesCorrectApiCalls() {
            //Given
            when(githubUserRestService.searchGithubUsers(anyString())).thenReturn(Observable.just(githubUserList()));
            when(githubUserRestService.getUser(anyString()))
                    .thenReturn(Observable.just(user1FullDetails()), Observable.just(user2FullDetails()));
    
            //When
            TestSubscriber<List<User>> subscriber = new TestSubscriber<>();
            userRepository.searchUsers(USER_LOGIN_RIGGAROO).subscribe(subscriber);
    
            //Then
            subscriber.awaitTerminalEvent();
            subscriber.assertNoErrors();
    
            List<List<User>> onNextEvents = subscriber.getOnNextEvents();
            List<User> users = onNextEvents.get(0);
            Assert.assertEquals(USER_LOGIN_RIGGAROO, users.get(0).getLogin());
            Assert.assertEquals(USER_LOGIN_2_REBECCA, users.get(1).getLogin());
            verify(githubUserRestService).searchGithubUsers(USER_LOGIN_RIGGAROO);
            verify(githubUserRestService).getUser(USER_LOGIN_RIGGAROO);
            verify(githubUserRestService).getUser(USER_LOGIN_2_REBECCA);
        }
    
        private UsersList githubUserList() {
            User user = new User();
            user.setLogin(USER_LOGIN_RIGGAROO);
    
            User user2 = new User();
            user2.setLogin(USER_LOGIN_2_REBECCA);
    
            List<User> githubUsers = new ArrayList<>();
            githubUsers.add(user);
            githubUsers.add(user2);
            UsersList usersList = new UsersList();
            usersList.setItems(githubUsers);
            return usersList;
        }
    
        private User user1FullDetails() {
            User user = new User();
            user.setLogin(USER_LOGIN_RIGGAROO);
            user.setName("Rigs Franks");
            user.setAvatarUrl("avatar_url");
            user.setBio("Bio1");
            return user;
        }
    
        private User user2FullDetails() {
            User user = new User();
            user.setLogin(USER_LOGIN_2_REBECCA);
            user.setName("Rebecca Franks");
            user.setAvatarUrl("avatar_url2");
            user.setBio("Bio2");
            return user;
        }
    
        @Test
        public void searchUsers_IOExceptionThenSuccess_SearchUsersRetried() {
            //Given
            when(githubUserRestService.searchGithubUsers(anyString()))
                    .thenReturn(getIOExceptionError(), Observable.just(githubUserList()));
            when(githubUserRestService.getUser(anyString()))
                    .thenReturn(Observable.just(user1FullDetails()), Observable.just(user2FullDetails()));
    
            //When
            TestSubscriber<List<User>> subscriber = new TestSubscriber<>();
            userRepository.searchUsers(USER_LOGIN_RIGGAROO).subscribe(subscriber);
    
            //Then
            subscriber.awaitTerminalEvent();
            subscriber.assertNoErrors();
    
            verify(githubUserRestService, times(2)).searchGithubUsers(USER_LOGIN_RIGGAROO);
    
            verify(githubUserRestService).getUser(USER_LOGIN_RIGGAROO);
            verify(githubUserRestService).getUser(USER_LOGIN_2_REBECCA);
        }
    
        @Test
        public void searchUsers_GetUserIOExceptionThenSuccess_SearchUsersRetried() {
            //Given
            when(githubUserRestService.searchGithubUsers(anyString())).thenReturn(Observable.just(githubUserList()));
            when(githubUserRestService.getUser(anyString()))
                    .thenReturn(getIOExceptionError(), Observable.just(user1FullDetails()),
                            Observable.just(user2FullDetails()));
    
            //When
            TestSubscriber<List<User>> subscriber = new TestSubscriber<>();
            userRepository.searchUsers(USER_LOGIN_RIGGAROO).subscribe(subscriber);
    
            //Then
            subscriber.awaitTerminalEvent();
            subscriber.assertNoErrors();
    
            verify(githubUserRestService, times(2)).searchGithubUsers(USER_LOGIN_RIGGAROO);
    
            verify(githubUserRestService, times(2)).getUser(USER_LOGIN_RIGGAROO);
            verify(githubUserRestService).getUser(USER_LOGIN_2_REBECCA);
        }
    
        @Test
        public void searchUsers_OtherHttpError_SearchTerminatedWithError() {
            //Given
            when(githubUserRestService.searchGithubUsers(anyString())).thenReturn(get403ForbiddenError());
    
            //When
            TestSubscriber<List<User>> subscriber = new TestSubscriber<>();
            userRepository.searchUsers(USER_LOGIN_RIGGAROO).subscribe(subscriber);
    
            //Then
            subscriber.awaitTerminalEvent();
            subscriber.assertError(HttpException.class);
    
            verify(githubUserRestService).searchGithubUsers(USER_LOGIN_RIGGAROO);
    
            verify(githubUserRestService, never()).getUser(USER_LOGIN_RIGGAROO);
            verify(githubUserRestService, never()).getUser(USER_LOGIN_2_REBECCA);
        }
    
    
        private Observable getIOExceptionError() {
            return Observable.error(new IOException());
        }
    
        private Observable<UsersList> get403ForbiddenError() {
            return Observable.error(new HttpException(
                    Response.error(403, ResponseBody.create(MediaType.parse("application/json"), "Forbidden"))));
    
        }
    }

Run the Unit Tests

Now after writing these tests, we need to run them, see if they pass and see how much of the code is covered by tests.

  1. To run the tests you can right click on the test class name and select “Run UserRepositoryImplTest  with Coverage”Run unit tests with coverage
  2. You will then see the results appear on the right hand side of Android Studio. Code Coverage Report - Unit test Android Studio

We have 100% unit test coverage on our UserRepositoryImpl  class. Yay!

In the next blog post, we will take a look at implementing the UI to display the set of search results and writing more tests for that. Be sure to subscribe so you don’t miss the next blog post in this series!

Subscribe to Blog via Email

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


If you enjoy my posts, please consider buying me a cupcake to keep them coming.
[buy_cupcake]

Links:

Defering observable code until subscription in RxJava

RxJavas retryWhen() and repeatWhen()

Github user search app on Github

12 replies on “Introduction to Android Testing – Part 3”

Hey! Glad to see somebody else writing about testing 🙂

Couple of comments if you don’t mind:

>Not like Observable.create() which run when it is created

This is not true, you pass lambda to `Observable.create()` which gets executed when Subscriber subscribes to the Observable -> it’s lazy as defer(). In your case I would recommend `Observable.fromCallable()`. (Anyway pls don’t use Observable.create(), it’s unsafe api of RxJava).

> User & UsersList

Code would be much safer and easy to support and test if such classes would be immutable (no setters)!

Hi Rebecca
Thanks for the post, I’ve been trying to test a repository that is basically the same that you have, but without using RxJava. Is it possible to do that or you recommend using Rxjava when working with Retrofit?

It is possible to do it without RxJava.
I can recommend using RxJava with Retrofit as they work really well together. Retry mechanisms, backoff mechanisms and error handling is a made a lot easier. It is also easy to choose specific threads to run certain operations on.

Not that relevant, I am using Android Studio 2.2 beta and the shortcut to generate the test file seems to be Control + Shift + T

Thanks you so much Rebecca,

I think that it will be really interesting if you can help us with mocking server connections.

Anyway, I’m glad to read this useful posts.

Thanks you again.

Thanks you so much Rebecca for great post.
Just in case if someone was looking out for writing this function without lambda

@Override
public Observable<List> searchUsers(final String searchTerm) {

return Observable.defer(new Func0<Observable<List>>() {
@Override
public Observable<List> call() {
return (githubUserRestService.searchGithubUsers(searchTerm)).concatMap(new Func1<UsersList, Observable>() {
@Override
public Observable call(UsersList usersList) {
return (Observable.from(usersList.getItems()).concatMap(new Func1<User, Observable>() {
@Override
public Observable call(User user) {
return (githubUserRestService.getUser(user.getLogin()));
}
}));
}
}).toList();
}
}).retryWhen(new Func1<Observable, Observable>() {
@Override
public Observable call(Observable observable) {
return observable.flatMap(new Func1<Throwable, Observable>() {
@Override
public Observable call(Throwable throwable) {
if(throwable instanceof IOException){
return Observable.just(null);
}
else{
return Observable.error(throwable);
}
}
});
}
});
}

Yea. But that still requires Java 8 and is crazy read. It’d be better to just understand lambdas and get on with it.

Hi Rebecca,

I’m having trouble with Observable in return statement. To be more precise, Im getting ERROR: Type mismatch: cannot convert from Observable to Observable<List>. I’m pretty new with RxJava so I don’t realy know where to start with troubleshooting. Can you please hand me some advice?

Comments are closed.