Editor’s note: Zhao Wei interned with us from January to March 2019, before beginning his Full-Time National Service obligations. He worked on an impressive variety of software and hardware projects, including web apps, chatbots, native apps, and more. Here, he goes into a deep dive on what he learned about Android UI testing:
The Android team and community advocates testing. This includes unit, integration, and UI testing.
I’ll focus on UI testing for this post.
Testing brings about several benefits to the app development process:
- Acts as a specification for the app’s behaviour
- Verifies that the app behaves according to the specification
- Gives confidence that any changes made during development do not break existing functionality
The last point is especially important to apps undergoing major refactors and redesigns. Get Hacking Android is no exception.
The Initial State of Get Hacking Android
Get Hacking Android is an app that showcases micro:bit and maker projects. Tinkertanker released a first version over a year ago on the Play Store, but without any UI tests for the app. This meant that functionality could break during development without anyone noticing. (Unfortunately, some of these bugs did actually slip into production.)
The app is also undergoing a major redesign, and this impacts many areas of the code base. Breakages are likely to happen.
Writing UI tests can help avoid these issues. I’ve written some and documented what I’ve learnt below. This includes some of the patterns I’ve followed and the pitfalls I’ve encountered.
Preparing to Test
Before writing any tests, I set up the dependencies needed for testing. These include the Espresso and AndroidX Test libraries.
What Is Espresso?
Espresso is a UI testing library for Android. It provides APIs for writing idiomatic actions and assertions on the UI.
The logo for Espresso
Tests written with Espresso are concise and readable.
What Is AndroidX Test?
AndroidX Test is a set of libraries to ease testing on Android. It provides test harnesses and utilities that make writing tests simpler.
The logo for AndroidX Test
The JUnit and Truth extensions provide methods for fluent assertions. These exist under the androidx.test.ext namespace.
AndroidJUnitRunner makes it possible to run JUnit-style tests on Android devices. It exists under the androidx.test:runner namespace.
Android Test Orchestrator enhances test reliability by running tests in separate Instrumentation instances. Some of my tests were flaky and failed without it. It exists under the androidx.test:orchestrator namespace.
Understanding the Android Test File Structure
There are a few common rules to writing UI tests:
- Store all on-device UI tests in the
androidTestdirectory. - Store each test class in the same subdirectory under
androidTestas the class under test. - Give each test class the same name as the class under test, but suffixed with the word Test.
Side note: Store off-device UI and unit tests in the test directory instead.
Here’s an example.
I want to test an ExampleActivity class. I store it in app/src/main/java/com/example/example-app/ExampleActivity.java.
In this case, I should name my test class ExampleActivityTest. I should also store it in app/src/androidTest/java/com/example/example-app/ExampleActivityTest.java.
Writing My First Test Class
To get started, I first wrote a test class for a simple Activity in the app: IntroductionActivity.
There’s a bunch of different things going on at first glance. Let’s walk through each part in more detail.
Using ActivityScenario and ActivityScenarioRule
ActivityScenario is essential to testing Activities on Android. It manages the state and lifecycle events for the Activity under test. This eases Activity behaviour testing.
ActivityScenarioRule builds upon ActivityScenario. It launches an ActivityScenario before each test and cleans up after each test.
For IntroductionActivityTest, I used ActivityScenarioRule to launch IntroductionActivity before each test.
Writing My First Test Case
The IntroductionActivity displays a TextView with introductory text and a Button.
The first test verifies that the app displays these two views:
I used Espresso APIs here. They may be unfamiliar if you haven’t written UI tests before, but the intent of the code should be clear.
What Are These Methods?
The Android documentation provides a detailed explanation of common Espresso APIs. For convenience, I’ll give a quick introduction to each method I used.
onView() matches a View given one or more ViewMatchers objects.
check() checks the state of the matched View given one or more ViewAssertions objects.
matches() returns a ViewAssertion object that asserts on the given ViewMatcher objects.
isDisplayed()returns a ViewMatcher object that matches a displayed View.
Testing User Interactions
Clicking the Button in IntroductionActivity launches an Intent to start the MainActivity.
The next test verifies this interaction.
Naming Test Cases
I named each test case following a structured scheme. It goes like this: [action/condition]_[expected result]().
This naming scheme tells readers the intent of a test case at a glance.
In onClickButton_showsMainActivity(), onClickButton is the action and showsMainActivity is the expected result.
Structuring Code in Test Cases
The Android team recommends structuring test code based on Given-When-Then (GWT).
GWT splits code into three phases:
- Given: The initial state of the app.
- When: The action to carry out. This is like the action used in naming the test case.
- Then: The expected result. This is like the expected result used in naming the test case.
In onClickButton_showsMainActivity(), I separated the three parts with a newline.
There is no Given phase in the test.
onView(...) occurs during the When phase.
intended(...) occurs during the Then phase.
Performing ViewActions
Espresso provides ViewActions that perform actions like clicks, typing, and scrolls on Views.
I used perform(...) and click() to click on the Button in IntroductionActivity.
Verifying Intents
To verify that the app launches the correct Intent when clicking the Button, the test has to do two things:
- Capture the launched Intent
- Check that the Intent has the MainActivity as the component
First, I used the Espresso-Intents extension to capture Intents. It lives in the androidx.test.espresso:espresso-intents dependency.
I set up pre-test and post-test hooks using the Before and After JUnit annotations.
The pre-test hook initialises the Intent captor to capture launched Intents.
The post-test hook releases the Intent captor for garbage collection.
Note: An IntentsTestRule can automate initialisation and release for each test.
But IntentsTestRule also launches the Activity under test. This conflicts with the ActivityScenarioRule that I’ve set up.
Next, I used the intended() and hasComponent() methods to verify the captured Intent.
And there you have it! onClickButton_showsMainActivity() verifies that the app launches the MainActivity after clicking on the Button.
What’s Next?
I’ve walked through the process of writing my first UI test class for Get Hacking Android. I’ve documented common patterns like test naming schemes and file structure. I’ve introduced Espresso APIs used for handling Activities, Intents, and Views.
But that isn’t all.
Get Hacking Android uses Fragments for navigating between pages. It also performs network operations with Retrofit to fetch data from a backend.
Possibly in another article, I’ll explain testing Fragments with FragmentTransactions within ActivityScenarios. I’ll also explain waiting on network operations with Espresso idling resources.
Note: I used FragmentTransactions instead of the new FragmentScenario API. This is because Get Hacking Android has yet to migrate to AndroidX Fragments.
If you use AndroidX Fragments, you should check out FragmentScenarios instead.
I may also explain testing authentication and user inputs using different product flavours.
Further Reading
Here are some of the articles and talks I watched before starting on Android testing:
- Android Developers guide on Android testing
- Google I/O 2018 talk on writing unified tests with AndroidX Test
- Google I/O 2017 talk on test-driven development on Android
- Android Developers blog post on using different product flavours for testing
To end off, here’s a tweet by Guillermo Rauch
Editor’s note: Zhao Wei impressed us early on with his technical know-how, his willingness to learn, and his sheer speed in getting things done. As a result, he’s contributed to myriad projects during his internship, such as developing the above Android app, creating a couple of very useful Slack bots, helping build some Swift Playgrounds notebooks for our micro:bit courses, creating demo web apps for our classes, and much more. If you’re here looking for a testimonial for Zhao Wei, and this hasn’t impressed you sufficiently, email us for more enthusiastic praise.