ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

An Introduction to Testing Custom Views on Android

Damian Burke
ProAndroidDev
Published in
7 min readMay 8, 2018

--

If you’re working on a large app, your product designers have extraordinary wishes or you just like to split your app into small modules to ensure maintainability and separation of concerns — you’ll most likely end up implementing custom views at some point.

Writing a custom view is pretty easy. You can for example extend android.view.View or you can extend your favorite layout like a FrameLayout and inflate the XML for your custom view. Instead of including the whole view hierarchy in your layout files, all you have to do is include the custom view and use it.

Writing a custom view to encapsulate certain functions makes sense. Not every custom component has to be an Activity or Fragment. Smaller modules mean less responsibility and thus less room for errors. To further reduce errors, we are going to add some tests to ensure that everything is working as intended — i.e. the state is processed correctly.

Thinking about testing a custom view, you’d most likely consider UI tests with Espresso. UI tests are pretty cost intensive. It takes time to write them properly and it takes time to execute them, since you’ll need to boot an emulator or install on a physical device. To reduce both the time we have to spend writing the test and the time it takes to execute it, we’ll shift most of the testing into our unit test layer.

However, since at the moment our custom view class is just extending android.view.View or a layout, there’s no practical way to do this. Luckily, architectures like MVP and MVVM are not required to be used with Activity and Fragment exclusively. It’s just as easy to implement these architectures in custom views, to separate logic from the view itself — resulting in a unit-testable logic layer and a UI-testable view layer.

Let’s think about a custom view that is used to display the current user’s state. The view is supposed to display an image (the user’s avatar) and their given username.

Since not every user of our app will be logged in, we’ll also need a logged-out state. Also, we’d like to have a verified userbase, so we’ll require a verification of the user’s email address which we will display with a small badge.

Identified view states:

  • Logged out
  • Logged in (verified, verification pending, verification required)

This state directly depends on the user’s account. In our case, the account data class looks like this:

To differentiate between a logged out user and an active user, we’ve implemented a sealed class encapsulating both of them.

In a barebones custom view, we’d pass in the name, avatar and activated field (with an option to set the view to its logged-out state).

The custom view would then put these values into their respective text fields and display the right avatar or it would show the logged-out state. Since these methods are all operating on the custom view itself, it requires us to write UI tests to verify the behavior.

Before jumping into the tests, lets take a look at our custom view and refactor it a bit. For a quick example, we’ll refactor the view by utilizing MVP.

Identifying our model is quite easy: the Account.

Identifying the view is simple as well: It’s our custom view, but without the logic. Reducing the view to simple setter methods manipulating the state, we have to identify possible entry points into the view state. Our view contains a couple of components:

  • TextView for the username / logged out text
  • ImageView for the user’s avatar / logged out avatar
  • ImageView to show activated / pending state

For each of these possible states we’ll create a method in our View interface:

Our Presenter is going to be pretty straightforward, all it does is care about the view’s lifecycle (subscribing to data sources and handling its teardown by unsubscribing):

The first steps towards a testable custom view are done. We have successfully identified our entry and exit points between the logic layer and the view layer.

The next step is to actually refactor and/or implement our newly created interfaces. To keep it short, we’ll skip that — though if you’re interested, a full implementation can be found in the linked GitHub repository at the bottom.

Finally we can start writing some tests. We’ll start off with our unit tests to verify our logic and behavior. Our logic is in our presenter class.

For our unit tests, we’ll use the following dependencies:

To start, we’ll define the mock objects we’ll use in our test as well as a setup-function to initialize our mocks and our presenter before each executed test.

Each of our tests will only cover a single verification, to allow us to identify failures as small-grained as possible. This will allow us to spot bugs and their scope easily. Before writing unit tests, we’ll identify possible states, possible interactions with the presenter and how they are going to affect our view.

An incomplete list of possible things to test (the full list is in the repository):

  • logged out account hides badge
  • active account sets name
  • active account sets avatar
  • active account with pending activation shows badge

Each of these test cases is simple. Writing the tests will not take much of your time, but the more the project grows, the more valuable they will become. This is especially true as more developers work on the project concurrently.

An implementation of these tests could look like this:

Notice that:

  • Everything except for our test target (the presenter) is a mock
  • The presenter uses a data source to receive the data, which we can manipulate to trigger different states (logged in, logged out, …)
  • Each test verifies a single behavior for a specified set of conditions

Now that we’ve covered the logic side of our view, its time to take a look at UI tests. For the UI tests, we’ll use Espresso. With Espresso, we can verify our UI’s behavior. For example, that certain views are visible and contain the information they should contain at the right times. In Espresso tests, we can simulate certain user behaviors like tapping on buttons, swiping left and right or typing text. We can then check if the view is behaving correctly.

Espresso provides test rules to start Activities which are being tested. However, since we are focusing on custom views, we don’t really want to start the Activity our view is contained in, since that would be overkill. Ideally, we’d like to have the custom view separated to allow us to completely focus on the view itself, without distractions.

To achieve this, you might add a mock build flavor to apps which contain utilities for testing views (and Activities / Fragments). Doing this is pretty simple. Just change your module’s build.gradle and add the flavor.

In this example, the mock flavor will be used to store mock data sources, as well as our test utilities. The prod flavor (production) will contain the “real” implementations. Separating these with flavors offers a big advantage: none of your mocked code will appear in the compiled binary of your production application— it’s a separated environment.

Our view’s data source provides us with the user’s account:

In our production flavor, we’ll prepare the implementation of this interface and inject it into our custom view’s presenter. In our mock flavor, we can implement a mocked version of this which allows us to prepare and change the returned account at runtime.

Now, we still need a way to test our custom view with Espresso. For that, we’ll create a MockActivity in our mock flavor, which we can (just like the data source) manipulate before and during runtime. To display our custom view, we’ll need to be able to either change the used layout resource or to directly inject our view into the Activity before it is created. For this example, we’ll use a layout file.

We’re all set-up, now it’s time to finally test our custom view. We’ll create a class in our androidTest directory, and set-up the ActivityTestRule for our MockActivity.

Our MockActivity relies on us providing a layout resource ID, which will then be used to set the Activity’s content view. In our test, we’ll just wrap our custom view in a layout. The XML file is created in the mock flavor’s resource folder.

Coming back to our AccountViewTest, we’ll now set the MockActivity’s layout resource ID to our newly created one:

Now, each time we start our Activity with our ActivityTestRule, it’ll inflate our mock XML layout, thus display our custom view. Together with our mocked data source, we’re now able to manipulate the view’s state and test and verify its behavior.

An example of a single Espresso UI test with our MockActivity, a mocked data source and our custom view in a mock-only XML layout file:

This will successfully start up the Activity, display our custom view, set the provided account state to LoggedOut and verify that the displayed username matches our logged-out string as well as that the account validation badge is hidden.

In summary, this is quite a nice way to increase test coverage without too much complexity. Once the setup is prepared, it takes minimal effort to create UI tests for custom views, which can be verified and manipulated easily.

Combining this setup with a library like spoon will enable you to easily take screenshots and create informative test reports. This will not only help you find errors in added code, but also help product designers verify UI behavior on different devices, screen sizes, languages, etc.

Below this, you will find a link to a GitHub repository I’ve set up with a full example of a custom view with unit and UI tests.

If you liked this post, want to discuss it or simply stay in the loop — follow me on twitter:

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Responses (3)

Write a response