java.lang.Object
↳androidx.test.rule.provider.ProviderTestRule
Gradle dependencies
compile group: 'androidx.test', name: 'rules', version: '1.4.1-alpha06'
- groupId: androidx.test
- artifactId: rules
- version: 1.4.1-alpha06
Artifact androidx.test:rules:1.4.1-alpha06 it located at Google repository (https://maven.google.com/)
Androidx artifact mapping:
androidx.test:rules com.android.support.test:rules
Androidx class mapping:
androidx.test.rule.provider.ProviderTestRule android.support.test.rule.provider.ProviderTestRule
Overview
A to test ContentProvider
s, with additional APIs to enable easy
initialization such as restoring database from a file, running database commands passed in as a
String or a file. By default, all permissions are granted when they are checked in
ContentProviders under test, method ProviderTestRule.revokePermission(String) can be used to revoke specific
permissions to test the cases when they are denied in ContentProviders.
Note: The database related methods ProviderTestRule.Builder.setDatabaseFile(String, File), ProviderTestRule.Builder.setDatabaseCommands(String, String...), ProviderTestRule.Builder.setDatabaseCommandsFile(String, File) and ProviderTestRule.runDatabaseCommands(String, String...) should only be used when ContentProvider under test is implemented based on
SQLiteDatabase
.
If more than one database related methods are used for a ContentProvider under test, the
execution order of restoring database from file and running database commands are independent of
the order those methods are called. If all methods are used, when setting up the ContentProvider
for test, the execution order is as follows:
- Restore database from file passed in via ProviderTestRule.Builder.setDatabaseFile(String, File)
- Run database commands passed in via ProviderTestRule.Builder.setDatabaseCommands(String, String...)
- Run database commands from file passed in via ProviderTestRule.Builder.setDatabaseCommandsFile(String, File)
If the ContentProvider
under test is not implemented based on SQLiteDatabase
,
or is implemented based on SQLiteDatabase
but no extra database initialization workloads
are needed, the rule can be created by simply using Builder(Class,String).
Usage example:
@Rule
public ProviderTestRule mProviderRule =
new ProviderTestRule.Builder(MyContentProvider.class, MyContentProvider.AUTHORITY).build();
@Test
public void verifyContentProviderContractWorks() {
ContentResolver resolver = mProviderRule.getResolver();
// perform some database (or other) operations
Uri uri = resolver.insert(testUrl, testContentValues);
// perform some assertions on the resulting URI
assertNotNull(uri);
}
Alternatively, if the ContentProvider
under test is based on SQLiteDatabase
,
then all database related methods can be used. However, the database name argument passed in via
these methods must match the actual database name used by the ContentProvider
under test.
Usage example:
@Rule
public ProviderTestRule mProviderRule =
new ProviderTestRule.Builder(MyContentProvider.class, MyContentProvider.AUTHORITY)
.setDatabaseCommands(DATABASE_NAME, INSERT_ONE_ENTRY_CMD, INSERT_ANOTHER_ENTRY_CMD)
.build();
@Test
public void verifyTwoEntriesInserted() {
ContentResolver resolver = mProviderRule.getResolver();
// two entries are already inserted by rule, we can directly perform assertions to verify
Cursor c = null;
try {
c = resolver.query(URI_TO_QUERY_ALL, null, null, null, null);
assertNotNull(c);
assertEquals(2, c.getCount());
} finally {
if (c != null && !c.isClosed()) {
c.close();
}
}
}
This API is currently in beta.
Summary
Methods |
---|
protected void | afterProviderCleanedUp()
Override this method to execute any code that should run after provider is cleaned up. |
public Statement | apply(Statement base, Description description)
|
protected void | beforeProviderSetup()
Override this method to execute any code that should run before provider is set up. |
public ContentResolver | getResolver()
Get the isolated that should be used for testing of the
ContentProviders. |
public void | revokePermission(java.lang.String permission)
Revoke permission anytime during the tests. |
public void | runDatabaseCommands(java.lang.String dbName, java.lang.String dbCmds[])
Run database commands anytime during the tests, after the rule is created. |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Methods
public ContentResolver
getResolver()
Get the isolated that should be used for testing of the
ContentProviders.
Returns:
the isolated created by this ProviderTestRule.
public Statement
apply(Statement base, Description description)
public void
runDatabaseCommands(java.lang.String dbName, java.lang.String dbCmds[])
Run database commands anytime during the tests, after the rule is created.
Parameters:
dbName: The name of the underlying database used by the ContentProvider under test.
dbCmds: The SQL commands to run during tests. Each command will be passed to SQLiteDatabase
to execute.
public void
revokePermission(java.lang.String permission)
Revoke permission anytime during the tests. The default return value of the following methods
is PackageManager.PERMISSION_GRANTED. After a specific permission is revoked, the value
returned becomes PackageManager.PERMISSION_DENIED when calling the methods with the
revoked permission. After a test, the return values are restored to PackageManager.PERMISSION_GRANTED.
Consequentially, calling the following methods with the revoked permission will throw a
java.lang.SecurityException
.
protected void
beforeProviderSetup()
Override this method to execute any code that should run before provider is set up. This method
is called before each test method, including any method annotated with Before
.
protected void
afterProviderCleanedUp()
Override this method to execute any code that should run after provider is cleaned up. This
method is called after each test method, including any method annotated with After
.
Source
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the Lice`nse is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.test.rule.provider;
import static androidx.test.internal.util.Checks.checkArgument;
import static androidx.test.internal.util.Checks.checkNotNull;
import static androidx.test.internal.util.Checks.checkState;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.Build;
import android.test.mock.MockContentResolver;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.test.annotation.ExperimentalTestApi;
import androidx.test.platform.app.InstrumentationRegistry;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
/**
* A {@link TestRule} to test {@link ContentProvider}s, with additional APIs to enable easy
* initialization such as restoring database from a file, running database commands passed in as a
* String or a file. By default, all permissions are granted when they are checked in
* ContentProviders under test, method {@link #revokePermission} can be used to revoke specific
* permissions to test the cases when they are denied in ContentProviders.
*
* <p>Note: The database related methods {@link Builder#setDatabaseFile}, {@link
* Builder#setDatabaseCommands}, {@link Builder#setDatabaseCommandsFile} and {@link
* #runDatabaseCommands} should only be used when ContentProvider under test is implemented based on
* {@link SQLiteDatabase}.
*
* <p>If more than one database related methods are used for a ContentProvider under test, the
* execution order of restoring database from file and running database commands are independent of
* the order those methods are called. If all methods are used, when setting up the ContentProvider
* for test, the execution order is as follows:
*
* <ol>
* <li>Restore database from file passed in via {@link Builder#setDatabaseFile}
* <li>Run database commands passed in via {@link Builder#setDatabaseCommands}
* <li>Run database commands from file passed in via {@link Builder#setDatabaseCommandsFile}
* </ol>
*
* <p>If the {@link ContentProvider} under test is not implemented based on {@link SQLiteDatabase},
* or is implemented based on {@link SQLiteDatabase} but no extra database initialization workloads
* are needed, the rule can be created by simply using {@link Builder Builder(Class,String)}.
*
* <p>Usage example:
*
* <pre>
* @Rule
* public ProviderTestRule mProviderRule =
* new ProviderTestRule.Builder(MyContentProvider.class, MyContentProvider.AUTHORITY).build();
*
* @Test
* public void verifyContentProviderContractWorks() {
* ContentResolver resolver = mProviderRule.getResolver();
* // perform some database (or other) operations
* Uri uri = resolver.insert(testUrl, testContentValues);
* // perform some assertions on the resulting URI
* assertNotNull(uri);
* }
* </pre>
*
* <p>Alternatively, if the {@link ContentProvider} under test is based on {@link SQLiteDatabase},
* then all database related methods can be used. However, the database name argument passed in via
* these methods must match the actual database name used by the {@link ContentProvider} under test.
*
* <p>Usage example:
*
* <pre>
* @Rule
* public ProviderTestRule mProviderRule =
* new ProviderTestRule.Builder(MyContentProvider.class, MyContentProvider.AUTHORITY)
* .setDatabaseCommands(DATABASE_NAME, INSERT_ONE_ENTRY_CMD, INSERT_ANOTHER_ENTRY_CMD)
* .build();
*
* @Test
* public void verifyTwoEntriesInserted() {
* ContentResolver resolver = mProviderRule.getResolver();
* // two entries are already inserted by rule, we can directly perform assertions to verify
* Cursor c = null;
* try {
* c = resolver.query(URI_TO_QUERY_ALL, null, null, null, null);
* assertNotNull(c);
* assertEquals(2, c.getCount());
* } finally {
* if (c != null && !c.isClosed()) {
* c.close();
* }
* }
* }
* </pre>
*
* <p><b>This API is currently in beta.</b>
*/
@ExperimentalTestApi
public class ProviderTestRule implements TestRule {
private static final String TAG = "ProviderTestRule";
private final Set<WeakReference<ContentProvider>> providersRef;
private final Set<DatabaseArgs> databaseArgsSet;
private final ContentResolver resolver;
private final DelegatingContext context;
@VisibleForTesting
ProviderTestRule(
Set<WeakReference<ContentProvider>> providersRef,
Set<DatabaseArgs> databaseArgsSet,
ContentResolver resolver,
DelegatingContext context) {
this.providersRef = providersRef;
this.databaseArgsSet = databaseArgsSet;
this.resolver = resolver;
this.context = context;
}
/**
* Get the isolated {@link ContentResolver} that should be used for testing of the
* ContentProviders.
*
* @return the isolated {@link ContentResolver} created by this {@code ProviderTestRule}.
*/
public ContentResolver getResolver() {
return resolver;
}
@Override
public Statement apply(Statement base, Description description) {
return new ProviderStatement(base);
}
/**
* Run database commands anytime during the tests, after the rule is created.
*
* <p>
*
* @param dbName The name of the underlying database used by the ContentProvider under test.
* @param dbCmds The SQL commands to run during tests. Each command will be passed to {@link
* SQLiteDatabase#execSQL(String)} to execute.
*/
public void runDatabaseCommands(@NonNull String dbName, @NonNull String... dbCmds) {
checkNotNull(dbName);
checkNotNull(dbCmds);
if (dbCmds.length > 0) {
SQLiteDatabase database = context.openOrCreateDatabase(dbName, 0, null);
for (String cmd : dbCmds) {
if (!TextUtils.isEmpty(cmd)) {
try {
database.execSQL(cmd);
} catch (SQLiteException e) {
Log.e(
TAG,
String.format(
"Error executing sql command %s, possibly wrong or duplicated commands (e.g."
+ " same table insertion command without checking current table"
+ " existence).",
cmd));
throw e;
}
}
}
}
}
/**
* Revoke permission anytime during the tests. The default return value of the following methods
* is {@code PackageManager.PERMISSION_GRANTED}. After a specific permission is revoked, the value
* returned becomes {@code PackageManager.PERMISSION_DENIED} when calling the methods with the
* revoked permission. After a test, the return values are restored to {@code
* PackageManager.PERMISSION_GRANTED}.
*
* <ul>
* <li>{@link Context#checkPermission}
* <li>{@link Context#checkCallingPermission}
* <li>{@link Context#checkSelfPermission}
* <li>{@link Context#checkCallingOrSelfPermission}
* </ul>
*
* Consequentially, calling the following methods with the revoked permission will throw a {@link
* SecurityException}.
*
* <ul>
* <li>{@link Context#enforcePermission}
* <li>{@link Context#enforceCallingPermission}
* <li>{@link Context#enforceCallingOrSelfPermission}
* </ul>
*/
public void revokePermission(@NonNull String permission) {
checkArgument(!TextUtils.isEmpty(permission), "permission cannot be null or empty");
context.addRevokedPermission(permission);
}
/**
* Override this method to execute any code that should run before provider is set up. This method
* is called before each test method, including any method annotated with <a
* href="http://junit.sourceforge.net/javadoc/org/junit/Before.html"><code>Before</code></a>.
*/
protected void beforeProviderSetup() {
// empty by default
}
/**
* Override this method to execute any code that should run after provider is cleaned up. This
* method is called after each test method, including any method annotated with <a
* href="http://junit.sourceforge.net/javadoc/org/junit/After.html"><code>After</code></a>.
*/
protected void afterProviderCleanedUp() {
// empty by default
}
private void setUpProviders() throws IOException {
beforeProviderSetup();
for (DatabaseArgs databaseArgs : databaseArgsSet) {
setUpProvider(databaseArgs);
}
}
private void setUpProvider(DatabaseArgs databaseArgs) throws IOException {
if (databaseArgs.hasDBDataFile()) {
restoreDBDataFromFile(databaseArgs);
}
if (databaseArgs.hasDBCmdFile()) {
collectDBCmdsFromFile(databaseArgs);
}
if (databaseArgs.hasDBCmds()) {
runDatabaseCommands(databaseArgs.getDBName(), databaseArgs.getDBCmds());
}
}
private void restoreDBDataFromFile(DatabaseArgs databaseArgs) throws IOException {
File dbDataFile = databaseArgs.getDBDataFile();
checkState(
dbDataFile.exists(), String.format("The database file %s doesn't exist!", dbDataFile));
String dbName = databaseArgs.getDBName();
copyFile(dbDataFile, context.getDatabasePath(dbName));
// Add the restored database to the DelegatingContext
context.addDatabase(dbName);
}
private void collectDBCmdsFromFile(DatabaseArgs databaseArgs) throws IOException {
BufferedReader br = null;
File dbCmdFile = databaseArgs.getDBCmdFile();
List<String> cmdsToAdd = new ArrayList<>();
try {
br =
new BufferedReader(
new InputStreamReader(new FileInputStream(dbCmdFile), Charset.forName("UTF-8")));
String currentLine;
while ((currentLine = br.readLine()) != null) {
if (!TextUtils.isEmpty(currentLine)) {
cmdsToAdd.add(currentLine);
}
}
} catch (IOException ioe) {
Log.e(TAG, String.format("Cannot open command file %s to read", dbCmdFile));
throw ioe;
} finally {
if (br != null) {
br.close();
}
}
databaseArgs.addDBCmds(cmdsToAdd.toArray(new String[cmdsToAdd.size()]));
}
private void copyFile(File src, File dest) throws IOException {
File destParent = dest.getParentFile();
if (!destParent.exists() && !destParent.mkdirs()) {
String errorMessage = String.format("error happened creating parent dir for file %s", dest);
Log.e(TAG, errorMessage);
throw new IOException(errorMessage);
}
FileChannel in = new FileInputStream(src).getChannel();
FileChannel out = new FileOutputStream(dest).getChannel();
try {
in.transferTo(0, in.size(), out);
} catch (IOException ioe) {
Log.e(TAG, String.format("error happened copying file from %s to %s", src, dest));
throw ioe;
} finally {
in.close();
out.close();
}
}
private void cleanUpProviders() {
// ContentProvider.shutdown() method is added in HONEYCOMB
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
for (WeakReference<ContentProvider> providerRef : providersRef) {
ContentProvider provider = providerRef.get();
if (provider != null) {
provider.shutdown();
}
}
}
for (DatabaseArgs databaseArgs : databaseArgsSet) {
String dbName = databaseArgs.getDBName();
if (dbName != null) {
context.deleteDatabase(dbName);
}
}
afterProviderCleanedUp();
}
/**
* A Builder to ease {@link ProviderTestRule} creation. Users can input one or more {@link
* ContentProvider}s and their corresponding {@code authority} for tests. It also allows to
* specify the prefix to use when renaming test files for isolation by {@link #setPrefix}. If the
* ContentProvider under test is implemented based on {@link SQLiteDatabase}, users can also pass
* in database file to restore or database commands to run before tests.
*/
public static class Builder {
private static final String DEFAULT_PREFIX = "test.";
private final Map<String, Class<? extends ContentProvider>> providerClasses = new HashMap<>();
private final Map<String, DatabaseArgs> databaseArgsMap = new HashMap<>();
private String prefix = DEFAULT_PREFIX;
/**
* The basic builder to use when creating a {@code ProviderTestRule}, which allows to specify
* one {@link ContentProvider} and the corresponding {@code authority} for tests.
*
* @param providerClass The class of ContentProvider under test.
* @param providerAuth The authority defined for ContentProvider under test.
*/
public <T extends ContentProvider> Builder(
@NonNull Class<T> providerClass, @NonNull String providerAuth) {
checkNotNull(providerClass);
checkNotNull(providerAuth);
providerClasses.put(providerAuth, providerClass);
}
/**
* Enables users to specify the prefix to use when renaming test files for isolation. If not
* used, this rule will use "{@code #DEFAULT_PREFIX}" by default.
*
* @param prefix The non-empty prefix to append to test files.
*/
public Builder setPrefix(@NonNull String prefix) {
checkArgument(!TextUtils.isEmpty(prefix), "The prefix cannot be null or empty");
this.prefix = prefix;
return this;
}
/**
* Allows to pass in a SQLite database file containing the intended initial data to restore.
*
* <p>Note: Restoring the database from the file are executed before any sql commands execution
* in {@code SQLiteOpenHelper}'s {@code onCreate} method defined in ContentProvider under test,
* also before running the database commands, if any, passed in via {@link #setDatabaseCommands}
* and {@link #setDatabaseCommandsFile}.
*
* <p>In the case of the database file with prefixed name already exists, the database
* restoration will overwrite existing file.
*
* <p>
*
* @param dbName The name of the underlying database used by the ContentProvider under test.
* @param dbDataFile The SQLite database file that contains the data to restore.
*/
public Builder setDatabaseFile(@NonNull String dbName, @NonNull File dbDataFile) {
checkNotNull(dbName);
checkNotNull(dbDataFile);
getDatabaseArgs(dbName).setDBDataFile(dbDataFile);
return this;
}
/**
* Allows to pass in specific SQL commands to run against the database with a given name.
*
* <p>Note: The passed in commands are executed before any sql commands execution in {@code
* SQLiteOpenHelper}'s {@code onCreate} method defined in ContentProvider under test, also
* before executing commands, if any, passed in via {@link #setDatabaseCommandsFile}, but after
* restoring the database file, if any, passed in via {@link #setDatabaseFile}.
*
* <p>
*
* @param dbName The name of the underlying database used by the ContentProvider under test.
* @param dbCmds The SQL commands to run. Each command will be passed to {@link
* SQLiteDatabase#execSQL(String)} to execute.
*/
public Builder setDatabaseCommands(@NonNull String dbName, @NonNull String... dbCmds) {
checkNotNull(dbName);
checkNotNull(dbCmds);
getDatabaseArgs(dbName).setDBCmds(dbCmds);
return this;
}
/**
* Allows to pass in a file containing commands to run against the database with a given name.
*
* <p>Note: Commands in the file are executed before any sql commands execution in {@code
* SQLiteOpenHelper}'s {@code onCreate} method defined in ContentProvider under test, but after
* restoring the database file, if any, passed in via {@link #setDatabaseFile} and executing
* commands, if any, passed in via {@link #setDatabaseCommands}.
*
* <p>
*
* @param dbName The name of the underlying database used by the ContentProvider under test.
* @param dbCmdFile The file that contains line separated database commands to run. Each line
* will be treated as a separate command and passed to {@link
* SQLiteDatabase#execSQL(String)} to execute.
*/
public Builder setDatabaseCommandsFile(@NonNull String dbName, @NonNull File dbCmdFile) {
checkNotNull(dbName);
checkNotNull(dbCmdFile);
getDatabaseArgs(dbName).setDBCmdFile(dbCmdFile);
return this;
}
/**
* Allows to add additional ContentProvider and the corresponding authority for testing.
* Similarly, {@link #setDatabaseFile}, {@link #setDatabaseCommands}, {@link
* #setDatabaseCommandsFile}, and {@link #runDatabaseCommands} can be used for this
* ContentProvider.
*
* @param providerClass The class of the added ContentProvider under test.
* @param providerAuth The authority defined for the added ContentProvider under test.
*/
public <T extends ContentProvider> Builder addProvider(
@NonNull Class<T> providerClass, @NonNull String providerAuth) {
checkNotNull(providerClass);
checkNotNull(providerAuth);
checkState(providerClasses.size() > 0, "No existing provider yet while trying to add more");
checkState(
!providerClasses.containsKey(providerAuth),
String.format("ContentProvider with authority %s already exists.", providerAuth));
providerClasses.put(providerAuth, providerClass);
return this;
}
public ProviderTestRule build() {
Set<WeakReference<ContentProvider>> mProvidersRef = new HashSet<>();
MockContentResolver resolver = new MockContentResolver();
DelegatingContext context =
new DelegatingContext(
InstrumentationRegistry.getInstrumentation().getTargetContext(), prefix, resolver);
for (Map.Entry<String, Class<? extends ContentProvider>> entry : providerClasses.entrySet()) {
ContentProvider provider =
createProvider(entry.getKey(), entry.getValue(), resolver, context);
mProvidersRef.add(new WeakReference<>(provider));
}
return new ProviderTestRule(
mProvidersRef, new HashSet<>(databaseArgsMap.values()), resolver, context);
}
private ContentProvider createProvider(
String auth,
Class<? extends ContentProvider> clazz,
MockContentResolver resolver,
Context context) {
ContentProvider provider;
try {
provider = clazz.getConstructor().newInstance();
} catch (NoSuchMethodException me) {
Log.e(
TAG,
"NoSuchMethodException occurred when trying create new Instance for "
+ clazz.toString());
throw new RuntimeException(me);
} catch (InvocationTargetException ite) {
Log.e(
TAG,
"InvocationTargetException occurred when trying create new Instance for "
+ clazz.toString());
throw new RuntimeException(ite);
} catch (IllegalAccessException iae) {
Log.e(
TAG,
"IllegalAccessException occurred when trying create new Instance for "
+ clazz.toString());
throw new RuntimeException(iae);
} catch (InstantiationException ie) {
Log.e(
TAG,
"InstantiationException occurred when trying create new Instance for "
+ clazz.toString());
throw new RuntimeException(ie);
}
ProviderInfo providerInfo = new ProviderInfo();
providerInfo.authority = auth;
// attachInfo will call ContentProvider.onCreate(), so will refresh the context
// used by ContentProvider.
provider.attachInfo(context, providerInfo);
resolver.addProvider(providerInfo.authority, provider);
return provider;
}
private DatabaseArgs getDatabaseArgs(String dbName) {
if (databaseArgsMap.containsKey(dbName)) {
return databaseArgsMap.get(dbName);
} else {
DatabaseArgs databaseArgs = new DatabaseArgs(dbName);
databaseArgsMap.put(dbName, databaseArgs);
return databaseArgs;
}
}
}
private class ProviderStatement extends Statement {
private final Statement base;
public ProviderStatement(Statement base) {
this.base = base;
}
@Override
public void evaluate() throws Throwable {
try {
setUpProviders();
base.evaluate();
} finally {
cleanUpProviders();
}
}
}
}