Android Testing part 3: Espresso tests from 0 to 1
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:
- “Number of repos found: {a number}” in a happy case
- “Something went wrong” when there is a network problem
- “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.
- Android Studio menu: Run >> Record Espresso Test
- Select the device and wait for app to be ready
- Type “android” into the search input textfield
- Click the search magnifier icon in the soft keyboard
- Click Add assertments
- 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:
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.
- First, we will replace our single implementation GitHubRepository with GithubRepository interface + 2 implementations (RealGithubRepoImpl and FakeGithubRepoImpl)
- 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?
- 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 customAppScope
, several screens will need GitHubRepoRepository, only ActivityABC will need ABCPresenter (customActivityScope)
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:
- 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: