I've written a class that is using Context, a third party library and SharedPreferences from PreferenceManager.
It's possible to mock Context, the third party library can be mocked using some mocking framework, but what to do with PreferenceManager?
I have two methods:
public void saveString(ThirdPartyObject obj) {
SharedPreferences appPreferences =
PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor editor = appPreferences.edit();
editor.putString(mContext.getString(
R.string.preferences_string_name), obj.getString());
editor.commit();
}
and corresponding, that loads preferences.
It doesn't look like you actually want a mock instance of PreferenceManager (which is mostly used in a PreferenceFragment or PreferenceActivity).
You probably want either:
A mock SharedPreferences, in which case you can just mock Context#getSharedPreferences (which is called by PreferenceManager#getDefaultSharedPreferences anyhow). You'll probably also have to make a mock SharedPreferences.Editor if preferences are edited, as above. You say you already know how to mock the context, so this should be fairly straightforward.
To use the actual preferences in the environment. This is easiest, and not necessarily a bad idea. Do make certain it's cleaned up properly so that your tests don't interfere with each other (or, depending on your test environment, aren't affected by manual use of the app).
If you really do want to mock PreferenceManager instance (like that you get in PreferenceFragment or PreferenceActivity), you can absolutely do so.
Since it's non-final, you can generate a mock PreferenceManager and SharedPreferences using Mockito (or another mocking library) as long as you have a way to provide it to your code wherever you would ordinarily get one (in non-test code, this normally comes from the getPreferenceManager()).
You can use specialized context for shared preference. RenamingDelegatingContext delegates everything to a Context. When we access SharedPreference from a Context, we use getSharedPreferences(String name, int mode).
Here by extending RenamingDelegatingContext we override getSharedPreferences and pretend the name parameter with test PREFIX, So when test runs it will write to preference file which is different then main application.
public class SpecializedMockContext extends RenamingDelegatingContext {
public static final String PREFIX = "test.";
public SpecializedMockContext(Context context) {
super(context, PREFIX);
}
#Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return super.getSharedPreferences(PREFIX + name, mode);
}
}
Set this SpecialisedMockContext to your test Application context. setContext(specialisedMockContext) and createApplication().
Related
I'm reading SharedPreferences in my app at startup, and after a few runs it will crash and tell me that I'm trying to cast from a String to a Boolean. Below is the code that I use to read and write this value.
// Checks if the realm has been copied to the device, and copies it if it hasn't.
private void copyRealm() {
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
if (!sharedPreferences.getBoolean(getString(R.string.pref_copied), false)) {
// Copy the realm to device.
final String path = copyBundledRealmFile(getResources().openRawResource(R.raw.realm), getString(R.string.realm_name));
// Save the path of the realm, and that the realm has been copied.
sharedPreferences.edit()
.putBoolean(getString(R.string.pref_copied), true)
.putString(getString(R.string.pref_path), path)
.apply();
}
}
The two strange things are that it doesn't start happening for a few builds, and so far it has only happened on a simulator. I haven't been able to check a physical device yet, but I've also been running this code without change for several months and had no trouble.
Why would I be getting this message?
Caused by: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Boolean
at android.app.SharedPreferencesImpl.getBoolean(SharedPreferencesImpl.java:293)?
Take a look at this question Android getDefaultSharedPreferences.
It seems it's a better idea to just use
SharedPreferences preferences = getPreferences(MODE_PRIVATE);
or
SharedPreferences references1=getSharedPreferences("some_name",MODE_PRIVATE);
instead of using
SharedPreferences preferences= getDefaultSharedPreferences(this);
From the documentation:
getPreferences(MODE_PRIVATE) retrieves a SharedPreferences object for
accessing preferences that are private to this activity. This simply
calls the underlying getSharedPreferences(String, int) method by
passing in this activity's class name as the preferences name.
I regularly use one of these two approaches and had no problem of any kind so far.
I'm trying to write a test case to verify a class that writes to Shared Preferences.
I'm using Android Studio v1.5.
In the good old eclipse, when using AndroidTestCase, a second apk file was deployed to the device, and tests could be run using the instrumentation context, so you could run tests using the instrumentation apk's shared preferences without altering the main apk's existing shared preferences files.
I've spent the entire morning trying to figure out how to get a non null context in Android Studio tests. Apparently unit tests made for eclipse are not compatible with the Android Studio testing framework, as calling getContext() returns null.
I thought I've found the answer in this question:
Get context of test project in Android junit test case
Things have changed over time as old versions of Android Studio didn't have full testing support. So a lot of answers are just hacks. Apparently now instead of extending InstrumentationTestCase or AndroidTestCase you should write your tests like this:
#RunWith(AndroidJUnit4.class)
public class MyTest {
#Test
public void testFoo(){
Context instrumentationContext = InstrumentationRegistry.getContext();
Context mainProjectContext = InstrumentationRegistry.getTargetContext();
}
}
So I now have a non null instrumentation context, and the getSharedPreferences method returns an instance that seems to work, but actually no preferences file is being written.
If I do:
context = InstrumentationRegistry.getContext();
Then the SharedPreferences editor writes and commits correctly and no exception is thrown. On closer inspection I can see that the editor is trying to write to this file:
data/data/<package>.test/shared_prefs/PREFS_FILE_NAME.xml
But the file is never created nor written to.
However using this:
context = InstrumentationRegistry.getTargetContext();
the editor works correctly and the preferences are written to this file:
/data/data/<package>/shared_prefs/PREFS_FILE_NAME.xml
The preferences are instantiated in private mode:
SharedPreferences sharedPreferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
As far as I know, no test apk has been uploaded to the device after running the test. This might explain why the file was not written using the instrumentation context. Is it possible that this context is a fake context that fails silently?
And if this were the case, how could I obtain a REAL instrumentation context so that I can write preferences without altering the main project's preferences?
Turns out you can't write to shared preferences using the instrumentation context, and this was true even in eclipse. This would be the equivalent test for eclipse:
import android.content.Context;
import android.content.SharedPreferences;
import android.test.InstrumentationTestCase;
public class SharedPrefsTest extends InstrumentationTestCase {
public void test() throws Exception {
Context context = getInstrumentation().getContext();
String fileName = "FILE_NAME";
SharedPreferences sharedPreferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("key", "value");
editor.commit();
SharedPreferences sharedPreferences2 = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
assertEquals("value", sharedPreferences2.getString("key", null));
}
}
I just ran it and it also fails. The preferences are never written. I think internal storage file access is forbidden in this context, as calling Context.getFilesDir() throws an InvocationTargetException, and so does calling File.exists() over the preferences file (you can check which file is the editor writing to using the debugger, just look for a private variable called mFile inside the this.$0 member instance).
So I was wrong in thinking this was actually possible. I though we had used the instrumentation context for data access layer testing in the past, but we actually used the main context (AndroidTestCase.getContext()), although we used different names for the preferences and SQLite files. And this is why the unit tests didn't modify the regular app files.
The instrumentation will be installed alongside with your application. The application will run itself, thus reading and writing its own SharedPreferences.
It is odd, that the SharedPreferences of the Instrumentation get deleted (or never created), but even if they would be created, you would have a hard time passing them into your application under test. As stated above just calling context.getSharedPreferences(); inside your app will still provide the actual apps preferences, never the ones of your instrumentation.
You will need to find a way to provide the preferences to your application under test. A good solution to this would be to keep the preferences in your Application like the following:
public class App extends Application {
SharedPreferences mPreferences;
public void onCreate() {
mPreferences = getSharedPreferences(fileName, Context.MODE_PRIVATE);
}
// add public getter / setter
}
This way, you can
As long as you have a context get the preferences from a single source using ((App) context.getApplicationContext()).getPreferences()
Set the preferences yourself before running your tests and starting any activities to inject your test data.
In your test setup then call the following to inject any preferences you need
#Before
public void before() {
((App) InstrumentationRegistry.getTargetContext()).setPreferences(testPreferences);
}
Be sure to correctly finish off activities after each test, so that each test can get their own dependencies.
Also, you should strongly think about just mocking the SharedPreferences using Mockito, other frameworks, or simply implementing the SharedPreferences interface yourself, since this greatly simplifies verifying interactions with models.
Note that you can still use Android Studio to run your old-style tests which extend InstrumentationTestCase, AndroidTestCase, or ActivityInstrumentationTestCase2 in Android Studio.
The new Testing Support Library provides ActivityTestRule to provide functional testing of a single activity. I believe you need to create one of these objects before attempting to get the Instrumentation and/or Context used for the test. The documentation for Espresso has an example for using this class.
If you want to test classes without having to create an Activity, I found it's easiest to use Robolectric. Robolectric provides you with a mock context that does everything a context does. In fact, this context is the main reason I use Robolectric for unit testing.
I've defined an instance of SharedPreferences that used on multi-process mode.
public class Prefs {
private static SharedPreferences prefs;
private static SharedPreferences.Editor editor;
private static void init(Context context) {
prefs = context.getSharedPreferences("alaki",
Context.MODE_MULTI_PROCESS);
editor = prefs.edit();
}
// static methods to set and get preferences
}
Now I'm using this class on a service with separate process and also in my main application process in static way.
Everything is going well, but sometimes all stored data on SharedPreferences instance removed!
How can I solve this problem?
Edit:
Finally I've solved my problem using by IPC.
There is currently no way of safely accessing SharedPreferences on multiple processes, as described in its documentation.
Note: This class does not support use across multiple processes.
After testing a lot with MODE_MULTI_PROCESS, I've three trials to share:
1- Initialize the SharedPreferences once in each process and use it multiple times.
The problem: The values are not reflected in each process as expected. So each process has its own value of the SharedPreferences.
2- Initialize the SharedPreferences in each put or get.
This actually works and the value now is interchangeable between processes.
The problem: sometimes after aggressively accessing the sharedpref, the shared preferences file got deleted with all its content, as described in this issue, and I get this warning in the log:
W/FileUtils﹕ Failed to chmod(/data/data/com.hegazy.multiprocesssharedpref/shared_prefs/myprefs.xml): android.system.ErrnoException: chmod failed: ENOENT (No such file or directory)
You can find why this happens in the issue.
3- Use synchronization to lock the methods that put and get values in the SharedPreferences.
This is completely wrong; synchronization doesn't work across processes. The SharedPreferences is actually using synchronization in its implementation, but that only ensures thread safety, not process safety. This is described very well here.
SharedPreferences itself is not process-safe. That's probably why SharedPreferences documentation says
Note: currently this class does not support use across multiple processes. This will be added later.
I've worked around this by combining:
Providing each process mutually-exclusive access to the SharedPreferences file (such as by using a socket-based locking mechanism)
Re-initialising the SharedPreferences with the MODE_MULTI_PROCESS flag every time you want to use it to bypass in-memory caching
This seems to work OK, but it hasn't been thoroughly tested in the real world, so I don't know if it's perfectly reliable.
You can see a working example I wrote here.
Warning: Looks like MODE_MULTI_PROCESS has been deprecated in Android M. It might stop working in the future.
Using the commit() method store the changes in persistent storage, hence it is slow and would make conflict across multiple call from other processes.
However there is an alternative to this method, you should call the apply() method, this method stores the changes in memory and then in disk storage asynchronously, so it is more reliable.
recalls that the use of context objects as static field, you have the risk of leakage of context because not declare the object in the application class
public class CustomApplication extends Application{
private Prefs prefs;
public void onCreate(){
prefs = new Prefs(this);
}
public Prefs getPrefs(){
return prefs;
}
}
From any context you can get the prefs
((MyApplication)context.getApplicationContext()).getPrefs();
Use a Content Provider which uses SharedPreferences. Example see here: https://github.com/hamsterksu/MultiprocessPreferences
public static int getValore(Context ctx, String contenitore, String chiave, int valore){
try {
SharedPreferences sh = ctx.getApplicationContext()
.getSharedPreferences(contenitore, Context.MODE_MULTI_PROCESS);
//SharedPreferences.Editor editor = sh.edit();
return sh.getInt(chiave, valore);
}catch (Exception ex){
return valore;
}
}
If two processes write data to SharedPreferences, then it might possible all SharedPreferences are reset to default values.
Also you can try to call clear() on the editor before storing val
SharedPreferences.Editor sp = settings.edit();
sp.clear();
sp.putString("Name", "YourName");
sp.commit();
I'm developing an android application. I'm using android 2.2
In my application I am capturing GPS data and sending it to service with the 1 hour time interval. If user exits from application it's also working (it is required).
I'm using 2 services (User defined), one for capturing GPS data and other for sending to the server.
Here my doubt
In service, can we use shared preferences.
If we store any data in shared preferences in any activity of the application, will we be able to use that data in service with the help of shared preferences?
You can access the default shared preferences instance, which is shared across all your Activity and Service classes, by calling PreferenceManager.getDefaultSharedPreferences(Context context):
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
This is great for storing simple primitives (like booleans) or serializable objects. However, if you're capturing a lot of location data, you might consider using a SQLite database instead.
I find the solution.
Inside a service we call the following method to get the shared preferences
myapp.bmodel.getApplicationContext().getSharedPreferences("myPrefs_capture_gps_per_hour", Context.MODE_PRIVATE);
In the above code myapp is a object of the application class which is derived from Application
You need a context to get access to shared preferences. The best way is to create MyApplication as a descendant of Application class, instantiate there the preferences and use them in the rest of your application as MyApplication.preferences:
public class MyApplication extends Application {
public static SharedPreferences preferences;
#Override
public void onCreate() {
super.onCreate();
preferences = getSharedPreferences( getPackageName() + "_preferences", MODE_PRIVATE);
For example, if you need access to your preferences somewhere else, you may call this to read preferences:
String str = MyApplication.preferences.getString( KEY, DEFAULT );
Or you may call this to save something to the preferences:
MyApplication.preferences.edit().putString( KEY, VALUE ).commit();
(don't forget to call commit() after adding or changing preferences!)
Yes Shivkumar, you can use your share preferences in any kind of services as normal as you are using in your Activity.
same like
SharedPreferences preferences = getSharedPreferences("<PrefName>",
MODE_PRIVATE);
There are two ways to create instance of SharedPreference:
Case 1:
SharedPreferences preferences = activity.getSharedPreferences("<PrefName>", MODE_PRIVATE);
Case 2:
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
Notice if you create a preference with the same name (case 1) or same context (case 2) even at different places, it's still the same, and can share data, obviously.
Looking at the SharedPreferences docs it says:
"Note: currently this class does not
support use across multiple processes.
This will be added later."
So in and of itself it doesn't appear to be Thread Safe. However, what kind of guarantees are made in regards to commit() and apply()?
For example:
synchronized(uniqueIdLock){
uniqueId = sharedPreferences.getInt("UNIQUE_INCREMENTING_ID", 0);
uniqueId++;
sharedPreferences.edit().putInt("UNIQUE_INCREMENTING_ID", uniqueId).commit();
}
Would it be guaranteed that the uniqueId was always unique in this case?
If not, is there a better way to keep track of a unique id for an application that persists?
Processes and Threads are different. The SharedPreferences implementation in Android is thread-safe but not process-safe. Normally your app will run all in the same process, but it's possible for you to configure it in the AndroidManifest.xml so, say, the service runs in a separate process than, say, the activity.
To verify the thready safety, see the ContextImpl.java's SharedPreferenceImpl from AOSP. Note there's a synchronized wherever you'd expect there to be one.
private static final class SharedPreferencesImpl implements SharedPreferences {
...
public String getString(String key, String defValue) {
synchronized (this) {
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
...
public final class EditorImpl implements Editor {
public Editor putString(String key, String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
...
}
}
However for your case of the unique id it seems you'd still want a synchronized as you don't want it to change between the get and the put.
I was wondering the same thing - and came across this thread that says they are not thread safe:
The implementations of Context.getSharedPreferences() and Editor.commit
() do not synchronize on the same monitor.
I have since looked at the Android 14 code to check, and it is quite involved. Specifically SharedPreferencesImpl seems to use different locks when reading & writing to disk:
enqueueDiskWrite() locks on mWritingToDiskLock
startLoadFromDisk() locks on this, and launches a thread locking on SharedPreferencesImpl.this
I'm unconvinced that this code really is safe.
I think that will do it.
You can test it using sleep inside the synchronized section and call it from different threads
You should be aware that SharedPreferences are not working on Samsung handsets, have a look at android issue.
I have implemented simple database preferences storage which you can find on github.
Cheers,