This page outlines the core tenets of testing Android apps, including the
central best practices and their benefits.
Benefits of testing
Testing is an integral part of the app development process. By running tests
against your app consistently, you can verify your app's correctness, functional
behavior, and usability before you release it publicly.
You can
manually
test your app by navigating through it. You might use
different devices and emulators, change the system language, and try to generate
every user error or traverse every user flow.
However, manual testing scales poorly, and it can be easy to overlook
regressions in your app's behavior.
Automated testing
involves using tools
that perform tests for you, which is faster, more repeatable, and generally
gives you more actionable feedback about your app earlier in the development
process.
Types of tests in Android
Mobile applications are complex and must work well in many environments. As
such, there are many types of tests.
Subject
For example, there are different types of tests depending on the
subject
:
- Functional testing
: does my app do what it's supposed to?
- Performance testing
: does it do it quickly and efficiently?
- Accessibility testing
: does it work well with accessibility services?
- Compatibility testing
: does it work well on every device and API level?
Scope
Tests also vary depending on
size
, or
degree of isolation
:
- Unit tests
or
small tests
only verify a very small portion of the app,
such as a method or class.
- End-to-end
tests or
big tests
verify larger parts of the app at the
same time, such as a whole screen or user flow.
- Medium tests
are in between and check the
integration
between two or
more units.
There are many ways to classify tests. However, the most important distinction
for app developers is where tests run.
Instrumented versus local tests
You can run tests on an Android device or on another computer:
- Instrumented tests
run on an Android device, either physical or emulated.
The app is built and installed alongside a
test app
that injects commands and
reads the state. Instrumented tests are usually UI tests, launching an app and
then interacting with it.
- Local tests
execute on your development machine or a server, so they're
also called
host-side tests
. They're usually small and fast, isolating
the subject under test from the rest of the app.
Not all unit tests are local, and not all end-to-end tests run on a device. For
example:
- Big local test
: You can use an Android simulator that runs locally, such
as
Robolectric
.
- Small instrumented test
: You can verify that your code works well with a
framework feature, such as a SQLite database. You might run this test on
multiple devices to check the integration with multiple versions of SQLite.
Examples
The following snippets demonstrate how to interact with the UI in an
instrumented UI test
that clicks on an element and verifies that another
element is displayed.
Espresso
// When the Continue button is clicked
onView(withText("Continue"))
.perform(click())
// Then the Welcome screen is displayed
onView(withText("Welcome"))
.check(matches(isDisplayed()))
Compose UI
// When the Continue button is clicked
composeTestRule.onNodeWithText("Continue").performClick()
// Then the Welcome screen is displayed
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
This snippet shows part of a
unit test
for a ViewModel (local, host-side
test):
// Given an instance of MyViewModel
val viewModel = MyViewModel(myFakeDataRepository)
// When data is loaded
viewModel.loadData()
// Then it should be exposing data
assertTrue(viewModel.data != null)
Defining a testing strategy
In an ideal world, you would test every line of code in your app on every device
that your app is compatible with. Unfortunately, this approach is too slow and
costly to be practical.
A good testing strategy finds an appropriate balance between the fidelity of a
test, its speed, and its reliability. The similarity of the test environment to
a real device determines the test’s fidelity. Higher fidelity tests run on
emulated devices or the physical device itself. Lower fidelity tests might run
on your local workstation’s JVM. High-fidelity tests are often slower and
require more resources, so not every test should be a high-fidelity test.
Flaky tests
Errors occur even in correctly designed and implemented test runs. For example,
when running a test on a real device, an automatic update might start in the
middle of a test and cause it to fail. Subtle race conditions in your code might
occur only a small percentage of the time. Tests that do not pass 100% of the
time are
flaky
.
Testable architecture
With a testable app architecture, the code follows a structure that allows you
to easily test different parts of it in isolation. Testable architectures have
other advantages, such as better readability, maintainability, scalability, and
reusability.
An architecture that is
not testable
produces the following:
- Bigger, slower, more flaky tests. Classes that can't be unit-tested might have
to be covered by bigger integration tests or UI tests.
- Fewer opportunities for testing different scenarios. Bigger tests are slower,
so testing all possible states of an app might be unrealistic.
To learn more about architecture guidelines, see the
guide to app
architecture
.
Approaches to decoupling
If you can extract part of a function, class, or module from the rest, testing
it is easier, and more effective. This practice is known as decoupling, and it
is the concept most important to testable architecture.
Common decoupling techniques include the following:
- Split an app into
layers
such as Presentation, Domain, and Data. You can
also split an app into
modules
, one per feature.
- Avoid adding logic to entities that have large dependencies, such as
activities and fragments. Use these classes as entry points to the framework and
move
UI and business logic
elsewhere, such as to a Composable, ViewModel, or
domain layer.
- Avoid direct
framework dependencies
in classes containing business logic.
For example,
don't use Android Contexts in ViewModels
.
- Make dependencies easy to
replace
. For example, use
interfaces
instead of concrete implementations. Use
Dependency injection
even if you don't use a DI framework.
Next steps
Now that you know why you should test and the two main types of tests, you can
read
What to test
.
Alternatively, if you want to create your first test and learn by doing, check
out the
Testing codelabs
.