Ported AppProvider tests to Robolectric.

Get around silly `final` methods in `ContentResolver` with Mockito and `delegatesTo`.

The Robolectric library presumes that people always want to test content providers by
manually invoking the `query`/`update`/`delete` methods on the `ShadowContentResolver`.
While that is a great feature for testing, we have helper methods that require testing,
and these methods accept either a _real_ `ContentResolver` or `Context`. Robolectric
did some cool magic in terms of intercepting runtime calls to content resolvers and
forwarding them to the "shadow" verison, to deal with final/package private/etc methods.
However, as a side effect, the `ShadowContentProvider` _is not a `ContentProvider` as
far as the Java compiler is concerned.

By utilising Mockito + `delegatesTo` method, we are able to achieve what is required:
 * An actual `ContentProvider` instance.
 * It forwards calls to the `ShadowContentProvider` provided by Robolectric.
This commit is contained in:
Peter Serwylo
2016-06-06 08:02:53 +10:00
parent 09fd3d188c
commit 4e66bb810f
13 changed files with 553 additions and 433 deletions

View File

@@ -13,7 +13,7 @@ import junit.framework.AssertionFailedError;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.FDroidProviderTest;
import org.fdroid.fdroid.data.FDroidProviderTestOld;
import java.io.File;
import java.io.FileOutputStream;
@@ -113,11 +113,11 @@ public class TestUtils {
resolver.insert(uri, values);
}
public static Uri insertApk(FDroidProviderTest<ApkProvider> providerTest, String id, int versionCode) {
public static Uri insertApk(FDroidProviderTestOld<ApkProvider> providerTest, String id, int versionCode) {
return insertApk(providerTest, id, versionCode, new ContentValues());
}
public static Uri insertApk(FDroidProviderTest<ApkProvider> providerTest, String id, int versionCode, ContentValues additionalValues) {
public static Uri insertApk(FDroidProviderTestOld<ApkProvider> providerTest, String id, int versionCode, ContentValues additionalValues) {
ContentValues values = new ContentValues();

View File

@@ -1,389 +0,0 @@
package org.fdroid.fdroid.data;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.pm.PackageInfo;
import android.content.res.Resources;
import android.database.Cursor;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.TestUtils;
import java.util.ArrayList;
import java.util.List;
import mock.MockCategoryResources;
import mock.MockContextSwappableComponents;
import mock.MockInstallablePackageManager;
@SuppressWarnings("PMD") // TODO port this to JUnit 4 semantics
public class AppProviderTest extends FDroidProviderTest<AppProvider> {
public AppProviderTest() {
super(AppProvider.class, AppProvider.getAuthority());
}
@Override
public void setUp() throws Exception {
super.setUp();
getSwappableContext().setResources(new MockCategoryResources(getContext()));
}
@Override
protected Resources getMockResources() {
return new MockCategoryResources(getContext());
}
@Override
protected String[] getMinimalProjection() {
return new String[] {
AppProvider.DataColumns.PACKAGE_NAME,
AppProvider.DataColumns.NAME,
};
}
/**
* Although this doesn't directly relate to the AppProvider, it is here because
* the AppProvider used to stumble across this bug when asking for installed apps,
* and the device had over 1000 apps installed.
*/
public void testMaxSqliteParams() {
MockInstallablePackageManager pm = new MockInstallablePackageManager();
getSwappableContext().setPackageManager(pm);
insertApp("com.example.app1", "App 1");
insertApp("com.example.app100", "App 100");
insertApp("com.example.app1000", "App 1000");
for (int i = 0; i < 50; i++) {
String packageName = "com.example.app" + i;
pm.install(packageName, 1, "v" + 1);
PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
InstalledAppProviderService.insertAppIntoDb(getSwappableContext(), packageName, packageInfo);
}
assertResultCount(1, AppProvider.getInstalledUri());
for (int i = 50; i < 500; i++) {
String packageName = "com.example.app" + i;
pm.install(packageName, 1, "v" + 1);
PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
InstalledAppProviderService.insertAppIntoDb(getSwappableContext(), packageName, packageInfo);
}
assertResultCount(2, AppProvider.getInstalledUri());
for (int i = 500; i < 1100; i++) {
String packageName = "com.example.app" + i;
pm.install(packageName, 1, "v" + 1);
PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
InstalledAppProviderService.insertAppIntoDb(getSwappableContext(), packageName, packageInfo);
}
assertResultCount(3, AppProvider.getInstalledUri());
}
public void testCantFindApp() {
assertNull(AppProvider.Helper.findByPackageName(getMockContentResolver(), "com.example.doesnt-exist"));
}
public void testUris() {
assertInvalidUri(AppProvider.getAuthority());
assertInvalidUri(ApkProvider.getContentUri());
assertValidUri(AppProvider.getContentUri(), "content://org.fdroid.fdroid.data.AppProvider");
assertValidUri(AppProvider.getSearchUri("'searching!'"), "content://org.fdroid.fdroid.data.AppProvider/search/'searching!'");
assertValidUri(AppProvider.getSearchUri("/"), "content://org.fdroid.fdroid.data.AppProvider/search/%2F");
assertValidUri(AppProvider.getSearchUri(""), "content://org.fdroid.fdroid.data.AppProvider");
assertValidUri(AppProvider.getSearchUri(null), "content://org.fdroid.fdroid.data.AppProvider");
assertValidUri(AppProvider.getNoApksUri());
assertValidUri(AppProvider.getInstalledUri());
assertValidUri(AppProvider.getCanUpdateUri());
App app = new App();
app.packageName = "org.fdroid.fdroid";
List<App> apps = new ArrayList<>(1);
apps.add(app);
assertValidUri(AppProvider.getContentUri(app));
assertValidUri(AppProvider.getContentUri(apps));
assertValidUri(AppProvider.getContentUri("org.fdroid.fdroid"));
}
public void testQuery() {
Cursor cursor = queryAllApps();
assertNotNull(cursor);
cursor.close();
}
private void insertApps(int count) {
for (int i = 0; i < count; i++) {
insertApp("com.example.test." + i, "Test app " + i);
}
}
private void insertAndInstallApp(
MockInstallablePackageManager packageManager,
String id, int installedVercode, int suggestedVercode,
boolean ignoreAll, int ignoreVercode) {
ContentValues values = new ContentValues(3);
values.put(AppProvider.DataColumns.SUGGESTED_VERSION_CODE, suggestedVercode);
values.put(AppProvider.DataColumns.IGNORE_ALLUPDATES, ignoreAll);
values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVercode);
insertApp(id, "App: " + id, values);
InstalledAppTestUtils.install(getSwappableContext(), packageManager, id, installedVercode, "v" + installedVercode);
}
public void testCanUpdate() {
MockContextSwappableComponents c = getSwappableContext();
MockInstallablePackageManager pm = new MockInstallablePackageManager();
c.setPackageManager(pm);
insertApp("not installed", "not installed");
insertAndInstallApp(pm, "installed, only one version available", 1, 1, false, 0);
insertAndInstallApp(pm, "installed, already latest, no ignore", 10, 10, false, 0);
insertAndInstallApp(pm, "installed, already latest, ignore all", 10, 10, true, 0);
insertAndInstallApp(pm, "installed, already latest, ignore latest", 10, 10, false, 10);
insertAndInstallApp(pm, "installed, already latest, ignore old", 10, 10, false, 5);
insertAndInstallApp(pm, "installed, old version, no ignore", 5, 10, false, 0);
insertAndInstallApp(pm, "installed, old version, ignore all", 5, 10, true, 0);
insertAndInstallApp(pm, "installed, old version, ignore latest", 5, 10, false, 10);
insertAndInstallApp(pm, "installed, old version, ignore newer, but not latest", 5, 10, false, 8);
ContentResolver r = getMockContentResolver();
// Can't "update", although can "install"...
App notInstalled = AppProvider.Helper.findByPackageName(r, "not installed");
assertFalse(notInstalled.canAndWantToUpdate());
App installedOnlyOneVersionAvailable = AppProvider.Helper.findByPackageName(r, "installed, only one version available");
App installedAlreadyLatestNoIgnore = AppProvider.Helper.findByPackageName(r, "installed, already latest, no ignore");
App installedAlreadyLatestIgnoreAll = AppProvider.Helper.findByPackageName(r, "installed, already latest, ignore all");
App installedAlreadyLatestIgnoreLatest = AppProvider.Helper.findByPackageName(r, "installed, already latest, ignore latest");
App installedAlreadyLatestIgnoreOld = AppProvider.Helper.findByPackageName(r, "installed, already latest, ignore old");
assertFalse(installedOnlyOneVersionAvailable.canAndWantToUpdate());
assertFalse(installedAlreadyLatestNoIgnore.canAndWantToUpdate());
assertFalse(installedAlreadyLatestIgnoreAll.canAndWantToUpdate());
assertFalse(installedAlreadyLatestIgnoreLatest.canAndWantToUpdate());
assertFalse(installedAlreadyLatestIgnoreOld.canAndWantToUpdate());
App installedOldNoIgnore = AppProvider.Helper.findByPackageName(r, "installed, old version, no ignore");
App installedOldIgnoreAll = AppProvider.Helper.findByPackageName(r, "installed, old version, ignore all");
App installedOldIgnoreLatest = AppProvider.Helper.findByPackageName(r, "installed, old version, ignore latest");
App installedOldIgnoreNewerNotLatest = AppProvider.Helper.findByPackageName(r, "installed, old version, ignore newer, but not latest");
assertTrue(installedOldNoIgnore.canAndWantToUpdate());
assertFalse(installedOldIgnoreAll.canAndWantToUpdate());
assertFalse(installedOldIgnoreLatest.canAndWantToUpdate());
assertTrue(installedOldIgnoreNewerNotLatest.canAndWantToUpdate());
Cursor canUpdateCursor = r.query(AppProvider.getCanUpdateUri(), AppProvider.DataColumns.ALL, null, null, null);
canUpdateCursor.moveToFirst();
List<String> canUpdateIds = new ArrayList<>(canUpdateCursor.getCount());
while (!canUpdateCursor.isAfterLast()) {
canUpdateIds.add(new App(canUpdateCursor).packageName);
canUpdateCursor.moveToNext();
}
canUpdateCursor.close();
String[] expectedUpdateableIds = {
"installed, old version, no ignore",
"installed, old version, ignore newer, but not latest",
};
TestUtils.assertContainsOnly(expectedUpdateableIds, canUpdateIds);
}
public void testIgnored() {
MockInstallablePackageManager pm = new MockInstallablePackageManager();
getSwappableContext().setPackageManager(pm);
insertApp("not installed", "not installed");
insertAndInstallApp(pm, "installed, only one version available", 1, 1, false, 0);
insertAndInstallApp(pm, "installed, already latest, no ignore", 10, 10, false, 0);
insertAndInstallApp(pm, "installed, already latest, ignore all", 10, 10, true, 0);
insertAndInstallApp(pm, "installed, already latest, ignore latest", 10, 10, false, 10);
insertAndInstallApp(pm, "installed, already latest, ignore old", 10, 10, false, 5);
insertAndInstallApp(pm, "installed, old version, no ignore", 5, 10, false, 0);
insertAndInstallApp(pm, "installed, old version, ignore all", 5, 10, true, 0);
insertAndInstallApp(pm, "installed, old version, ignore latest", 5, 10, false, 10);
insertAndInstallApp(pm, "installed, old version, ignore newer, but not latest", 5, 10, false, 8);
assertResultCount(10, AppProvider.getContentUri());
String[] projection = {AppProvider.DataColumns.PACKAGE_NAME};
List<App> ignoredApps = AppProvider.Helper.findIgnored(getMockContext(), projection);
String[] expectedIgnored = {
"installed, already latest, ignore all",
"installed, already latest, ignore latest",
// NOT "installed, already latest, ignore old" - because it
// is should only ignore if "ignored version" is >= suggested
"installed, old version, ignore all",
"installed, old version, ignore latest",
// NOT "installed, old version, ignore newer, but not latest"
// for the same reason as above.
};
assertContainsOnlyIds(ignoredApps, expectedIgnored);
}
private void assertContainsOnlyIds(List<App> actualApps, String[] expectedIds) {
List<String> actualIds = new ArrayList<>(actualApps.size());
for (App app : actualApps) {
actualIds.add(app.packageName);
}
TestUtils.assertContainsOnly(actualIds, expectedIds);
}
public void testInstalled() {
MockInstallablePackageManager pm = new MockInstallablePackageManager();
getSwappableContext().setPackageManager(pm);
insertApps(100);
assertResultCount(100, AppProvider.getContentUri());
assertResultCount(0, AppProvider.getInstalledUri());
for (int i = 10; i < 20; i++) {
InstalledAppTestUtils.install(getSwappableContext(), pm, "com.example.test." + i, i, "v1");
}
assertResultCount(10, AppProvider.getInstalledUri());
}
public void testInsert() {
// Start with an empty database...
Cursor cursor = queryAllApps();
assertNotNull(cursor);
assertEquals(0, cursor.getCount());
cursor.close();
// Insert a new record...
insertApp("org.fdroid.fdroid", "F-Droid");
cursor = queryAllApps();
assertNotNull(cursor);
assertEquals(1, cursor.getCount());
// We intentionally throw an IllegalArgumentException if you haven't
// yet called cursor.move*()...
try {
new App(cursor);
fail();
} catch (IllegalArgumentException e) {
// Success!
} catch (Exception e) {
fail();
}
// And now we should be able to recover these values from the app
// value object (because the queryAllApps() helper asks for NAME and
// PACKAGE_NAME.
cursor.moveToFirst();
App app = new App(cursor);
cursor.close();
assertEquals("org.fdroid.fdroid", app.packageName);
assertEquals("F-Droid", app.name);
}
private Cursor queryAllApps() {
return getMockContentResolver().query(AppProvider.getContentUri(), getMinimalProjection(), null, null, null);
}
// ========================================================================
// "Categories"
// (at this point) not an additional table, but we treat them sort of
// like they are. That means that if we change the implementation to
// use a separate table in the future, these should still pass.
// ========================================================================
public void testCategoriesSingle() {
insertAppWithCategory("com.dog", "Dog", "Animal");
insertAppWithCategory("com.rock", "Rock", "Mineral");
insertAppWithCategory("com.banana", "Banana", "Vegetable");
List<String> categories = AppProvider.Helper.categories(getMockContext());
String[] expected = new String[] {
getMockContext().getResources().getString(R.string.category_Whats_New),
getMockContext().getResources().getString(R.string.category_Recently_Updated),
getMockContext().getResources().getString(R.string.category_All),
"Animal",
"Mineral",
"Vegetable",
};
TestUtils.assertContainsOnly(categories, expected);
}
public void testCategoriesMultiple() {
insertAppWithCategory("com.rock.dog", "Rock-Dog", "Mineral,Animal");
insertAppWithCategory("com.dog.rock.apple", "Dog-Rock-Apple", "Animal,Mineral,Vegetable");
insertAppWithCategory("com.banana.apple", "Banana", "Vegetable,Vegetable");
List<String> categories = AppProvider.Helper.categories(getMockContext());
String[] expected = new String[] {
getMockContext().getResources().getString(R.string.category_Whats_New),
getMockContext().getResources().getString(R.string.category_Recently_Updated),
getMockContext().getResources().getString(R.string.category_All),
"Animal",
"Mineral",
"Vegetable",
};
TestUtils.assertContainsOnly(categories, expected);
insertAppWithCategory("com.example.game", "Game",
"Running,Shooting,Jumping,Bleh,Sneh,Pleh,Blah,Test category," +
"The quick brown fox jumps over the lazy dog,With apostrophe's");
List<String> categoriesLonger = AppProvider.Helper.categories(getMockContext());
String[] expectedLonger = new String[] {
getMockContext().getResources().getString(R.string.category_Whats_New),
getMockContext().getResources().getString(R.string.category_Recently_Updated),
getMockContext().getResources().getString(R.string.category_All),
"Animal",
"Mineral",
"Vegetable",
"Running",
"Shooting",
"Jumping",
"Bleh",
"Sneh",
"Pleh",
"Blah",
"Test category",
"The quick brown fox jumps over the lazy dog",
"With apostrophe's",
};
TestUtils.assertContainsOnly(categoriesLonger, expectedLonger);
}
// =======================================================================
// Misc helper functions
// (to be used by any tests in this suite)
// =======================================================================
private void insertApp(String id, String name) {
insertApp(id, name, new ContentValues());
}
private void insertAppWithCategory(String id, String name, String categories) {
ContentValues values = new ContentValues(1);
values.put(AppProvider.DataColumns.CATEGORIES, categories);
insertApp(id, name, values);
}
private void insertApp(String id, String name,
ContentValues additionalValues) {
TestUtils.insertApp(getMockContentResolver(), id, name, additionalValues);
}
}

View File

@@ -16,7 +16,7 @@ import java.util.List;
* This should not contain any test methods, or else they get executed
* once for every concrete subclass.
*/
abstract class BaseApkProviderTest extends FDroidProviderTest<ApkProvider> {
abstract class BaseApkProviderTest extends FDroidProviderTestOld<ApkProvider> {
BaseApkProviderTest() {
super(ApkProvider.class, ApkProvider.getAuthority());

View File

@@ -15,7 +15,7 @@ import mock.MockContextSwappableComponents;
import mock.MockFDroidResources;
@SuppressWarnings("PMD") // TODO port this to JUnit 4 semantics
public abstract class FDroidProviderTest<T extends FDroidProvider> extends ProviderTestCase2MockContext<T> {
public abstract class FDroidProviderTestOld<T extends FDroidProvider> extends ProviderTestCase2MockContext<T> {
private FDroidProvider[] allProviders = {
new AppProvider(),
@@ -26,7 +26,7 @@ public abstract class FDroidProviderTest<T extends FDroidProvider> extends Provi
private MockContextSwappableComponents swappableContext;
public FDroidProviderTest(Class<T> providerClass, String providerAuthority) {
public FDroidProviderTestOld(Class<T> providerClass, String providerAuthority) {
super(providerClass, providerAuthority);
}

View File

@@ -5,11 +5,11 @@ import mock.MockInstallablePackageManager;
/**
* Tests the ability of the {@link InstalledAppCacheUpdater} to stay in sync with
* the {@link android.content.pm.PackageManager}.
* For practical reasons, it extends FDroidProviderTest<InstalledAppProvider>, although there is also a
* For practical reasons, it extends FDroidProviderTestOld<InstalledAppProvider>, although there is also a
* separate test for the InstalledAppProvider which tests the CRUD operations in more detail.
*/
@SuppressWarnings("PMD") // TODO port this to JUnit 4 semantics
public class InstalledAppCacheTest extends FDroidProviderTest<InstalledAppProvider> {
public class InstalledAppCacheTest extends FDroidProviderTestOld<InstalledAppProvider> {
private MockInstallablePackageManager packageManager;

View File

@@ -1,24 +0,0 @@
package org.fdroid.fdroid.data;
import android.content.pm.PackageInfo;
import mock.MockContextSwappableComponents;
import mock.MockInstallablePackageManager;
public class InstalledAppTestUtils {
/**
* Will tell {@code pm} that we are installing {@code packageName}, and then update the
* "installed apps" table in the database.
*/
public static void install(MockContextSwappableComponents context,
MockInstallablePackageManager pm, String packageName,
int versionCode, String versionName) {
context.setPackageManager(pm);
pm.install(packageName, versionCode, versionName);
PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
InstalledAppProviderService.insertAppIntoDb(context, packageName, packageInfo);
}
}