Android Testing part 3: Espresso tests from 0 to 1

Eric N
AndroidPub
Published in
7 min readApr 1, 2018

--

This is part 3 of my series of articles on Android testing.

Part 3: This article

We will use the same GitHub repository, starting from step 3a.

Overview

There are many kinds of applications out there. However, most can relate to this simple application which fetches data from a remote source and displays the data to the user.

Preparation

This tutorial will include Dagger 2. But it will go from the plainest and simplest Espresso test, adding extra stuffs only when necessary.

We will add a TextView called status below the search input and above the list of results. This view will display:

  1. Number of repos found: {a number}” in a happy case
  2. Something went wrong” when there is a network problem
  3. E10* — System error” if Github does not give us the repos user is searching for

We will test this TextView instead of the list of results as testing RecyclerView involves complication beyond this tutorial. Naturally we will have 3 test cases as described above.

We’ll start by using Android Studio’s GUI tool to create our Espresso test for that’s the easiest thing to do. Later on we will identify the limitations of this kind of tests and improve the tests gradually.

Step 3a (happy path, device connected to the Internet)

Code is here.

  1. Android Studio menu: Run >> Record Espresso Test
  2. Select the device and wait for app to be ready
  3. Type “android” into the search input textfield
  4. Click the search magnifier icon in the soft keyboard
  5. Click Add assertments
  6. Select the status TextView and click OK to save the generated MainActivityTest.java

(Though quite self-explanatory, the whole process can be found in a video here)

