Automated unit tests for an Android app are necessary for its long-term quality. Unit tests help to test one unit of your code (for example a class). This helps in catching and identifying bugs or regressions very early in the development cycle. In this article, we are going to see how we can write unit tests for our Android app. In Android, Unit Tests can be of two types:
- Local unit tests – which run on the development machine itself not on an actual machine
- Instrumented unit test – which runs on an actual Android device.
Creating Unit tests with JUnit and Mocking objects with Mockito
We will start by creating local unit tests for an Android app that adds two numbers. The app contains the following helper class
package com.testsinandroid;
import android.support.annotation.VisibleForTesting;
public class NumberAdder {
private final MainActivity mMainActivity;
public NumberAdder(MainActivity activity) {
mMainActivity = activity;
}
public void performAddition() {
double number1 = mMainActivity.getFirstNumber();
double number2 = mMainActivity.getSecondNumber();
if(!isNumberValid(number1) || !isNumberValid(number2)) {
throw new RuntimeException("invalid numbers");
}
double result = number1 + number2;
mMainActivity.setAdditionResult(result);
}
@VisibleForTesting
boolean isNumberValid(double number) {
if(number > 0) {
return true;
} else {
return false;
}
}
}
And also contains the activity which uses this class
package com.testsinandroid;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
public class MainActivity extends Activity {
EditText firstNumber;
EditText secondNumber;
TextView addResult;
Button btnAdd;
NumberAdder numberAdder = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
firstNumber = (EditText)findViewById(R.id.txtNumber1);
secondNumber = (EditText)findViewById(R.id.txtNumber2);
addResult = (TextView)findViewById(R.id.txtResult);
btnAdd = (Button)findViewById(R.id.btnAdd);
if(numberAdder == null) {
numberAdder = new NumberAdder(this);
}
btnAdd.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
numberAdder.performAddition();
}
});
}
public double getFirstNumber() {
return Double.parseDouble(firstNumber.getText().toString());
}
public double getSecondNumber() {
return Double.parseDouble(secondNumber.getText().toString());
}
public void setAdditionResult(double result) {
addResult.setText(Double.toString(result));
}
}
The activity layout is as follows
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">
<EditText
android:id="@+id/txtNumber1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="2"
android:inputType="number" >
<requestFocus />
</EditText>
<EditText
android:id="@+id/txtNumber2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txtNumber1"
android:ems="2"
android:inputType="number" >
</EditText>
<Button
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="46dp"
android:layout_below="@+id/txtNumber2"
android:text="Add" />
<TextView
android:id="@+id/txtResult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btnAdd"
android:textAppearance="?android:attr/textAppearanceMedium" />
</RelativeLayout>
Once we have our app ready, we now have two main units to test in our code
- NumberAdder – This is a plain Java class which takes the MainActivity as a dependency.
- MainActivity – The activity which shows the UI.
As NumberAdder
is a plain Java class, one can use Junit to test such classes.
JUnit is a simple framework to write repeatable unit tests. When we are testing one unit (NumberAdder in this case) all other classes which NumberAdder depends on can be mocked out. A good framework for mocking out dependencies in Java is Mockito. To add JUnit and Mockito as test dependencies in our project add the following to build.gradle
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:25.0.1'
// Required -- JUnit 4 framework
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
}
To write a Junit + Mockito test for NumberAdder create a file NumberAdderTest.java in the folder src/test/java/com/testsinandroid with the following content
package com.testsinandroid;
import com.testsinandroid.MainActivity;
import com.testsinandroid.NumberAdder;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class NumberAdderTest {
@Mock
MainActivity mMockMainActivity;
@Test
public void testIsNumberValid() {
//setup
//test
NumberAdder numberAdder = new NumberAdder(mMockMainActivity);
assert(numberAdder.isNumberValid(55.0));
}
@Test
public void testIsNumberNotValid() {
//setup
//test
NumberAdder numberAdder = new NumberAdder(mMockMainActivity);
assertFalse(numberAdder.isNumberValid(-55.0));
}
@Test
public void testPerformAddition() {
//setup
when(mMockMainActivity.getFirstNumber())
.thenReturn(10.0);
when(mMockMainActivity.getSecondNumber())
.thenReturn(11.0);
//test
NumberAdder numberAdder = new NumberAdder(mMockMainActivity);
numberAdder.performAddition();
//verify
verify(mMockMainActivity).setAdditionResult(21.0);
}
}
The above code creates three tests, which are annotated with the @Test annotation. The test is run using MockitoJUnitRunner. This runner injects a mock object for each field annotated with @Mock. MainActivity is mocked in the above test. In the first two tests, we are doing an assert based on the value returned from the function. In the third test, we set up the values that are to be returned when getFirstNumber and getSecondNumber are called. Then we verify if the method setAdditionResult with the correct value on the mMockMainActivity.
Once we have written these local JUnit tests we can run them using the gradle command. The build will be successful if all the tests pass or fail if they don’t.
./gradlew test
Write Tests using Roboelectric
Robolectric is a Unit testing framework for Android. With Roboelectric you can run the android unit test on JVM on your workstation. This becomes very handy to unit test your Android code on your workstation. One can fail the build if tests are not passed.
To add Roboelectric to your test dependencies, update your dependencies as follows
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:25.0.1'
// Add Junit and mockito as testing dependencies
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
//For Roboelectric
testCompile "org.robolectric:robolectric:3.0"
}
Once we have added the dependency, we can add the following test for MainActivity
package com.testsinandroid;
import org.junit.Test;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import static org.junit.Assert.*;
import static org.junit.Assert.assertTrue;
import org.robolectric.RobolectricGradleTestRunner;
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {
MainActivity activity;
EditText firstNumber;
EditText secondNumber;
TextView addResult;
Button btnAdd;
@Before
public void setUp() {
activity = Robolectric.setupActivity(MainActivity.class);
firstNumber = (EditText)activity.findViewById(R.id.txtNumber1);
secondNumber = (EditText)activity.findViewById(R.id.txtNumber2);
addResult = (TextView)activity.findViewById(R.id.txtResult);
btnAdd = (Button) activity.findViewById(R.id.btnAdd);
}
@Test
public void testMainActivityAddition() {
//setup
firstNumber.setText("12.2");
secondNumber.setText("13.3");
//test
btnAdd.performClick();
//verify
assertEquals(25.5, Double.parseDouble(addResult.getText().toString()), 0.0);
}
}
In the above test, we create the MainActivityTest. It is run it with the RobolectricGradleTestRunner. Then we use the Roboelectric API ‘Robolectric.setupActivity’ which instantiates the activity. It also calls its lifecycle methods like ‘OnCreate’, ‘onStart’ etc. Then we get the different view elements in the activity and set the values. Once the values are set we perform a click on the button. Then we finally verify that the result TextView has the appropriate value by using ‘assertEquals’.
As shown in the above example Roboelectric makes it easy to write a unit test for your Android code.
Writing Android Instrumentation tests
Android lets you write instrumentation unit tests. These are tests which run on an actual Android device. As you are testing on the actual device you don’t need to mock the Android classes. Testing the android components like activity etc get easy with an intrumentation test. But the problem is that you need an actual device to run the test. These cannot run it on your workstation.
To add instrumentation update your Gradle dependencies as follows
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:25.0.1'
// Add Junit and mockito as testing dependencies
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
//For Roboelectric
testCompile "org.robolectric:robolectric:3.0"
//For Instrumentation tests
androidTestCompile 'com.android.support:support-annotations:25.0.1'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
}
To write an intrumentation test for ‘MainActivity’ create a file MainActivityIntrumentationTest.java. The file should be in the folder src/androidTest/java/com/testsinandroid with the following content
package com.testsinandroid;
import android.app.Instrumentation;
import android.test.ActivityInstrumentationTestCase2;
import android.test.TouchUtils;
import android.test.ViewAsserts;
import android.view.View;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import static org.junit.Assert.*;
import android.test.UiThreadTest;
public class MainActivityIntrumentationTest extends ActivityInstrumentationTestCase2<MainActivity> {
MainActivity activity;
EditText firstNumber;
EditText secondNumber;
TextView addResult;
Button btnAdd;
public MainActivityIntrumentationTest() {
super(MainActivity.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
setActivityInitialTouchMode(true);
activity = getActivity();
firstNumber = (EditText)activity.findViewById(R.id.txtNumber1);
secondNumber = (EditText)activity.findViewById(R.id.txtNumber2);
addResult = (TextView)activity.findViewById(R.id.txtResult);
btnAdd = (Button) activity.findViewById(R.id.btnAdd);
}
@UiThreadTest
public void testMainActivityAddition() {
//setup
firstNumber.setText("12.2");
secondNumber.setText("13.3");
//test
btnAdd.performClick();
//verify
assertEquals(25.5, Double.parseDouble(addResult.getText().toString()), 0.0);
}
}
In the above code, our test class inherits from ActivityInstrumentationTestCase2. ActivityInstrumentationTestCase2 lets us write test for Activity. In the constructor of this class, we should pass the activity under test (‘MainActivity’ in our case). Then we override the ‘setUp’ method in which we get the activity object and other UI elements of the activity. Then we write a test which is similar to the one we wrote in the previous section. The test has an annotation ‘@UiThreadTest’. This will make this test run on UI thread as we have some UI operations in the test. We can run the instrumentation test after connecting a device using the command
./gradlew connectedCheck
Conclusion
In this article, we have seen many ways of writing unit tests for your Android code. Depending upon your needs for the project you can have one or many types of above tests in your project. Automated unit tests have a lot of long-term benefits. They should be thought of and estimated as the part of your development effort itself. The above-described frameworks help to write unit tests for your android app. They do the most of the heavy lifting under the hood for you so you can focus on the test. So have fun writing unit tests in you next Android app.
Frequently Asked Questions (FAQs) about Writing Tests for Android Development
What are the key principles of testing in Android development?
The key principles of testing in Android development include understanding the test pyramid, which is a concept that helps developers to create a balanced and sustainable set of tests by suggesting how many of each type of test (unit, integration, and UI tests) to create. Another principle is to test early and often, which means that testing should be integrated into the development process from the beginning and conducted regularly to catch issues as soon as possible. Lastly, developers should aim to write tests that are fast, reliable, and independent from each other.
How can I write local unit tests in Android Studio?
Local unit tests in Android Studio are written in the src/test directory. These tests run on the JVM and do not have access to functional Android framework APIs. They are typically used to test business logic, algorithms, and interactions between classes. To write a local unit test, you need to create a new test class, define test methods within this class, and then use assertions to verify the expected behavior.
What are the benefits of using AndroidJUnitRunner?
AndroidJUnitRunner is a test runner that allows you to run JUnit 3 or JUnit 4 style test classes on Android devices. The benefits of using AndroidJUnitRunner include the ability to use Android test context, which provides access to databases and preferences, and the ability to run tests across different devices and configurations. It also supports both instrumented and unit tests, making it a versatile tool for Android testing.
How can I write instrumented tests in Android Studio?
Instrumented tests in Android Studio are written in the src/androidTest directory. These tests have access to functional Android framework APIs and they run on actual Android devices or emulators. They are typically used to test user interactions, performance, and device integration. To write an instrumented test, you need to create a new test class, define test methods within this class, and then use assertions to verify the expected behavior.
What is the role of Espresso in Android testing?
Espresso is a testing framework provided by Android to write reliable user interface tests. It provides APIs for writing UI tests that simulate user interactions within a single app. Espresso tests can run on actual devices or emulators and they can be integrated into your development workflow to run automatically with your other tests.
How can I use Mockito in Android testing?
Mockito is a popular mocking framework used in unit testing. In Android testing, Mockito can be used to mock objects and verify that certain methods were called on those objects. This is particularly useful when you want to isolate the class you are testing and ensure that it behaves correctly in response to different inputs or states of its dependencies.
What is the purpose of the @Before and @After annotations in JUnit?
The @Before and @After annotations in JUnit are used to define methods that are executed before and after each test method. The @Before annotated method is typically used to set up the test environment, while the @After annotated method is used to clean up the test environment. This ensures that each test method is run in a clean state, which helps to prevent tests from affecting each other.
How can I run tests in Android Studio?
To run tests in Android Studio, you can right-click on the test class or method and select ‘Run’. You can also use the ‘Run’ button in the toolbar. For instrumented tests, you need to have an Android device or emulator connected. The test results will be displayed in the ‘Run’ window, where you can see which tests passed or failed and view detailed error messages.
What is the difference between unit tests and integration tests in Android development?
Unit tests in Android development are used to test individual components or functions in isolation, while integration tests are used to test the interaction between multiple components. Unit tests are typically faster and easier to write, but they may not catch issues that arise when components interact with each other. Integration tests are more complex and take longer to run, but they provide a more comprehensive test of your app’s functionality.
How can I improve the reliability of my Android tests?
To improve the reliability of your Android tests, you should aim to write tests that are independent, deterministic, and repeatable. This means that each test should not depend on the state of other tests, it should produce the same result given the same input, and it should be able to be run multiple times in any order. You should also use mocking and stubbing to isolate the code you are testing and control its dependencies.
Abbas is a software engineer by profession and a passionate coder who lives every moment to the fullest. He loves open source projects and WordPress. When not chilling around with friends he's occupied with one of the following open source projects he's built: Choomantar, The Browser Counter WordPress plugin, and Google Buzz From Admin.