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:
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:
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 requestsUserRepository
– queries backend API for user dataLoginActivity
– 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!