|
|
Introduction
Here we describe some features and helper functions that are specifically handy for acceptance test. These helpers fall into a few natural categories, which we'll examine in turn.
Remember also that this page may get stale, and the best references is the source.
Activity Stack
Instrumentations provide the ActivityMonitor class to examine transitions between activities. They can either mock out the target activity, or simply grab a reference to it once it resolves. The approach is powerful because of the flexibility of the intent filter that is used for detection. However, if a tester is concerned with only examining what activity the user is seeing, it can be a bit much.
An alternative way to monitor new activities in batch is to catch them as they are created by overriding callActivityOnResume. Positron does this for you and exposes the resulting collection to tests:
- activities() - Get positron's view of the activity stack, topmost first.
- activity() - Get just the top of the stack.
For example, a test could assertTrue(activity() instanceof MyActivity).
Thread Safety
Instrumentations run in parallel with (the UI thread of) their tested application. This introduces the possibility of race conditions when inspecting application data. To address this android provides the runOnMainSync method. It executes the given runnable on the UI thread, which usually means the target application can't do anything else while it is running. If all the data for a particular assertion is take from the application within one call to runOnMainSync, then no race condition can occur between those data. If all the assertions in one section of a test are meant to be simultaneously valid in a single 'instant', then it may be appropriate to wrap them all in a single call. However, after a while wrapping segments of test code inside anonymous inner classes can becoming cumbersome.
Suppose we pass runOnMainSync a Runnable that just blocked execution until some signal was received. If this call was made from a thread other than the one running the test, then the test itself wouldn't be blocked. Assuming the test controls when this runOnMainSync call is made and released, then it would be free to 'pause' execution of the target application and inspect its state. Later it could 'resume' the target's execution, to allow it to process events, for example. Wrap this functionality in a new class and call it a PauseButton. Its methods, which are exposed to tests, are:
- pause() - halt execution of the target application. Pausing a paused app is safe.
- resume() - resume execution of the paused application. Resuming a running app is safe.
It is important to emphasize that a paused application cannot process any events. For example, if a test called Instrumentation.sendStringSync on a paused application, the test would deadlock. The test would need to call resume(), then sendStringSync and optionally pause to return to the paused state. Since nobody wants to write that, test cases have access to a sendString method, which will "momentarily resume" the target application if needed. The other xxxSync methods also have equivalents. The xxxSync methods themselves are overrode to check for deadlock and throw an exception instead. They are otherwise unchanged.
Inspecting Views
activity() will give a test a reference to the last-resumed activity, but it is still up to the test to navigate through the activity's view layout to find and inspect individual widgets. This can be bulky. As an example, suppose the tested activity is a ListActivity with customized rows.
The test is interested in seeing the contents of one of the text views in the 2nd row:
ListActivity activity = (ListActivity)activity(); LinearLayout row = (LinearLayout)activity.getListView().getChildAt(1); CharSequence actual = ((TextView)row.getChildAt(0)).getText();
To help out, positron provides the at methods: at("listView.1.0.text") will fetch the same string as above. It uses a simple dot-separated syntax and reflection to call getters for each named property. If the 'property' is an index (integer), then we try to index the receiving object in a reasonable way. Arrays use literal [] indexing, and ViewGroups (such as ListViews) use getChildAt.
The variations of at tweak the return type, or allow inspection to start at something other than the current activity():
- Object at(String path) - Navigates path, starting from activity().
- <T> T at(Class<T> asA, Object from, String path) - The kitchen sink. Uses passed class as the return type, and starts inspection from the 2nd parameter.
- <T> T at(Class<T> asA, String path)
- View viewAt(String path)
- String stringAt(String path)
- int intAt(String path)
- boolean booleanAt(String path)
- double doubleAt(String path)
Sending Key Events
Tests have some methods to make pressing keys more concise.
- click() - Hit the big button in the middle (sendKeyDownUp(KEYCODE_DPAD_CENTER))
- press(KEY, "keys", KEY...) - Takes a varargs of ints and Strings, calling sendKeyDownUp or sendString on each, as appropriate. For example, one could press(UP, UP, "and away")
In addition, several KeyEvent.KEYCODE_* keycodes have aliases within tests:
// Mixin key codes with shorter names. /** KeyEvent.KEYCODE_DPAD_CENTER */ public final static int CLICK = KeyEvent.KEYCODE_DPAD_CENTER; /** KeyEvent.KEYCODE_DPAD_CENTER */ public final static int CENTER = KeyEvent.KEYCODE_DPAD_CENTER; /** KeyEvent.KEYCODE_DPAD_UP */ public final static int UP = KeyEvent.KEYCODE_DPAD_UP; /** KeyEvent.KEYCODE_DPAD_DOWN */ public final static int DOWN = KeyEvent.KEYCODE_DPAD_DOWN; /** KeyEvent.KEYCODE_DPAD_LEFT */ public final static int LEFT = KeyEvent.KEYCODE_DPAD_LEFT; /** KeyEvent.KEYCODE_DPAD_RIGHT */ public final static int RIGHT = KeyEvent.KEYCODE_DPAD_RIGHT; /** KeyEvent.KEYCODE_BACK */ public final static int BACK = KeyEvent.KEYCODE_BACK; /** KeyEvent.KEYCODE_CALL */ public final static int CALL = KeyEvent.KEYCODE_CALL; /** KeyEvent.KEYCODE_CAMERA */ public final static int CAMERA = KeyEvent.KEYCODE_CAMERA; /** KeyEvent.KEYCODE_CAP */ public final static int CAP = KeyEvent.KEYCODE_CAP; /** KeyEvent.KEYCODE_CLEAR */ public final static int CLEAR = KeyEvent.KEYCODE_CLEAR; /** KeyEvent.KEYCODE_ENDCALL */ public final static int ENDCALL = KeyEvent.KEYCODE_ENDCALL; /** KeyEvent.KEYCODE_ENVELOPE */ public final static int ENVELOPE = KeyEvent.KEYCODE_ENVELOPE; /** KeyEvent.KEYCODE_EXPLORER */ public final static int EXPLORER = KeyEvent.KEYCODE_EXPLORER; /** KeyEvent.KEYCODE_FN */ public final static int FN = KeyEvent.KEYCODE_FN; /** KeyEvent.KEYCODE_HOME */ public final static int HOME = KeyEvent.KEYCODE_HOME; /** KeyEvent.KEYCODE_NEWLINE */ public final static int NEWLINE = KeyEvent.KEYCODE_NEWLINE; /** KeyEvent.KEYCODE_NUM */ public final static int NUM = KeyEvent.KEYCODE_NUM; /** KeyEvent.KEYCODE_POWER */ public final static int POWER = KeyEvent.KEYCODE_POWER; /** KeyEvent.KEYCODE_SOFT_LEFT */ public final static int SOFT_LEFT = KeyEvent.KEYCODE_SOFT_LEFT; /** KeyEvent.KEYCODE_SOFT_RIGHT */ public final static int SOFT_RIGHT = KeyEvent.KEYCODE_SOFT_RIGHT; /** KeyEvent.KEYCODE_SYM */ public final static int SYM = KeyEvent.KEYCODE_SYM; /** KeyEvent.KEYCODE_TAB */ public final static int TAB = KeyEvent.KEYCODE_TAB; /** KeyEvent.KEYCODE_VOLUME_DOWN */ public final static int VOLUME_UP = KeyEvent.KEYCODE_VOLUME_DOWN; /** KeyEvent.KEYCODE_VOLUME_UP */ public final static int VOLUME_DOWN = KeyEvent.KEYCODE_VOLUME_UP;
Database Fixtures
It's hard to write meaningful tests without making assumptions about the subject's state. Android applications will probably make use of SQLite3 databases for all but the simplest persistent state. Positron lets tests alter or prepopulate database contents before (or during execution) by running arbitrary sql scripts loaded as 'raw' resources:
/** As a hand set user I want to browse local packages so that I know what is installed. */
public class BrowseInstalledDistributions extends TestCase {
public void testMayBrowseInstalledDistributions() {
execSql("bolt.db", R.raw.three_distributions);
Intent fromApplicationsFolder = new Intent(MAIN_ACTION).setClass(getTargetContext(), Installed.class);
startActivity(fromApplicationsFolder.addLaunchFlags(NEW_TASK_LAUNCH));
pause();The execSql("bolt.db", R.raw.three_distributions) line looks for a database in the target context named 'bolt.db', and runs the R.raw.three_distributions sql script against it. The sql itself is fairly standard:
-- We can junk previous contents since positron will backup & restore the database -- when it runs a suite. delete from distributions; insert into distributions (package, name, version, summary, description) values ( 'com.google.twisty', 'Twisty', '0.1-alpha', 'Play z-machine games!', 'You are likely to be eaten by a grue.'); insert into distributions (package, name, version, summary, description) values ( 'com.example.calculator', 'Calculator', '0.1-alpha', 'A calculator.', 'Has simple and scientific modes.'); insert into distributions (package, name, version, summary, description) values ( 'com.example.ovum', 'Ovum Egg Timer', '0.1-alpha', 'Never over-boil your eggs again!', 'It slices, it dices, it makes Julian Fries!');
Statements are terminated with a semicolon, comments begin with -- and continue to end of line.
To include the script (or any other 'raw' resource), place it in the 'raw' directory within your project's resources (creating it if necessary.) Notice android might be picky about the filenames; try sticking to alphanumerics and underscores.
Positron will try to backup and restore all databases in the tested application when it runs a test suite, by copying the relevant *.db files. If a test crashes in a particularly awesome way then restoration might fail, so watch out.
Sign in to add a comment
