I am new in Android unit testing and I want to add some unit tests in an existing project. I am using the MVP design architecture. Inside my presenter, I have a call to PreferenceManager in order to get the default SharedPrefences but it always returns null. I have followed some tutorials and advices across stackoverflow on how to mock PreferenceManager and SharedPreferences but I can't make it work. This is my presenter class
#RunWith(MockitoJUnitRunner.class)
public class SettingsPresenterTest {
#Mock
private SettingsView mView;
#Mock
private LocalConfiguration conf;
private SettingsPresenter mPresenter;
public SettingsPresenterTest() {
super();
}
#Before
public void startUp() throws Exception {
MockitoAnnotations.initMocks(LocalConfiguration.class);
MockitoAnnotations.initMocks(PreferenceManager.class);
mPresenter = new SettingsPresenter(mView);
final SharedPreferences sharedPrefs =
Mockito.mock(SharedPreferences.class);
final Context context = Mockito.mock(Context.class);
Mockito.when(PreferenceManager.getDefaultSharedPreferences(context)).
thenReturn(sharedPrefs);
}
#Test
public void notificationsEnabledClicked() throws Exception {
boolean notifsEnabled = false;
mPresenter.notificationsEnabledClicked(notifsEnabled);
Mockito.verify(mView).setNotificationsView(notifsEnabled);
}
}
and here is the method where the SharedPreferences are returned null
public class LocalConfiguration {
public TerritoryDto getLastSavedTerritory() {
SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(H.getContext());
String terrritoryString = preferences.getString(SAVED_TERRITORY,
null);
return
SerializerHelper.getInstance().deserialize(terrritoryString,
TerritoryDto.class);
}
}
Could you give me some guidelines on how to resolve this error?
Instead of directly referring to Android SDK, abstract that out from your presenter logics. What this means is, that instead of performing PreferenceManager.getDefaultSharedPreferences(), create an abstraction and ask for territoryString from your abstraction.
What will this give to you, is that your presenter won't know about the precense of neither PreferenceManager nor SharedPreferences, which are from Android SDK, thus you would have enough seams to perform pure unit testing.
Having said this, let's implement the abstractions. Having declared following interface:
public interface Storage {
#Nullable
String getSavedTerritory();
}
To which the concrete implementation would be:
public SharedPrefsStorage implements Storage {
private final SharedPreferences prefs;
public SharedPrefsStorage(Context context) {
prefs = PreferenceManager.getDefaultSharedPreferences();
}
#Nullable
#Override
public String getSavedTerritory() {
return prefs.getString(SAVED_TERRITORY, null);
}
}
Then your presenter would become something like this:
public class LocalConfiguration {
final Storage storage;
public LocalConfiguration(Storage storage) {
this.storage = storage;
}
public TerritoryDto getLastSavedTerritory() {
final String territory = storage.getSavedTerritory();
return SerializerHelper.getInstance().deserialize(territory, TerritoryDto.class);
}
}
This would give you a seam to perform pure unit testing:
#Test
void someTest() {
when(storage.getSavedTerritory()).thenReturn("New York");
...
}
No need to worry about mocking PreferenceManager anymore.
Related
I've started learning android unit tests, but it looks very hard to find some good guides or information. Every example have a stupid example about 2+2 = 4
Say I write a little SDK which has few functions
MySdk.Init(Context context)
MySdk.CallTask()
I create an androidTest file
How should I call my SDK functions to check how they work? Somewhere required parameters like int/string/context. I just really don't understand, please help me.
This is what I've tried
public class AndroidTest {
private Activity context;
//default test
#Test
public void addition_correct() throws Exception {
assertEquals(4, 2 + 2);
}
#Test
public void checkContext() {
context = getActivity();
assertNotNull(context);
}
#Test
public void testInitPhase() {
MySdk.Init(context, new SdkInitializationListener() {
#Override
public void onInitializationSuccessful(String adv_id) {
assert (adv_id != null);
}
#Override
public void onInitializationError() {
}
});
}
}
For context i was tried context = new mockContext();. It's passed as context = null and my SDK failed with initialization.
Unit tests are mainly about testing an individual class in isolation, so that you can check if individual public methods of a class behave as you intend them to, and continue to do so if you change that class' code in the future. Let's say you have a class like this:
public class UtilityFunctions {
public int double(int value) {
return value * 2;
}
public String mirror(String value) {
if (value == null) return "";
return value + new StringBuilder(value).reverse().toString();
}
}
You want to test these two methods with:
valid input values, and check the output is as expected
invalid values, and check that errors are handled accordingly (and the correct exceptions thrown if necessary)
So a test class for the above class may look like this
#RunWith(JUnit4.class)
public class UtilityFunctionsTest {
private UtilityFunctions utility;
#Before
public void setUp() {
// Initialises any conditions before each test
utility = new UtilityFunctions();
}
#Test
public void testDoubleFunction() {
assertEquals(2, utility.double(1));
assertEquals(8, utility.double(4));
assertEquals(-12, utility.double(-6));
assertEquals(0, utility.double(0));
}
#Test
public void testMirror() {
assertEquals("", utility.mirror(null));
assertEquals("", utility.mirror(""));
assertEquals("aa", utility.mirror("a"));
assertEquals("MirrorrorriM", utility.mirror("Mirror"));
}
}
These standard Java unit tests are run from the test directory. However, you'll need to run tests in the androidTest directory whenever you're using Android-specific classes such as Context. If you're creating a MockContext, you're simply creating an empty Context whose methods don't do anything.
Without me knowing anything about what your MySDK does, I think you may need to pass a fully-functioning Context into your class for your tests. The Android JUnit runner does provide this with InstrumentationRegistry.getTargetContext(), so for your example, you may need to add this #Before method:
#Before
public void setUp() {
context = InstrumentationRegistry.getTargetContext();
}
You'll also need to remove the context = getActivity(); line from your first test.
I tried to create custom class to fetch some values from SharedPreferences.
My aim is to reach to that values from any class.
I am getting null Pointer exception on
SharedPreferences prefs = getApplicationContext().getSharedPreferences("UserFile", MODE_PRIVATE);
My code is as below;
public class UserInfo extends Application {
private String token;
private String SAVED_USERNAME;
public UserInfo() {
SharedPreferences prefs = getApplicationContext().getSharedPreferences("UserFile", MODE_PRIVATE);
token = prefs.getString("Token", null);
}
public String getToken() {
return token;
}
}
What might be the wrong?
Usually Android components are initialized during their lifecycle. In this particular case you can't access application Context and SharedPreferences because they're not initialized yet.
Second problem might be (thanks to my crystall ball) that you did not added your Application to AndroidManifest
So, your first thought might be to move initialization code from constructor to onCreate. This would solve this particular problem.
However, it's a bad practice to do what you're doing. Because there can be only 1 Application component per application. This will limit you to 1 such singleton per app. Consider using Application to provide application Context as singleton and create another singleton for providing UserInfo.
No examples, please exercise yourself.
Just have this method in a util class. No need to extend application.
public static String getToken(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getString("Token", null);
}
There is a rule in android - don't use constructor of app component: Activity/Fragment/Application/Service... there is onCreate() method, because in your constructor context will be null. So move your code to onCreate(). Also you need set your UserInfo as application in Manifest.
You don't create constructor of Application class instead, use the code in onCreate():
#Override
public void onCreate() {
super.onCreate();
SharedPreferences prefs = getApplicationContext().getSharedPreferences("UserFile", MODE_PRIVATE);
token = prefs.getString("Token", null);
}
and use it from any activity:
UserInfo userInfo = (UserInfo)getApplication();
String token = userInfo.getToken();
public class MyApp extends Application {
private static MyApp _instance;
#Override
public void onCreate() {
super.onCreate();
_instance = this;
}
public static MyApp getInstance(){
return _instance;
}
public String getToken() {
return getSharedPreferences("UserFile", MODE_PRIVATE).getString("Token", null);
}
}
In your manifest:
<application
android:name="your.package.MyApp"
>
If you whant use :
String token = MyApp.getInstance().getToken();
Make sure you have registered this class in your AndroidManifest.XML file.
<application android:name=".UserInfo"
...
/>
Note: Your way for accessing shared preferences does not seem good. I rather myself declare a class named PreferencesHelper and put all preferences stuff there.
public class PreferencesHelper{
private SharedPreferences mPrefs;
public PreferencesHelper(Context context){
this.mPrefs = context.getSharedPreferences("name", Context.MODE_PRIVATE);
}
public getToken() {
return mPrefs.getString("Token", null);
}
public String setToken(String token) {
mPrefs.edit().putString("Token", token).apply();
}
}
I've been playing around with Transfuse (http://androidtransfuse.org/) and am now trying to tackle SharedPreferences. The documentation makes this seem pretty straight forward:
#Activity
public class Example{
#Inject #Preference(value = "favorite_color", default = "green")
String favColor;
}
However, as I understand it, SharedPreferences are retrieved by name, not just by key. So, how does Transfuse know the name of the SharedPreferences file I'm trying to access?
I've tried something like this to no avail:
#Activity
public class MainActivity{
public static final String PREF_NAME = "pref_name";
#Inject
android.app.Activity mActivity;
#Inject #Preference(value = PREF_NAME, defaultValue = "")
String mPreference;
#Inject #View(R.id.preference)
EditText mPreferenceEditText;
#RegisterListener(R.id.button_2)
android.view.View.OnClickListener mSavePrefListener = new android.view.View.OnClickListener() {
#Override
public void onClick(android.view.View v) {
String val = mPreferenceEditText.getText().toString();
mActivity.getSharedPreferences("the_shared_prefs", Context.MODE_PRIVATE)
.edit()
.putString(PREF_NAME, val)
.apply();
}
};
#OnResume
private void displayPrefText(){
mPreferenceEditText.setText(mPreference);
}
}
Thanks for your help!
The #Preference injection uses the PreferenceManager.getDefaultSharedPreferences() method (as CommonsWare suggested) to look up the SharedPreferences object. This is a convenience for using the default preferences directly. Your example would basically generate the following injection:
delegate.favColor = activity.getDefaultSharedPreferences()
.getString("favorite_color", "green");
If you like, you can set up Transfuse to inject a specific SharedPreferences object via a Provider or #Provides method and qualifier:
#TransfuseModule
class Module{
#Provides #Named("the_shared_prefs")
public SharedPreferences build(Activity activity){
return activity.getSharedPreferences("the_shared_prefs", Context.MODE_PRIVATE)
}
}
Which you could then inject into your activity like so:
#Activity
public class MainActivity{
public static final String PREF_NAME = "pref_name";
#Inject #Named("the_shared_prefs")
SharedPreferences theSharedPreferences;
#Inject #View(R.id.preference)
EditText mPreferenceEditText;
#RegisterListener(R.id.button_2)
android.view.View.OnClickListener mSavePrefListener = new android.view.View.OnClickListener() {
#Override
public void onClick(android.view.View v) {
String val = mPreferenceEditText.getText().toString();
theSharedPreferences
.edit()
.putString(PREF_NAME, val)
.apply();
}
};
}
We may want to expand the #Preference injection to allow you to specify a non-default shared preferences. Would this help?
I want to introduce dependency injection through Dagger to a project. The following code acts as an example to describe the problem of injection into static classes.
The static method setupTextView() is called from multiple classes:
public abstract class TextViewHelper {
public static void setupTextView(TextView textView,
Spanned text,
TrackingPoint trackingPoint) {
textView.setText(text, TextView.BufferType.SPANNABLE);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
MyApp.getTracker().track(trackingPoint);
}
});
}
}
Here is one example how the helper method is used:
TextViewHelper.setupTextView(this, R.id.some_text_view,
R.string.some_text,
TrackingPoint.SomeTextClick);
The tracking used in the helper method is provided by the application class:
public class MyApp extends Application {
private static Tracking mTracking;
public void onCreate() {
super.onCreate();
mTracking = getTracking(getApplicationContext());
}
private Tracking getTracking(Context context) {
if (BuildConfig.DEBUG) {
return new NoTracking();
} else {
return new NsaTracking(context);
}
}
public static Tracking getTracker() {
return mTracking;
}
}
Now, I want to inject the tracking via Dagger. When I refactored the code I noticed that I would need to pass the tracking object from my Activity or Fragment to the static helper since I cannot directly inject into the static class:
TextViewHelper.setupTextView(this, R.id.some_text_view,
R.string.some_text,
TrackingPoint.SomeTextClick,
Tracking tracking);
This does not feel like a good design pattern - since I pass the TrackPoint and the Tracking object. How would you improve this?
In your TextViewHelper create a static field with the tracker.
public class TextViewHelper {
private TextViewHelper(){}
#Inject
static Tracking sTracker;
public static void setupTextView(TextView textView,
Spanned text,
TrackingPoint trackingPoint) {
textView.setText(text, TextView.BufferType.SPANNABLE);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
sTracker.track(trackingPoint);
}
});
}
}
Here is how to configure the module:
#Module(staticInjections = TextViewHelper.class)
public class TrackerModule {
...
}
And the most important, call injectStatics on your graph.
mObjectGraph = ObjectGraph.create(new TrackerModule());
mObjectGraph.injectStatics();
Edit:
As you noted Dagger's documentation states that static injections "should be used sparingly because static dependencies are difficult to test and reuse." It is all true, but because you asked how to inject object into utility class this is the best solution.
But if you want your code to be more testable, create a module like below:
#Module(injects = {classes that utilizes TextViewHelper})
public class TrackerModule {
#Provides
Tracking provideTracker() {
...
}
#Provides
#Singleton
TextViewHelper provideTextViewHelper(Tracking tracker) {
return new TextViewHelper(tracker);
}
}
Now you can remove static from TextViewHelper methods because this utility class will be injected using dagger.
public class TextViewHelper {
private final Tracking mTracker;
public TextViewHelper(Tracking tracker){
mTracker = tracker;
}
public void setupTextView(TextView textView,
Spanned text,
TrackingPoint trackingPoint) {
...
}
}
This is how it should be done if you want to follow good practices. Both solution will work so it's up to you to choose one.
I want to create a singleton object using RoboGuice but I get null exception. I don't know what is wrong with my codes.
#Singleton
public class SessionService {
private static Session session;
public Session getSession() {
if (session == null){
session = new Session();
}
return session;
}
}
--
public class ChannelManager {
#Inject SessionService sessionService;
public String getName(){
return sessionService.getSession().getName();
}
}
public class MainActivity extends RoboActivity{
#InjectView(R.id.button1) Button btn;
#Inject SessionService a;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
a.getSession().setName("dsadas");
Log.i("A","NEW: "+ a.getSession().getName());
Log.i("A","NEW NAME: "+ new ChannelManager().getName());
}
I get null exception on "new ChannelManager().getName()" line. What's wrong with that?
Thanks in advance.
When you do new ChannelManager(), you are not using Guice injection, so your injected fields are null.
To inject your ChannelManager, either use the #Inject annotation or use the following code to create your instance:
ChannelManager myChannelManager = RoboGuice.getInjector(this).getInstance(ChannelManager.class);
Also consider if there is necessity to use 'new' operator to create e Object. This always implicate some problems especially in (unit)tests.