Android 4
12. Activity Testing - Part B
- 12.0 Activity Testing
- 12.1 Sample Code
- 12.2 Setting Up the Emulator
- 12.3 Setting Up the Project
- 12.4 Creating Test Case Class
- 12.5 Running the Test
We now have a test project SpinnerActivityTest, and the basic structure of a test application also called SpinnerActivityTest.
The basic structure includes all the files and directories we need to build and run a test application, except for the class that contains our tests, the test case class.
The next step is to define the test case class. In this tutorial, we'll be creating a test case class that includes:
- Test setup: This use of the JUnit setUp() method demonstrates some of the tasks we might perform before running an Android test.
- Testing initial conditions: This test demonstrates a good testing technique. It also demonstrates that with Android instrumentation we can look at the application under test before the main activity starts. The test checks that the application's important objects have been initialized. If the test fails, we then know that any other tests against the application are unreliable, since the application was running in an incorrect state.
The purpose of testing initial conditions is not the same as using setUp(). The JUnit setUp() runs once before each test method, and its purpose is to create a clean test environment. The initial conditions test runs once, and its purpose is to verify that the application under test is ready to be tested. - Testing the UI: This test shows how to control the main application's UI with instrumentation, a powerful automation feature of Android testing.
- Testing state management: This test shows some techniques for testing how well the application maintains state in the Android environment. Remember that to provide a satisfactory user experience, our application must never lose its current state, even if it's interrupted by a phone call or destroyed because of memory constraints. The Android activity lifecycle provides ways to maintain state, and the SpinnerActivity application uses them. The test shows the techniques for verifying that they work.
Android tests are contained in a special type of Android application that contains one or more test class definitions. Each of these contains one or more test methods that do the actual tests. In this tutorial, we will first add a test case class, and then add tests to it.
We first choose an Android test case class to extend. We choose from the base test case classes according to the Android component we're testing and the types of tests we're doing. In this tutorial, the application under test has a single simple activity, so the test case class will be for an Activity component. Android offers several, but the one that tests in the most realistic environment is ActivityInstrumentationTestCase2, so we'll use it as the base class. Like all activity test case classes, ActivityInstrumentationTestCase2 offers convenience methods for interacting directly with the UI of the application under test.
To add ActivityInstrumentationTestCase2 as the base test case class, follow these steps:
- In the Package Explorer, expand the test project SpinnerActivityTest if it is not open already.
- Within SpinnerActivityTest, expand the src/ folder and then the package marker for com.android.example.spinner.test.
Right-click on the package name and select New > Class: - In the wizard, enter the following:
- Name: "SpinnerActivityTest".
This becomes the name of our test class. - Superclass: "android.test.ActivityInstrumentationTestCase2<SpinnerActivity>".
The superclass is parameterized, so we have to provide it our main application's class name.
Do not change any of the other settings. Click Finish. - Name: "SpinnerActivityTest".
- We now have a new file SpinnerActivityTest.java in the project.
- To resolve the reference to SpinnerActivity, add the following import:
import com.android.example.spinner.SpinnerActivity;
To ensure that the test application is instantiated correctly, we must set up a constructor that the test runner will call when it instantiates our test class. This constructor has no parameters, and its sole purpose is to pass information to the superclass's default constructor. To set up this constructor, enter the following code in the class:
public SpinnerActivityTest() { super("com.android.example.spinner", SpinnerActivity.class); } // end of SpinnerActivityTest constructor definition
This calls the superclass constructor with the Android package name (com.android.example.spinner) and main activity's class (SpinnerActivity.class) for the application under test. Android uses this information to find the application and activity to test:
package com.android.example.spinner.test; import android.test.ActivityInstrumentationTestCase2; import com.android.example.spinner.SpinnerActivity; public class SpinnerActivityTest extends ActivityInstrumentationTestCase2{ public SpinnerActivityTest() { super("com.android.example.spinner", SpinnerActivity.class); } // end of SpinnerActivityTest constructor definition }
We're now ready to add tests, by adding test methods to the class.
The setUp() method is invoked before every test. We use it to initialize variables and clean up from previous tests. We can also use the JUnit tearDown() method, which runs after every test method. The tutorial does not use it.
The method we are going to add does the following:
- super.setUp(). Invokes the superclass constructor for setUp(), which is required by JUnit.
- Calls setActivityInitialTouchMode(false). This turns off touch mode in the device or emulator. If any of our test methods send key events to the application, we must turn off touch mode before we start any activities; otherwise, the call is ignored.
- Stores references to system objects. Retrieves and stores a reference to the activity under test, the Spinner widget used by the activity, the SpinnerAdapter that backs the widget, and the string value of the selection that is set when the application is first installed. These objects are used in the state management test. The methods invoked are:
- getActivity(). Gets a reference to the activity under test (SpinnerActivity). This call also starts the activity if it is not already running.
- findViewById(int). Gets a reference to the Spinner widget of the application under test.
- getAdapter(). Gets a reference to the adapter (an array of strings) backing the spinner.
@Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(false); mActivity = getActivity(); mSpinner = (Spinner) mActivity.findViewById( com.android.example.spinner.R.id.Spinner01 ); mPlanetData = mSpinner.getAdapter(); } // end of setUp() method definition
Add these members to the test case class:
private SpinnerActivity mActivity; private Spinner mSpinner; private SpinnerAdapter mPlanetData;
Add these imports:
import android.widget.Spinner; import android.widget.SpinnerAdapter;
We now have the the complete setUp() method.
The initial conditions test verifies that the application under test is initialized correctly. It is an illustration of the types of tests we can run, so it is not comprehensive. It verifies the following:
- The item select listener is initialized. This listener is called when a selection is made from the spinner.
- The adapter that provides values to the spinner is initialized.
- The adapter contains the right number of entries.
The actual initialization of the application under test is done in setUp(), which the test runner calls automatically before every test. The verifications are done with JUnit Assert calls. As a useful convention, the method name is testPreConditions():
public void testPreConditions() { assertTrue(mSpinner.getOnItemSelectedListener() != null); assertTrue(mPlanetData != null); assertEquals(mPlanetData.getCount(),ADAPTER_COUNT); } // end of testPreConditions() method definition
Add this member:
public static final int ADAPTER_COUNT = 9;
Now create a UI test that selects an item from the Spinner widget. The test sends key events to the UI with key events. The test sends key events to the UI with key events. The test confirms that the selection matches the result we expect.
To work with the spinner, the test has to request focus for it and then set it to a known position. The test uses requestFocus() and setSelection() to do this. Both of these methods interact with a View in the application under test, so we have to call them in a special way.
Code in a test application that interacts with a View of the application under test must run in the main application's thread, also known as the UI thread. To do this, we use the Activity.runOnUiThread() method. We pass the code to runOnUiThread() in an anonymous Runnable object. To set the Java statements in the Runnable object, we override the object's run() method.
To send key events to the UI of the application under test, we use the sendKeys() method. This method does not have to run on the UI thread, since Android uses instrumentation to pass the key events to the application under test.
The last part of the test compares the selection made by sending the key events to a pre-determined value. This tests that the spinner is working as intended. The following sections show us how to add the code for this test.
- Get focus and set selection: Create a new method public void testSpinnerUI(). Add code to request focus for the spinner and set its position to default or initial position, "Earth". This code is run on the UI thread of the application under test:
public void testSpinnerUI() { mActivity.runOnUiThread( new Runnable() { public void run() { mSpinner.requestFocus(); mSpinner.setSelection(INITIAL_POSITION); } // end of run() method definition } // end of anonymous Runnable object instantiation ); // end of invocation of runOnUiThread }
Add the following member to the test case class.public static final int INITIAL_POSITION = 0;
- Make a selection: Send key events to the spinner to select one of the items. To do this, open the spinner by "clicking" the center keypad button (sending a DPAD_CENTER key event) and then clicking (sending) the down arrow keypad button five times. Finally, click the center keypad button again to highlight the desired item. Add the following code:
this.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); for (int i = 1; i <= TEST_POSITION; i++) { this.sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); } // end of for loop this.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
Add the following member to the test case class:public static final int TEST_POSITION = 5;
This sets the final position of the spinner to "Saturn" (the spinner's backing adapter is 0-based). - Check the result: Query the current state of the spinner, and compare its current selection to the expected value. Call the method getSelectedItemPosition() to find out the current selection position, and then getItemAtPosition() to get the object corresponding to that position (casting it to a String). Assert that this string value matches the expected value of "Saturn":
mPos = mSpinner.getSelectedItemPosition(); mSelection = (String)mSpinner.getItemAtPosition(mPos); TextView resultView = (TextView) mActivity.findViewById( com.android.example.spinner.R.id.SpinnerResult ); String resultText = (String) resultView.getText(); assertEquals(resultText,mSelection); } // end of testSpinnerUI() method definition
Add the following members to the test case class:private String mSelection; private int mPos;
Add the following imports to the test case class:import android.view.KeyEvent; import android.widget.TextView;
Pause here to run the tests we have. The procedure for running a test application is different from running a regular Android application. We run a test application as an Android JUnit application. To see how to do this, see Running the Tests and Seeing the Results.
Eventually, we will see the SpinnerActivity application start, and the test application controlling it by sending it key events. We will also see a new JUnit view in the Explorer pane, showing the results of the test. The JUnit view is documented in a following section, Running the Test and Seeing the Results.
We now write two tests that verify that SpinnerActivity maintains its state when it is paused or terminated. The state, in this case, is the current selection in the spinner. When users make a selection, pause or terminate the application, and then resume or restart it, they should see the same selection.
Maintaining state is an important feature of an application. Users may switch from the current application temporarily to answer the phone, and then switch back. Android may decide to terminate and restart an activity to change the screen orientation, or terminate an unused activity to regain storage. In each case, users are best served by having the UI return to its previous state (except where the logic of the application dictates otherwise). SpinnerActivity manages its state in these ways:
- Activity is hidden. When the spinner screen (the activity) is running but hidden by some other screen, it stores the spinner's position and value in a form that persists while the application is running.
- Application is terminated. When the activity is terminated, it stores the spinner's position and value in a permanent form. The activity can read the position and value when it restarts, and restore the spinner to its previous state.
- Activity re-appears. When the user returns to the spinner screen, the previous selection is restored.
- Application is restarted. When the user starts the application again, the previous selection is restored.
An application can manage its state in other ways as well, but these are not covered in this tutorial.
When an activity is hidden, it is paused. When it re-appears, it resumes. Recognizing that these are key points in an activity's life cycle, the Activity class provides two callback methods onPause() and onResume() for handling pauses and resumes. SpinnerActivity uses them for code that saves and restores state.
If we like to learn more about the difference between losing focus/pausing and killing an application, refer to the Activity Lifecycle section.
The first test verifies that the spinner selection is maintained after the entire application is shut down and then restarted. The test uses instrumentation to set the spinner's variables outside of the UI. It then terminates the activity by calling Activity.finish(), and restarts it using the instrumentation method getActivity(). The test then asserts that the current spinner state matches the test values.
The second test verifies that the spinner selection is maintained after the activity is paused and then resumed. The test uses instrumentation to set the spinner's variables outside of the UI and then force calls to the onPause() and onResume() methods. The test then asserts that the current spinner state matches the test values.
Notice that these tests make limited assumptions about the mechanism by which the activity manages state. The tests use the activity's getters and setters to control the spinner. The first test also knows that hiding an activity calls onPause(), and bringing it back to the foreground calls onResume(). Other than this, the tests treat the activity as a "black box".
To add the code for testing state management across shutdown and restart, follow these steps:
- Add the test method testStateDestroy(), then set the spinner selection to a test value:
public void testStateDestroy() { mActivity.setSpinnerPosition(TEST_STATE_DESTROY_POSITION); mActivity.setSpinnerSelection(TEST_STATE_DESTROY_SELECTION);
- Terminate the activity and restart it:
mActivity.finish(); mActivity = this.getActivity();
- Get the current spinner settings from the activity:
int currentPosition = mActivity.getSpinnerPosition(); String currentSelection = mActivity.getSpinnerSelection();
- Test the current settings against the test values:
assertEquals(TEST_STATE_DESTROY_POSITION, currentPosition); assertEquals(TEST_STATE_DESTROY_SELECTION, currentSelection); } // end of testStateDestroy() method definition
Add the following members to the test case class:public static final int TEST_STATE_DESTROY_POSITION = 2; public static final String TEST_STATE_DESTROY_SELECTION = "Earth";
To add the code for testing state management across a pause and resume, follow these steps:
- Add the test method testStatePause():
@UiThreadTest public void testStatePause() {
The @UiThreadTest annotation tells Android to build this method so that it runs on the UI thread. This allows the method to change the state of the spinner widget in the application under test. This use of @UiThreadTest shows that, if necessary, we can run an entire method on the UI thread. - Set up instrumentation. Get the instrumentation object that is controlling the application under test. This is used later to invoke the onPause() and onResume() methods:
Instrumentation mInstr = this.getInstrumentation();
- Set the spinner selection to a test value:
mActivity.setSpinnerPosition(TEST_STATE_PAUSE_POSITION); mActivity.setSpinnerSelection(TEST_STATE_PAUSE_SELECTION);
- Use instrumentation to call the Activity's onPause():
mInstr.callActivityOnPause(mActivity);
Under test, the activity is waiting for input. The invocation of callActivityOnPause(android.app.Activity) performs a call directly to the activity's onPause() instead of manipulating the activity's UI to force it into a paused state. - Force the spinner to a different selection:
mActivity.setSpinnerPosition(0); mActivity.setSpinnerSelection("");
This ensures that resuming the activity actually restores the spinner's state rather than simply leaving it as it was. - Use instrumentation to call the Activity's onResume():
mInstr.callActivityOnResume(mActivity);
Invoking callActivityOnResume(android.app.Activity) affects the activity in a way similar to callActivityOnPause. The activity's onResume() method is invoked instead of manipulating the activity's UI to force it to resume. - Get the current state of the spinner:
int currentPosition = mActivity.getSpinnerPosition(); String currentSelection = mActivity.getSpinnerSelection();
- Test the current spinner state against the test values:
assertEquals(TEST_STATE_PAUSE_POSITION,currentPosition); assertEquals(TEST_STATE_PAUSE_SELECTION,currentSelection); } // end of testStatePause() method definition
Add the following members to the test case class:public static final int TEST_STATE_PAUSE_POSITION = 4; public static final String TEST_STATE_PAUSE_SELECTION = "Jupiter";
- Add the following imports:
import android.app.Instrumentation; import android.test.UiThreadTest;
Here is the final version of code: SpinnerActivityTest.java:
package com.android.example.spinner.test; import com.android.example.spinner.SpinnerActivity; import android.view.KeyEvent; import android.widget.Spinner; import android.widget.SpinnerAdapter; import android.widget.TextView; import android.test.ActivityInstrumentationTestCase2; import android.app.Instrumentation; import android.test.UiThreadTest; public class SpinnerActivityTest extends ActivityInstrumentationTestCase2{ private SpinnerActivity mActivity; private Spinner mSpinner; private SpinnerAdapter mPlanetData; private String mSelection; private int mPos; public static final int ADAPTER_COUNT = 9; public static final int INITIAL_POSITION = 0; public static final int TEST_POSITION = 5; public static final int TEST_STATE_DESTROY_POSITION = 2; public static final String TEST_STATE_DESTROY_SELECTION = "Earth"; public static final int TEST_STATE_PAUSE_POSITION = 4; public static final String TEST_STATE_PAUSE_SELECTION = "Jupiter"; public SpinnerActivityTest() { super("com.android.example.spinner", SpinnerActivity.class); } // end of SpinnerActivityTest constructor definition @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(false); mActivity = getActivity(); mSpinner = (Spinner) mActivity.findViewById( com.android.example.spinner.R.id.Spinner01 ); mPlanetData = mSpinner.getAdapter(); } // end of setUp() method definition public void testPreConditions() { assertTrue(mSpinner.getOnItemSelectedListener() != null); assertTrue(mPlanetData != null); assertEquals(mPlanetData.getCount(),ADAPTER_COUNT); } // end of testPreConditions() method definition public void testSpinnerUI() { mActivity.runOnUiThread( new Runnable() { public void run() { mSpinner.requestFocus(); mSpinner.setSelection(INITIAL_POSITION); } // end of run() method definition } // end of anonymous Runnable object instantiation ); // end of invocation of runOnUiThread this.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); mPos = mSpinner.getSelectedItemPosition(); mSelection = (String)mSpinner.getItemAtPosition(mPos); TextView resultView = (TextView) mActivity.findViewById( com.android.example.spinner.R.id.SpinnerResult ); String resultText = (String) resultView.getText(); assertEquals(resultText,mSelection); } // end of testSpinnerUI() method definition public void testStateDestroy() { mActivity.setSpinnerPosition(TEST_STATE_DESTROY_POSITION); mActivity.setSpinnerSelection(TEST_STATE_DESTROY_SELECTION); mActivity.finish(); mActivity = this.getActivity(); int currentPosition = mActivity.getSpinnerPosition(); String currentSelection = mActivity.getSpinnerSelection(); assertEquals(TEST_STATE_DESTROY_POSITION, currentPosition); assertEquals(TEST_STATE_DESTROY_SELECTION, currentSelection); } // end of testStateDestroy() method definition @UiThreadTest public void testStatePause() { Instrumentation mInstr = this.getInstrumentation(); mActivity.setSpinnerPosition(TEST_STATE_PAUSE_POSITION); mActivity.setSpinnerSelection(TEST_STATE_PAUSE_SELECTION); mInstr.callActivityOnPause(mActivity); mActivity.setSpinnerPosition(0); mActivity.setSpinnerSelection(""); mInstr.callActivityOnResume(mActivity); int currentPosition = mActivity.getSpinnerPosition(); String currentSelection = mActivity.getSpinnerSelection(); assertEquals(TEST_STATE_PAUSE_POSITION,currentPosition); assertEquals(TEST_STATE_PAUSE_SELECTION,currentSelection); } // end of testStatePause() method definition }
The simplest way to run the SpinnerActivityTest test case is to run it directly from the Package Explorer.
To run the SpinnerActivityTest test, follow these steps:
- In the Package Explorer, right-click the project SpinnerActivityTest at the top level, and then select Run As > Android JUnit Test:
- We will see the emulator start. When the unlock option is displayed (its appearance depends on the API level we specified for the AVD), unlock the home screen.
- The test application starts. We see a new tab for the JUnit view, next to the Package Explorer tab:
This view contains two sub-panes. The top pane summarizes the tests that were run, and the bottom pane shows failure traces for highlighted tests.
At the conclusion of a successful test run, this is the view's appearance:
The upper pane summarizes the test:
- Total time elapsed for the test application(labeled Finished after
seconds). - Number of runs (Runs:) - the number of tests in the entire test class.
- Number of errors (Errors:) - the number of program errors and exceptions encountered during the test run.
- Number of failures (Failures:) - the number of test failures encountered during the test run. This is the number of assertion failures. A test can fail even if the program does not encounter an error.
- A progress bar. The progress bar extends from left to right as the tests run. If all the tests succeed, the bar remains green. If a test fails, the bar turns from green to red.
- A test method summary. Below the bar, we see a line for each class in the test application. To look at the results for the individual methods in a test, click the arrow at the left to expand the line. We see the name of each test method. To the right of the name, we see the time taken by the test. We can look at the test's code by double-clicking its name.
The lower pane contains the failure trace. If all the tests are successful, this pane is empty. If some tests fail, then if we highlight a failed test in the upper pane, the lower view contains a stack trace for the test. This is demonstrated in the next section.
If we run the test application and nothing seems to happen, look for the JUnit view. If we do not see it, we may have run the test application as a regular Android application. Remember that we need to run it as an Android JUnit application.
Previous sections:
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization