A Comprehensive Guide to Unit Testing in Android

As an experienced Android developer, you know that building robust, bug-free apps requires rigorous testing. And unit testing serves as the foundation. When done right, unit testing leads to higher quality code, faster development cycles, and apps that delight users.

This guide aims to make you a unit testing expert by clearly explaining the what, why, and how – from basic concepts to advanced integrations.

Why Unit Testing Matters in Android Development

Before we dive into specifics, it‘s important to understand the critical role unit testing plays in releasing quality Android applications:

Android unit testing statistics

Unit testing catches up to 70% of bugs before code reaches production (Source: ResearchGate)

Finds Bugs Early

By testing small units of code in isolation, issues can be identified at the source before propagating across your app. Fixing bugs later in development prolongs projects and allows more bugs to be introduced in the meantime.

Prevents Regression

Unit tests detect when new changes break or alter existing functionality that previously worked. Tests fail if unintended changes occur, pinpointing exactly where new bugs were introduced.

Refactor with Confidence

Refactoring improves code structure without changing behavior. This leads to cleaner, more maintainable codebases over time. But refactoring also risks introducing tricky-to-notice bugs. Unit testing allows large refactors while ensuring full functionality remains intact.

Documents Functionality

Well-designed unit tests serve as living documentation describing how classes and methods are intended to operate from a functional perspective.

Let‘s explore a real example demonstrating the above benefits…

A Case Study on Preventing Regression Bugs

Recently our team set out to improve the performance of an app‘s home feed. This involved updating the data caching mechanisms and image loading libraries.

While performance tests showed promising improvements, our product manager soon reported issues with buttons not functioning and screens failing to navigate as expected.

Thankfully, our existing unit test coverage exposed these unintended regressions:

HomeFeedViewModelTest
  ✖ whenLikeButtonClicked_updatesDatabase (500ms)

ImageLoadingUtilTest    
  ✖ givenImageUrl_returnsBitmap (500ms)  

By catching these regressions quickly through unit testing, we avoided shipping broken functionality to users.

Now that you‘ve seen unit testing benefits in a real scenario, let‘s dive into more specifics…

Key Terminology

First, let‘s define some key terminology:

Unit – The smallest testable part of an application, like a function or class. Good candidates for unit testing tend to have defined inputs and expected outputs.

Unit Test – A type of test that exercises an individual unit in isolation to verify against expected behavior. Multiple related unit tests comprise a test suite.

Test Suite – Contains all unit tests targeting a specific feature or component.

Mocking – Isolating dependencies by simulating their interfaces to avoid relying on their real implementations during testing.

Test Coverage – Metrics tracking what percentage of code is executed by tests, aim for 70%+ coverage.

Overview of Android Unit Testing Approaches

There are two primary approaches to Android unit testing:

Local Unit Tests

  • Reside within Android project‘s test folder
  • Executed directly on local JVM
  • Fast execution since tests run locally
  • Cannot access Android framework classes

Instrumentation Tests

  • Special JVM running from an Android device
  • Able to access Android SDK classes and UI testing
  • Slower execution
  • Requires emulator or physical device

This guide focuses specifically on local unit testing techniques given faster test execution times.

Now let‘s jump into configuration…

Step-by-Step: Configuring Unit Test Environment

Setting up your environment is essential before writing test cases. Here are the exact steps to configure Android Studio for unit testing:

1. Set up Directory Structure

Start by creating a test directory with Java packages mirroring your main code structure:

Android Studio test folders

This separates test code from production code.

2. Add Unit Test Dependencies

Inside your module-level build.gradle, add JUnit and Mockito libraries:

dependencies {

    testImplementation ‘junit:junit:4.13.2‘
    testImplementation "org.mockito:mockito-core:3.+"

}

JUnit provides base testing capabilities, while Mockito aids with mocking objects.

3. Create Test Class

Within your test packages, create a test class like:

import org.junit.Test;

public class UserRepositoryTest {

}

Now your environment is ready for adding test cases!

Anatomy of a Unit Test Method

Well-structured unit test methods follow a common blueprint:

1. Arrange

Set up objects, inputs, mock dependencies, or other preconditions required to exercise the code under test:

UserRepository repository = new UserRepository(); 
String inputUsername = "foobar";

2. Act

Invoke the method being tested, passing any required parameters:

User resultUser = repository.findUser(inputUsername);

3. Assert

Verify that actual outcomes match expected outcomes:

assertEquals(inputUsername, resultUser.username);

Let‘s combine these AAA sections into a complete test method:

@Test
public void findUser_whenUserExists_returnsUser() {

    // Arrange 
    UserRepository repository = new UserRepository();
    String inputUsername = "foobar";

    // Act
    User resultUser = repository.findUser(inputUsername);

    // Assert
    assertEquals(inputUsername, resultUser.username);

}

Now let‘s explore testing common Android components…

Testing Android Component Lifecycles

Activities, fragments, services and more…all have unique lifecycles to test:

Activities

Use ActivityScenario to manage activity state transitions:

@Test
public void whenLaunchMainActivity_showsProgressBar() {

    // Launch activity
    ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class);

    // Check UI in RESUMED state 
    scenario.onActivity(activity -> {
        ProgressBar progressBar = activity.findViewById(R.id.progress_bar);
        assertTrue(progressBar.isShown());
    });

    // Close activity
    scenario.close();            
}

Fragments

Similarly, exercise fragments through lifecycle methods:

@Test 
public void whenFragmentStarted_makesNetworkCall() {

    // Initialize mock network
    MockResponse mockResponse = new MockResponse().setBody("{}");
    mockWebServer.enqueue(mockResponse);

    // Launch fragment
    FragmentScenario<SignupFragment> scenario = FragmentScenario.launchInContainer(SignupFragment.class);

    // Verify interaction occured 
    verify(mockWebServer).takeRequest();

    scenario.close();
}

Testing lifecycles will reveal issues that only occur during specific transitions.

Effective Techniques for Common Scenarios

Android apps frequently rely on:

  • Async operations
  • Database I/O
  • Network requests
  • SharedPreferences
  • Permissions
  • Sensor data

Testing code using above functionality poses challenges. Here are effective techniques to test them.

Async Operations

Use callbacks to continue assertions after completing:

@Test 
public void whenFetchUser_returnsResponseAsync() {

    // Initialize mock API response
    MockResponse mockResponse = new MockResponse().setBody("{}");
    mockWebServer.enqueue(mockResponse);

    // Kick off async operation
    viewModel.fetchUser(user -> {

        // Assert after async completion 
        assertNotNull(user);

    }); 

}

Database & Network Layers

Leverage @Mock and @InjectMocks to isolate database or network calls:

@Mock 
PostDao postDao;

@InjectMocks
PostRepository repository;  

@Test
public void getPosts_returnsMockResults() {

    // Stub mock DAO response
    List<Post> stubPosts = Arrays.asList(
        new Post("1", "Title"));
    given(postDao.getPosts()).willReturn(stubPosts);

    // Fetch posts
    List<Post> posts = repository.getPosts();

    // Verify mocked data returned
    Assert.assertEquals(stubPosts, posts);

}

This avoids messy setup just to test business logic.

Permissions & Hardware

Use real or custom shadows to simulate Android behavior:

@Config(shadows = [MyPermissionShadow.class])
@Test
fun requestLocationUpdates_grantsPermission() {

    var granted = false;

    locationManager.requestLocationUpdates {
        granted = true;
    } 

    assertTrue(granted)

}

Custom shadows override framework methods to provide test controls.

Best Practices for Unit Test Design

Well-designed unit tests optimize your effectiveness testing code. Here are best practices to follow:

Keep Tests Laser-Focused

Each test case should target one specific method or scenario. Testing multiple things will increase complexity.

Isolate External Factors

Avoid relying on out-of-scope moving parts. Mock network calls, databases, UIs, and other dependencies.

Validate Edge Cases

Exercise invalid parameters, connectivity issues, data anomalies – things that rarely occur but still could.

Design Independent Tests

Tests should not depend on execution order or state modified by other tests. Manage shared state carefully.

Use Parameterized Tests

Runs the same test logic repetitively with different inputs and expected outputs. Maximizes code reuse.

Follow AAA Structure

The Arrange-Act-Assert test structure improves readability.

By following best practices, your test suite will be robust and maintainable as complexity increases.

Top Tools for Android Unit Testing

Here are popular open-source tools for building Android test suites:

Tool Description
JUnit De-facto testing framework on Android and Java
Mockito Mocking framework for isolating dependencies
MockWebServer Simulates API responses for testing
Roboelectric Allows Android unit testing without emulator
Espresso Tool for automated Android UI testing

Putting It All Together

Now that you understand the fundamentals of unit test design and have an overview of helpful libraries – let‘s walk through an real-life example.

Imagine we need to add login functionality with the following user stories:

  • As a user, I want to login with my email & password
  • As a user, I want to see an error if invalid credentials provided

We need to test:

  • LoginViewModel – handles form validation and requests
  • UserRepository – queries backend API for user data
  • LoginActivity – displays UI and routes on success/failure

Here is how you can leverage principles covered in this guide to test each component:

LoginViewModel

  • Use simple JUnit tests to exercise input validation logic
  • Parameterize tests to cover multiple input permutations
  • Assert form states and expected view model outputs

UserRepository

  • Mock network client with stubbed login API response
  • Test API is called with correct parameters
  • Validate repository returns expected user object

LoginActivity

  • Utilize ActivityScenario to test lifecycle states
  • Stub view model to return fixed login results
  • Assert UI displays correct views for success/failure

By breaking functionality into small testable units, we can thoroughly validate the login feature with unit testing techniques covered in this guide.

The end result is higher confidence in releasing code changes and reduced debugging time fixing issues reported by users.

Keep Learning!

You now have a comprehensive overview of unit testing concepts tailored to Android development.

Be sure to apply these principles within your team‘s testing practices. Look at expanding to instrumentation tests on emulators and real devices to catch issues related to Android SDK integrations.

For more resources check out:

As you build unit testing knowledge through hands-on practice, feel free to reach out with any other questions!

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.