Run the test by clicking on the Run icon next to public class MainActivityTest {

Congratulations! You have just created your first Espresso test and ran it 🍾!

Pay attention to this code block:

ViewInteraction textView = onView(
allOf(withId(R.id.tv_status), withText(startsWith("Number of repos found: 668755")),
childAtPosition(
childAtPosition(
withId(android.R.id.content),
0),
1),
isDisplayed()));
textView.check(matches(withText(startsWith("Number of repos found: 668755"))));

Test may to fail because the number of repos on Github increases all the time. At the point of your test being recorded, there could be 649,716 repos matching “android” search keyword. A few minutes later, that number could well be 649,720 or more causing your test to fail.

The error message in Android Studio should be self-explanatory:

Expected: “Number of repos found: 668764” vs. Actual: “Number of repos found: 669446

We will improve our tests to better take care of that dynamic later on. For now, simply change the expected number of results in the code to make our test pass.

Remember that real world data like this is unpredictable and can break our tests anytime. Therefore, in most cases, it would be better to run hermetic tests with fake data. Our tests would be more reliable in such scenario.

It’s absolutely ok to start with failing tests. We will make them passed later on.

Now try one thing, turn your device’s Flight Mode on and run the test again. Test will definitely fail simply because our app cannot get data from Github without Internet. In real life, this can easily be the case for your Espresso tests. Have you considered the scenario where Internet is too slow or flaky or connection times out i.e. no Internet?

Step 3b Fake data to make tests hermetic and reliable

Code is here.

  1. First, we will replace our single implementation GitHubRepository with GithubRepository interface + 2 implementations (RealGithubRepoImpl and FakeGithubRepoImpl)
  2. We will use the Singleton pattern + Android build variants to provide our app with the appropriate implementations

In our Espresso test environment (everything under app/src/androidTest*/java/ folder), we will make sure FakeGithubRepoImpl is used instead of RealGithubRepoImpl using dependency injection (don’t freak out, explanation comes shortly :D)

Replacing GitHubRepository with GithubRepository interface + 2 implementations is easy with Android Studio. Simply extract GitHubRepository to interface and rename implementation class to RealGithubRepoImpl (fetch real data from GitHub API)

Remember to select the inner interface and our searchRepos method.

Next, we will leverage Android build variants to provide the above 2 different implementations of our GitHubRepository interface.

Create 2 flavours in your app/build.gradle:

android {    flavorDimensions "default"
productFlavors {
fake {
applicationIdSuffix ".fake"
versionNameSuffix "-fake"
}
real {
...
}
}
}

Right click on app/src folder and select New >> Folder >> Java Folder and select fakeDebug variant

Now switch to fakeDebug variant

Next, right click on app/src/fakeDebug/java and a class

tech.ericntd.githubsearch.repositories.FakeGithubRepoImpl

public class FakeGithubRepoImpl implements GitHubRepository {
@Override
public void searchRepos(@NonNull String query,
@NonNull GitHubRepositoryCallback callback) {

}
}

Instead of duplicating MainActivity in our 2 build variants

We will use Singleton pattern to inject an instance of the GitHubRepository interface into our activity:

final Retrofit retrofit = new Retrofit.Builder()
.baseUrl(“https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build();
final GitHubRepository repository = new GitHubRepository(retrofit.create(GitHubApi.class));
final SearchPresenterContract presenter = new SearchPresenter(this, GitHubRepoProvider.provide());

and create 2 GitHubRepoProvider classes under 2 build variants:

/app/src/real/java/tech.ericntd.githubsearch.search.GitHubRepoProvider

public class GitHubRepoProvider {
private static class SingletonHelper {
static Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build();
private static final GitHubRepository INSTANCE = new RealGitHubRepositoryImpl(retrofit
.create(GitHubApi.class));
}

public static GitHubRepository provide() {
return SingletonHelper.INSTANCE;
}
}

/app/src/fake/java/tech.ericntd.githubsearch.search.GitHubRepoProvider

public class GitHubRepoProvider {
private static class SingletonHelper {
private static final GitHubRepository INSTANCE = new FakeGithubRepoImpl();
}

public static GitHubRepository provide() {
return SingletonHelper.INSTANCE;
}
}

Open up Build Variants window in Android Studio, try switching between realDebug and fakeDebug to see how one of the 2 GitHubRepoProvider classes is resolved alternatively.

We are almost ready. We only need to fill in the details for FakeGithubRepoImpl.

public class FakeGithubRepoImpl implements GitHubRepository {
@Override
public void searchRepos(@NonNull String query,
@NonNull GitHubRepositoryCallback callback) {
List<SearchResult> resultList = new ArrayList<>();
resultList.add(new SearchResult("repo 1"));
resultList.add(new SearchResult("repo 2"));
resultList.add(new SearchResult("repo 3"));
SearchResponse searchResponse = new SearchResponse(resultList.size(), resultList);
Response<SearchResponse> response = Response.success(searchResponse);
callback.handleGitHubResponse(response);
}
}

Now, select fakeDebug build variant and run our Espresso test again, the test should fail as the status TextView is actually “Number of repos found: 3"

Update your test code to expect “Number of repos found: 3” and the test will pass.

Dagger makes dependency injection better

Code is here.

You may not have noticed but we have used dependency injection above to swap the real HTTP request with fake one

final Retrofit retrofit = new Retrofit.Builder()
.baseUrl(“https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build();
final GitHubRepository repository = new GitHubRepository(retrofit.create(GitHubApi.class));
final SearchPresenterContract presenter = new SearchPresenter(this, GitHubRepoProvider.provide());

Take note that the Presenter depends on the Repository and the Repository depends on the Retrofit object. In and actual production feature, there could be 10 instead of 3 objects in the dependency chain here.

Can you spot some problem here?

  1. There is way too much code here in your Activity/ Fragment

2. Difficult to see the important details

  • Presenter the the main object of interest here
  • The presenter depends on what? Can they be swapped through dependency injection for testing?

Dagger 2 is the best tool in the market for the job for reasons including but not limited to the following:

  • Dagger 2 is fast, all the object-creation code are generated at compile time
  • Logical organisation of dependencies e.g. your whole app needs your SharedPreferences wrapper classes (Singleton scope or custom AppScope, several screens will need GitHubRepoRepository, only ActivityABC will need ABCPresenter (custom ActivityScope)

Set up guide

It’s arguably quite complicated to set up Dagger compared to other tools like Toothpick. I will skip the details here as everything is included the Github branch above

There are 2 key classes here:

  1. AppModule

This provides all the non-activity specific/ app scope dependencies e.g. Retrofit. This is where we will provide the GitHubRepository instance for now.

@Module
public class AppModule {

@Provides
@Singleton
Retrofit provideRetrofit() {
return new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build();
}

@Provides
@Singleton
GitHubRepository provideGitHubRepo(Retrofit retrofit) {
return new RealGitHubRepositoryImpl(retrofit.create(GitHubApi.class));
}
}

Take note that we must have exactly 2 AppModule classes under app/src/fakeDebug/ and app/src/real/ folders and not under app/src/main or there will be a “duplicate class” compile error.

2. MainActivityModule

This contains all the dependencies MainActivity needs

@Module
public class MainActivityModule {

@Provides
SearchViewContract provideMainView(MainActivity mainActivity) {
return mainActivity;
}

@Provides
SearchPresenter provideMainPresenter(SearchViewContract view,
GitHubRepository gitHubRepository) {
return new SearchPresenter(view, gitHubRepository);
}
}

Here Dagger automatically figures out which GitHubRepository instance to use (provided in AppModule)

When your code base grows, these Dagger modules makes it easy to overview what dependencies are needed where. Similar to our humble GitHubRepositoryProvider class, we can simply create different AppModule or *ActivityModule classes in our “real” and “fake” build types to swap the actual data (slow and unreliable) and fake data (fast and reliable) for the sake of testing.

Relevant reads:

--

--