o.gonthier@gmail.com
Gonthier Olivier
Freelance Developer/Trainer in Paris
Contributor infoq FR
Work at Orange Vallee on libon
Testing is not easy, it requires time
BUT
You need stability in your apps
AND
tests are there to detect regressions
SO
You need to practice it.
Test an isolated component: A class, or a method.
Test a component in its environment: test how it interacts for real.
Create a test project
android create test-project -m <path-app> -n <project-name> -p <test-path>
Run tests
adb shell am instrument -w <package>/<runner-class>
or, with maven
mvn android:instrument
There are two families of Test Cases
TestCases for test Activities and Account Sync
Provides Context, and Instrumentation
TestCases for test Providers, Services, Loaders and ApplicationsProvides Context, and permissions asserts
syncProvider(Uri uri, String accountName, String authority)
cancelSyncsandDisableAutoSync()
setApplication(Application application)
startActivity(Intent intent, Bundle savedInstanceState, Object lastNonConfigurationInstance)
setActivityContext(Context activityContext)
Inner Mock Activity: class MockParent extends Activity
getActivity()
setActivityIntent(Intent i)
Functional Test of an activity, but launched one time for all tests methods in the TestCase. (ActivityUnitTestCase and ActivityInstrumentationTestCase launch activity each time setUp() is called)
createApplication()
terminateApplication()
getSystemContext()
getLoaderResultSynchronously(final Loader<T> loader)
startService(Intent intent)
bindService(Intent intent)
shutdownService()
Test Isolated Provider, using a MockContext, and a MockContentResolver.Data is not saved at the same place as usual!
It can also be used from other TestCases
newResolverWithContentProviderFromSql(Context targetContext, String filenamePrefix, Class<T> providerClass, String authority, String databaseName, int databaseVersion, String sql)
However, most of them are not really useful: they are only classes skeleton, that throw UnsupportedOperationException......Excepted MockContentResolver
A ContentResolver that allows you to plug yourself your providers!
addProvider(String name, ContentProvider provider)
notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork)
In case of ContentProviderTestCase2, only one provider is plugged.
There are two classes that takes benefits of ContextWrapper to mock Contexts
A contextWrapper is a Context, that delegate all method execution to another Context. Subclass it for... Mock Context!
Prevent tests from talking to the device:checkUriPermission() return permission Granted getFilesDir() return File("/dev/null") getSystemService() return null
getAndClearBroadcastIntents()
Performs Database and File operations with a renamed database/filename!
providerWithRenamedContext(Class<T> contentProvider, Context c, String filePrefix)
makeExistingFilesAndDbsAccessible()
Assert View and ViewGroup Objects
Test:
assertGroupContains(ViewGroup parent, View child)
assertLeftAligned(View first, View second)
assertHasScreenCoordinates(View origin, View view, int x, int y)
assertOnScreen(View origin, View view)
Assert Java objects
Test:
assertEmpty(Map<?,?> map)
assertContentsInOrder(Iterable<?> actual, Object... expected)
assertMatchesRegex(String expectedRegex, String actual)
assertEquals(int[] expected, int[] actual)
assertAssignableFrom(Class<?> expected, Object actual)
Set of static methods to generate Touch Events
scrollToTop(ActivityInstrumentationTestCase test, ViewGroup v)
dragViewToBottom(ActivityInstrumentationTestCase test, View v)
tapView(InstrumentationTestCase test, View v)
longClickView(ActivityInstrumentationTestCase test, View v)
getStartLocation(View v, int gravity, int[] xy)
@SmallTest, @MediumTest, @LargeTest, @Smoke
@Suppress
@FlakyTest(tolerance = 2)
@UiThreadTest
In sources, there are other classes, not part of sdk for now.
Some of these looks promising!
@TimedTest, @BandwidthTest, @RepetitiveTest
public class TestCarProvider extends ProviderTestCase2<CarProvider>{
public CarProviderTest() { super(CarProvider.class, "com.roly.carseller.carprovider"); } public void testInsert() { //Given CarProvider provider = getProvider(); Car car = new Car("BMW"); provider.insert(car);
//When Car fromDB = provider.query(car.getId());
//Then assertEquals(car.getBrand(),fromDB.getBrand());
}
}
¡This is not real code!
public class TestServerResponseParsing extends AndroidTestCase{
ServerResponseParser parser;
@Override protected void setUp() throws Exception { super.setUp(); parser = ((ClientApplication) getContext().getApplicationContext()).getParser(); parser.prepare(); }
public void testErrorResponse(){ ServerResult result = parser.parse(JSON_ERROR_EXAMPLE); assertEquals(result.getCode(), ServerResult.ERROR); }
@Override protected void tearDown() throws Exception { super.tearDown(); parser.clearAllTasks(); }
public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity>{ public MainActivityTest() { super(MainActivity.class); } public void testIntentValueUpdateTextView() { Intent intent = new Intent(); intent.putExtra("userId",45); setActivityIntent(intent); getInstrumentation().callActivityOnCreate(getActivity(), null); TextView view = (TextView) getActivity().findViewById(R.id.userIdText); assertNotNull(view.getText()); }
}
@SmallTest public void testActivityGetValueFromIntent()
@MediumTest public class TestServer extends ServiceTestCase<RestService>
@LargeTest public void testSyncAllContacts()
adb shell am instrsument -w -e size small ...
mvn android:instrument -Dandroid.test.testSize=small ...
Make your own TestSuite
<instrumentation android:name="com.roly.mycoolapp.test.CoolAppInstrumentation" android:targetPackage="com.roly.mycoolapp"/>
public class CoolAppInstumentation extends InstrumentationTestRunner{
@Override
public TestSuite getAllTests() {
TestSuite testSuite = new TestSuite();
testSuite.addTest(new TestSuiteBuilder(CoolAppInstrumentation.class)
.includePackages("com.roly.mycoolapp.test.sync")
.build());
return testSuite;
}
}
Predicate<TestMethod> smallTests = new Predicate<TestMethod>(){ @Override public boolean apply(TestMethod testMethod) { return testMethod.getAnnotation(SmallTest.class) != null;
}
} new TestSuiteBuilder(MyInstrumentation.class).include(...) .addRequirements(smallTests) .build(); ...
class MyTest extends AndroidTestCase {
@Override @Suppress public void testAndroidTestCaseSetupProperly() ...
class MyServiceTest extends ServiceTestCase { @Override @Suppress public void testServiceTestCaseSetUpProperly()
...
Keep only what you care about
Contexts are great objects to convey Mocks
In all AndroidTestCases: public void testMeImFamous(){ setContext(myMockContext); ... }
In ContentProvider constructor: public ContentProvider(Context context, String readPermission, String writePermission, PathPermission[] pathPermissions) And everywhere you pass a Context.
mResolver = new MockContentResolver(); MockContext mockContext = new MockContext() { @Override public ContentResolver getContentResolver() { return mResolver; } };
mProvider = new MyProvider(); mProvider.attachInfo(mockContext, null); mResolver.addProvider("com.roly.myapp.coolnessprovider", mProvider);
Try to isolate as much as possible your tests
Use RenamingDelegatingContext, and IsolatedContext
mContextWrapper = new RenamingDelegatingContext(getContext(), "test-");
mContext = new IsolatedContext(mResolver, mContextWrapper);
It will prevent you from:
When you write tests, you don't want to run async code!
You can force you test to wait if you get any callback or event
Use Java's CountDownLatch for that.
final CountDownLatch latch = new CountDownLatch(1); final BroadcastReceiver syncReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { latch.countDown(); } }
try { latch.await(60, TimeUnit.SECONDS); } catch (InterruptedException e) { fail(); }
In tests parts, you can enjoy Reflection API
It can facilitate your life!
//Get private field Field privateField = myObject.getClass.getDeclaredField("mParser");
//Set it accessible privateField.setAccessible(true);
//Inject a value privateField.set(myObject, new Parser());
//Then invoke a method parseMethod.invoke(myObject, "stuff to parse");
Make builders so that you can construct Objects easily
public class ContactBuilder { private String firstName;
public ContactBuilder firstName(String firstName) { this.firstName = firstName; return this; } public Contact build() { Contact contact = new Contact(); contact.setFirstname(firstname); return contact;
} }
new ContactBuilder().firstName("tata").build();
new ContactBuilder().firstName("toto").build();
Randomize your test data is a great thing. (imho)
BUT Don't do it yourself
Suggestions
Make high level static methods that wrap code reusable.
createUser()
login()
deleteUser()
forceDeleteUser()
Facilitate test writing to other developers is important.
Note that the class TestSetup from jUnit3 is not part of the framework
public void testDisplayBlackBox() {
//Enter 10 in first edit-field
solo.enterText(0,"10");
//Enter 20 in second edit-field
solo.enterText(1,"20");
//Click on Multiply button
solo.clickOnButton("Multiply");
//Verify that resultant of 10 x 20
assertTrue(solo.searchText("200"));
}
No need to deploy on device = So much time saved!
@RunWith(HttpTest.RobolectricTestRunner.class) public class HttpTest { @Before public void setup() { Robolectric.setDefaultHttpResponse(200, "OK"); } @Test public void testGet_shouldApplyCorrectHeaders(){ HashMap<String, String> headers=new HashMap<String, String>(); headers.put("foo", "bar"); http.get("www.example.com", headers, null, null); HttpRequest sent = Robolectric.getSentHttpRequest(0); assertThat(sent.getHeaders("foo")[0].getValue(), equalTo("bar")); }
}
assertEquals(View.GONE, view.getVisibility());
⇩
assertThat(view).isGone();
assertThat(layout).isVisible()
.isVertical()
.hasChildCount(4)
.hasShowDividers(SHOW_DIVIDERS_MIDDLE);
And it's also extensible, you can had your own assertions
A powerful Mock Framework
- mock(Class) create a mock instance of your class
- verify(myMock).someMethod() verify that a method has been called
- when(myMock.getUser()).thenReturn(new User()) specify what to return when a method is called
Context context = Mockito.mock(Context.class);
Mockito.when(context.getContentResolver())
.thenReturn(Mockito.mock(ContentResolver.class));
//Do something
Mockito.verify(context).getContentResolver();
Great tool to automate test on multi-devices
Generate a cool web interface to get tests result
For test Coverage, Framework suggest to use Emma
adb shell am instrsument -w -e emma true ...
mvn android:instrument -Dandroid.test.testCoverage=true ...
On Jenkins, you can find an android emulator plugin.
It provides all you need to automate tests.
Additionnally, if you want to get the state 'unstable', you need to add a property with maven.
mvn android:instrument -Dmaven.test.failure.ignore=true ...