Configuration change using Robolectric - android

To retain my AsyncTasks across configuration changes, I use a fragment-based solution with setRetainInstance(true), which hosts each AsyncTask and calls back to a listening Activity, similar to this solution http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
Ultimately, the purpose is to test the AsyncTask's retention functionality throughout configuration changes using Robolectric, but I need to start with setting up the actual configuration change correctly. However, it seems I can't mimic the exact reference behavior that occurs during a configuration change.
Real app: When running a real app, on configuration change, the Activity is destroyed and recreated while the Fragment is retained, so it seems to be working. I can see this by checking their references before and after the configuration change (example references used below):
Real app, before:
Activity: abc
Fragment: xyz
Real app, after:
Activity: bca
Fragment: xyz (properly retained and reattached)
Case 1: When running recreate() on the Activity in the Robolectric test, however, the Activity doesn't seem to have its instance properly recreated (despite the docs saying the method performs all the lifecycle calls):
mActivityController =
Robolectric.buildActivity(AsyncTaskTestActivity.class).attach().create().start().resume().visible();
mActivity = mActivityController.get();
mActivity.recreate();
Robolectric with recreate(), before:
Activity: abc
Fragment: xyz
Robolectric with recreate(), after
Activity: abc
Fragment: xyz
This leads me to believe that a new Activity instance isn't properly created and the reattachment functionality therefore hasn't happened in a real way.
Case 2: If I create the test based on individual lifecycle calls instead:
mActivityController = Robolectric.buildActivity(AsyncTaskTestActivity.class).attach().create().start().resume().visible();
mActivityController.pause().stop().destroy();
mActivityController = Robolectric.buildActivity(AsyncTaskTestActivity.class).attach().create().start().resume().visible();
In this version, it seems the Activity gets fully replaced from scratch, but so does also the Fragment:
Robolectric with separate lifecycle calls, before
Activity: abc
Fragment: xyz
Robolectric with separate lifecycle calls, after
Activity: bca
Fragment: yzx
It seems I'm either reusing the same Activity (case 1) or replacing everything with new instances, as if there is no underlying Application that retains the Fragment (case 2).
Question: is there any way I can set up my Robolectric test to mimic the reference result that I get when running the app in an actual Android environment (as per the Real app result), or am I stuck with either creating a separate test app or settling with Robotium functional tests? I tried to do it like this https://stackoverflow.com/a/26468296 but got the same result as my case 2.
Thanks in advance!

I have played around a bit and came up with a solution using Robolectric 3.0 and Mockito:
#RunWith(RobolectricGradleTestRunner.class)
#Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.KITKAT, shadows = {ExampleActivityTest.ExampleActivityShadow.class})
public class ExampleActivityTest {
#Mock
private FragmentManager fragmentManagerMock;
#Before
public void setup() {
initMocks(this);
setupFragmentManagerMock();
}
#Test
public void testRestoreAfterConfigurationChange() {
// prepare
ActivityController<ExampleActivity> controller = Robolectric.buildActivity(ExampleActivity.class);
ExampleActivity activity = controller.get();
ExampleActivityShadow shadow = (ExampleActivityShadow) Shadows.shadowOf(activity);
shadow.setFragmentManager(fragmentManagerMock);
ActivityController<ExampleActivity> controller2 = Robolectric.buildActivity(ExampleActivity.class);
ExampleActivity recreatedActivity = controller2.get();
ExampleActivityShadow recreatedActivityShadow = (ExampleActivityShadow) Shadows.shadowOf(recreatedActivity);
recreatedActivityShadow.setFragmentManager(fragmentManagerMock);
// run & verify
controller.create().start().resume().visible();
activity.findViewById(R.id.inc_button).performClick();
activity.findViewById(R.id.inc_button).performClick();
assertEquals(2, activity.lostCount.count);
assertEquals(2, activity.retainedCount.count);
Bundle bundle = new Bundle();
controller.saveInstanceState(bundle).pause().stop().destroy();
controller2.create(bundle).start().restoreInstanceState(bundle).resume().visible();
assertEquals(0, recreatedActivity.lostCount.count);
assertEquals(2, recreatedActivity.retainedCount.count);
}
private void setupFragmentManagerMock() {
final HashMap<String, Fragment> fragments = new HashMap<>();
doAnswer(new Answer<Object>() {
#Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return fragments.get(invocation.getArguments()[0]);
}
}).when(fragmentManagerMock).findFragmentByTag(anyString());
final HashMap<String, Fragment> fragmentsToBeAdded = new HashMap<>();
final FragmentTransaction fragmentTransactionMock = mock(FragmentTransaction.class);
doAnswer(new Answer<Object>() {
#Override
public Object answer(InvocationOnMock invocation) throws Throwable {
fragmentsToBeAdded.put((String) invocation.getArguments()[1], (Fragment) invocation.getArguments()[0]);
return fragmentTransactionMock;
}
}).when(fragmentTransactionMock).add(any(Fragment.class), anyString());
doAnswer(new Answer<Object>() {
#Override
public Object answer(InvocationOnMock invocation) throws Throwable {
fragments.putAll(fragmentsToBeAdded);
return null;
}
}).when(fragmentTransactionMock).commit();
when(fragmentManagerMock.beginTransaction()).thenReturn(fragmentTransactionMock);
}
#Implements(Activity.class)
public static class ExampleActivityShadow extends ShadowActivity {
private FragmentManager fragmentManager;
#Implementation
public FragmentManager getFragmentManager() {
return fragmentManager;
}
public void setFragmentManager(FragmentManager fragmentManager) {
this.fragmentManager = fragmentManager;
}
}
}
Note that I have only mocked the methods of FragmentManager (beginTransaction() and findFragmentByTag()) and FragmentTransaction (add() and commit()) that I use in my code, so you might need to expand these depending on your code.
I haven't done too much work with Robolectric yet, so there may be a more elegant solution to this, but this works for me for now.
You can see the full source code and project setup here: https://github.com/rgeldmacher/leash (might be worth a look if you still need to retain objects ;) )

Related

How to pass complex, non serializable object to android fragments

Hello fellow Android developers,
I wanna know how do you guys pass complex non serializable (& non parcelable) object to fragments. (such as Listener, Api client, ...)
Let me explain my use case:
The use case
I'm building an Android application composed of one "host" activity and 3 fragments.
Currently I'm passing the object using a custom constructor on the fragment (bad practice I know).
The fragments constructors looks like the following:
/**
* Do not remove ever or you'll face RuntimeException
*/
public FirstFragment() {
}
public FirstFragment(Session session,
ApiClient apiClient,
FirebaseAnalytics firebaseAnalytics) {
mSession = session;
mApiClient = apiClient;
mFirebaseAnalytics = firebaseAnalytics;
}
And I'm using them in the host activity like this
private FirstFragment getFirstFragment() {
if (mFirstFragment == null) {
mFirstFragment = new FirstFragment(mSession, mApiClient, mFirebaseAnalytics);
}
return mHomeFragment;
}
[...]
private void loadFragment(Fragment fragment, String tag) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.frame_container, fragment, tag);
transaction.commit();
}
[...]
private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
= new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
switch (item.getItemId()) {
case FIRST_FRAGMENT_RES_ID:
toolbar.setTitle(R.string.first_fragment_title);
loadFragment(getFirstFragment(), "first_fragment");
return true;
[...]
}
return false;
}
};
This solution works well almost all the time. But sometimes (and I don't know when exactly) the default constructor is invoked and therefore all local members are null.
Possible solutions
To solve the problem I'm thinking about the following solutions:
Singletons, singletons everywhere
Most of the objects I'm passing are singletons therefore I can access them in the default constructor of the fragments:
public FirstFragment() {
mSession = Session.getInstance(getContext());
mApiClient = ApiClient.getInstance(getContext());
mFirebaseAnalytics = FirebaseAnalytics.getInstance(getContext());
}
Problems
However the above solution wouldn't work if I need to pass a callback or something. How can it be done like this then?
Access the objects using parent activity
I think it's one of the ugliest possible solutions because it will couple the Fragments to the parent activity. The idea is something like this
public FirstFragment() {
mSession = Session.getInstance(getContext());
mApiClient = ApiClient.getInstance(getContext());
mFirebaseAnalytics = FirebaseAnalytics.getInstance(getContext());
mListener = (Listener) getActivity(); // <- will works because parent activity implement the interface
}
Using broadcast & receiver
The idea is to keep passing singleton everywhere and use broadcast & receiver instead of listener.
How do you guys managed this scenario?
Thanks in advance !
You probably want to look into dependency injection (using a tool like Dagger or alternatives), especially for objects like an Api Client. Post the setup, you'd define, just once, how an Api Client instance could be constructed. And later you can use it pretty much everywhere with a one-line statement. The instance is guaranteed to be available upon the fragment instantiation. Further reading: https://dagger.dev/tutorial/
According to your use case, it might be easier to use a ViewModel and store your objects there. Your ViewModel will be shared across your fragments and your host
activity.
See https://developer.android.com/topic/libraries/architecture/viewmodel
Have you considered using "Shared" ViewModel?
Essentially, a sub-class of ViewModel (which is class designed to store and manage UI-related data in a lifecycle conscious way for activities and fragments) can be created like below,
class SharedViewModel : ViewModel()
Inside this class you can have your custom objects with their correct state
Next, in your 1st Fragment you can obtain a handle to this SharedViewmodel like below,
class MasterFragment : Fragment() {
private lateinit var model: SharedViewModel
And obtain the handle to it using below code,
model = activity?.run {
ViewModelProviders.of(this)[SharedViewModel::class.java]
}
You can write your own logic/method/flow inside SharedViewModel to manipulate any custom object's states.
And once all this is done, In your 2nd Fragment, you can create the handle to SharedViewModel similar to above code and using SharedViewModel object you can retrieve the "modified" custom object from same SharedViewModel
It's been several months and I have now come up with a different solution.
For the UI related data
For the UI related stuff I'm now using the androidx livedata
For the complex non serializable data
My use case was to pass complex object to the fragment, such as manager, parent activity (trough a listener), etc... The approach I have taken is by injecting these data manually from the parent activity.
The first things to do was to remove the objects from the fragment constructor and use the default constructor instead, so that I won't face any instantiation errors.
Then I have created an inject() method on the fragment classes that look like this:
public void inject(BillingManager billingManager, Listener listener) {
mBillingManager = billingManager;
mListener = listener;
}
Each fragment will have their own inject method width the objects that should be injected as parameters.
In the parent activity I have override the onAttachFragment() method to handle the fragment attach process:
#Override
public void onAttachFragment(#NonNull Fragment fragment) {
super.onAttachFragment(fragment);
if (fragment.getClass().equals(FirstFragment.class)) {
((FirstFragment) fragment).inject(mBillingManager, this);
} else if (fragment.getClass().equals(HomeFragment.class)) {
((HomeFragment) fragment).inject(this);
}
}
Simple, and now everything work great.

Android ViewModel recreated on screen rotation

I found a case when architecture components ViewModel isn't retained - in short it goes as follows:
Activity is started and ViewModel instance is created
Activity is put to background
Device screen is rotated
Activity is put back to foreground
ViewModel's onCleared method is called and new object is created
Is it normal behavior of Android that my ViewModel instance is getting destroyed in this case? If so, is there any recommended solution of keeping its state?
One way I can think of is saving it once onCleared is called, however, it would also persist the state whenever activity is actually finishing. Another way could be making use of onRestoreInstanceState but it's fired on every screen rotation (not only if the app is in background).
Any silver bullet to handle such case?
Yes #tomwyr, this was a bug from an android framework. Bug details
The fix is available in 28.0.0-alpha3 and AndroidX 1.0.0-alpha3
But if you don't want to update to above versions now itself, Then you can solve like this (I know this is a bad solution but I didn't see any other good way)
In your activity override onDestroy method and save all the required fields to local variables before calling super.onDestroy. Now call super.onDestroy then Initialize your ViewModel again and assign the required fields back to your new instance of ViewModel
about isFinishing
Below code is in Kotlin:
override fun onDestroy() {
val oldViewModel = obtainViewModel()
if (!isFinishing) { //isFinishing will be false in case of orientation change
val requiredFieldValue = oldViewModel.getRequiredFieldValue()
super.onDestroy
val newViewModel = obtainViewModel()
if (newViewModel != oldViewModel) { //View Model has been destroyed
newViewModel.setRequiredFieldValue(requiredFieldValue)
}
} else {
super.onDestroy
}
}
private fun obtainViewModel(): SampleViewModel {
return ViewModelProviders.of(this).get(SampleViewModel::class.java)
}
AFAIK, ViewModel's only purpose is to survive and keep the data (i.e. "save the state") while its owner goes through different lifecycle events. So you don't have to "save the state" yourself.
We can tell from this that it's "not normal behavior". onCleared() is only called after the activity is finished (and is not getting recreated again).
Are you creating the ViewModel using the ViewModelProvider, or are you creating the instance using the constructor?
In your activity, you should have something like:
// in onCreate() - for example - of your activity
model = ViewModelProviders.of(this).get(MyViewModel.class);
// then use it anywhere in the activity like so
model.someAsyncMethod().observe(this, arg -> {
// do sth...
});
By doing this, you should get the expected effect.
For others that may not be helped by previous answers like me, the problem could be that you haven't set up your ViewModelProvider properly with a factory.
After digging around I solved my similiar problem by adding the following method to my Activities:
protected final <T extends ViewModel> T obtainViewModel(#NonNull AppCompatActivity activity, #NonNull Class<T> modelClass) {
ViewModelProvider.AndroidViewModelFactory factory = ViewModelProvider.AndroidViewModelFactory.getInstance(activity.getApplication());
return new ViewModelProvider(activity, factory).get(modelClass);
}
And then I did this in my Fragments:
protected final <T extends ViewModel> T obtainFragmentViewModel(#NonNull FragmentActivity fragment, #NonNull Class<T> modelClass) {
ViewModelProvider.AndroidViewModelFactory factory = ViewModelProvider.AndroidViewModelFactory.getInstance(fragment.getApplication());
return new ViewModelProvider(fragment, factory).get(modelClass);
}
I already had some abstract super classes for menu purposes so I hid the methods away there so I don't have to repeat it in every activity. That's why they are protected. I believe they could be private if you put them in every activity or fragment that you need them in.
To be as clear as possible I would then call the methods to assign my view model in onCreate() in my activity and it would look something like this
private MyViewModel myViewModel;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myViewModel = obtainViewModel(this, MyViewModel.class);
}
or in fragment
private MyViewModel myViewModel;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getActivity() != null) {
myViewModel = obtainFragmentViewModel(getActivity(), MyViewModel.class);
}
}
Change support library/compileSDK/targetSDK to 28.
I had similar issue with multi-window. When switching to split screen, my viewModel is recreated. Support library 28 fixed my problem. (My lifecycle version is 1.1.1)

How do I make my activity use testing data?

I have an application which displays data (posts) from a web API.
A background service syncs this data at some unknown time and saves it.
When visiting my main activity it loads this data and displays it in a RecyclerView
The loading is handled via a singleton class
I currently test the main activity as follows
#Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);
#Test
public void testDataLoad() {
int postsTotal = DataSingleton.getInstance().getPostsCount();
ViewInteraction empty = onView(withId(R.id.empty_view));
ViewInteraction recycler = onView(withId(R.id.recycler_view));
if (postsTotal == 0) {
empty.check(matches(isDisplayed()));
recycler.check(matches(not(isDisplayed())));
} else {
empty.check(matches(not(isDisplayed())));
recycler.check(matches(isDisplayed()));
recycler.check(new RecyclerViewItemCountAssertion(greaterThan(postsTotal)));
}
}
I know that this can't be the right way to write tests. I want to be able to test both with an empty data set and a non-empty set so that the if-else is two separate tests. The only way I think I can achieve it is to mock the data.
Is there another way?
Can I use Mockito to make the MainActivity use mock data without modifying the production code? Is my only choice to make it inject either real or mocked data providers in place of my singleton?
Is it better to just uninstall and reinstall my app each time so there is no data to start with and then continue with real data testing?
Android Activity are heavyweight and hard to test. Because we don't have control over the constructor, it is hard to swap in test doubles.
The first thing to do is to make sure you are depending on an abstraction of the data-source rather than a concretion. So if you are using a singleton with a getPostsCount() method then extract an interface:
interface DataSourceAbstraction {
int getPostsCount();
}
Make a wrapper class that implements your interface:
class ConcreteDataSource implements DataSourceAbstraction {
#Override
int getPostsCount() {
return DataSingleton.getInstance().getPostsCount();
}
}
And make the Activity depend on that rather than the concrete DataSingleton
DataSourceAbstraction dataSourceAbstraction;
#Override
protected void onCreate(Bundle savedInstanceState) {
super(savedInstanceState);
injectMembers();
}
#VisibleForTesting
void injectMembers() {
dataSourceAbstraction = new ConcreteDataSource();
}
You can now swap in a test double by subclassing and overriding injectMembers that has relaxed visibility. It's a bad idea do this in enterprise development, but there are less options in Android Activities where you don't control the constructor of the class.
You can now write:
DataSourceAbstraction dataSource;
//system under test
MainActivity mainActivity
#Before
public void setUp() {
mockDataSource = Mockito.mock(DataSourceAbstraction.class);
mainActivity = new MainActivity() {
#Override
void injectMembers() {
dataSourceAbstraction = mockDataSource;
}
};
}

Android: Writing test cases for Fragments

In my previous projects I've done most of the work through Activities and used ActivityInstrumentationTestCase2 as per the document:
http://developer.android.com/tools/testing/activity_testing.html
I have an idea how to work with Activity Test cases; but when it comes to Fragment ,I don't have much idea nor found much documents related to that.
So how to write test cases when I have several fragments with one or two actvities?
Any example code or sample would be more helpful.
Here's a rough guide using ActivityInstrumentationTestCase2:
Step 1. Create a blank Activity to hold your fragment(s)
private static class FragmentUtilActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout view = new LinearLayout(this);
view.setId(1);
setContentView(view);
}
}
Step 2:
Inside your test, instantiate your fragment and add it to the blank activity
public class MyFragmentTest extends ActivityInstrumentationTestCase2<FragmentUtilActivity> {
private MyFragment fragment;
#Before
public void setup() {
fragment = new MyFragment();
getActivity().getFragmentManager().beginTransaction().add(1, fragment, null).commit();
}
}
Step 3 Test your instantiated fragment
#Test
public void aTest() {
fragment.getView().findViewById(...);
}
If you're using robolectric, this is pretty straightforward using the FragmentUtilTest class:
#Test
public void aTest() {
// instantiate your fragment
MyFragment fragment = new MyFragment();
// Add it to a blank activity
FragmentTestUtil.startVisibleFragment(fragment);
// ... call getView().findViewById() on your fragment
}
Here is my working solution:
Create an instrumentation unit test class for this in androidTest directory, i.e.:
public class FragmentTest extends
ActivityInstrumentationTestCase2<MainActivity> {
private MainActivity testingActivity;
private TestFragment testFragment;
//...
}
call this constructor inside this new class:
public FragmentTest() {
super(MainActivity.class);
}
override the setUp() method (be sure to have R.id.fragmentContainer in your Activity class) where you will call at the end waitForIdleSync():
#Override
protected void setUp() throws Exception {
super.setUp();
// Starts the activity under test using
// the default Intent with:
// action = {#link Intent#ACTION_MAIN}
// flags = {#link Intent#FLAG_ACTIVITY_NEW_TASK}
// All other fields are null or empty.
testingActivity = getActivity();
testFragment = new TestFragment();
testingActivity.getFragmentManager().beginTransaction().add(R.id.fragmentContainer,testFragment,null).commit();
/**
* Synchronously wait for the application to be idle. Can not be called
* from the main application thread -- use {#link #start} to execute
* instrumentation in its own thread.
*
* Without waitForIdleSync(); our test would have nulls in fragment references.
*/
getInstrumentation().waitForIdleSync();
}
Write a test method, for example somethng like:
public void testGameFragmentsTextViews() {
String empty = "";
TextView textView = (TextView)testFragment.getView().findViewById(R.id.myTextView);
assertTrue("Empty stuff",(textView.getText().equals(empty)));
}
Run the test.
AndroidX provides a library, FragmentScenario, to create fragments and change their state.
app/build.gradle
dependencies {
def fragment_version = "1.0.0"
// ...
debugImplementation 'androidx.fragment:fragment-testing:$fragment_version'
}
example
#RunWith(AndroidJUnit4::class)
class MyTestSuite {
#Test fun testEventFragment() {
// The "fragmentArgs" and "factory" arguments are optional.
val fragmentArgs = Bundle().apply {
putInt("selectedListItem", 0)
}
val factory = MyFragmentFactory()
val scenario = launchFragmentInContainer<MyFragment>(
fragmentArgs, factory)
onView(withId(R.id.text)).check(matches(withText("Hello World!")))
}
}
More at official docs.
Use your main activity as the test activity that you send to ActivityInstrumentationTestCase2. Then you can work with the fragments through the fragment manager of your main activity that launches the fragments. This is even better than having a test activity because it uses the logic that you write in your main activity to test scenarios, which gives a fuller and more complete test.
Example:
public class YourFragmentTest extends ActivityInstrumentationTestCase2<MainActivity> {
public YourFragmentTest(){
super(MainActivity.class);
}
}
Right now ActivityInstrumentationTestCase2 is deprecated. Now you can use rules in order to use activities within your tests: http://wiebe-elsinga.com/blog/whats-new-in-android-testing/
In order for those to work you'll have to add the dependencies to you build.gradle:
testCompile 'com.android.support.test:rules:0.5'
testCompile 'com.android.support.test:runner:0.5'
(See Why cannot I import AndroidJUnit4 and ActivityTestRule into my unit test class?)

Mock objects in Android not passed as parameters

I am trying to test a Fragment I've created in Android. I have complete control of the code, so I can change it as I see fit. The issue is that I'm not sure what design pattern I'm missing to make it reasonable.
I am looking for a way to mock objects in Android that are not passed as parameters. This question suggests that anything you might want to mock should be written to be passed as a parameter.
This makes sense for some situations, but I can't figure out how to get it working on Android, where some of this isn't possible. With a Fragment, for example, you're forced to let much of the heavy lifting be done in callback methods. How can I get my mocked objects into the Fragment?
For example, in this ListFragment I need to retrieve an array of things to display to the user. The things I'm displaying need to be retrieved dynamically and added to a custom adapter. It currently looks as follows:
public class MyFragment extends ListFragment {
private List<ListItem> mList;
void setListValues(List<ListItem> values) {
this.mList = values;
}
List<ListItem> getListValues() {
return this.mList;
}
#Override
public void onCreateView(LayoutInflater i, ViewGroup vg, Bundle b) {
// blah blah blah
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
this.setListValues(ListFactory.getListOfDynamicValues());
CustomAdapter adapter = new CustomAdapter(
getActivity(),
R.layout.row_layout,
this.getListValues());
this.setListAdapter(adapter);
}
}
I'm trying to do this using Mockito and Robolectric.
This is the beginning of my robolectric test case:
public class MyFragmentTest {
private MyFragment fragment;
#Before
public void setup() {
ListItem item1 = mock(ListItem.class);
ListItem item2 = mock(ListItem.class);
when(item1.getValue()).thenReturn("known value 1");
when(item2.getValue()).thenReturn("known value 2");
List<ListItem> mockList = new ArrayList<ListItem>();
mockList.add(item1);
mockList.add(item2);
MyFragment real = new MyFragment();
this.fragment = spy(real);
when(this.fragment.getValueList()).thenReturn(mockList);
startFragment();
}
}
This feels so very wrong. This section from the mockito api points out that you shouldn't have to do partial mocks like this very frequently unless you're dealing with legacy code.
Further, I'm not actually able to mock out the CustomAdapter class using this approach.
What is the right way to do this sort of thing? Am I structuring things incorrectly in my Fragment classes? I suppose I might be able to add a bunch of package-private setters, but this still doesn't feel right.
Can someone shed some light on this? I'm happy to do rewrites, I just want to know some good patterns for dealing with the state in my Fragments and how I can make them testable.
I ended up creating my own solution to this. My approach was to add another level of indirection to each my calls that create or set an object.
First, let me point out that I couldn't actually get Mockito to work reliably with Fragment or Activity objects. It was somewhat hit or miss, but especially with trying to create Mockito Spy objects, some lifecycle methods appeared to not be called. I think this is related to gotcha number 2 shown here. Perhaps this is due to the ways that Android uses reflection to recreate and instantiate activities and fragments? Note that I was NOT incorrectly holding onto the reference, as it warns of, but interacting only with the Spy, as indicated.
So, I wasn't able to mock Android objects that required lifecycle methods be invoked by the framework.
My solution was to create to more types of methods in my Activity and Fragment methods. These methods are:
getters (getX()) that return the field named X.
retrievers (retrieveX()) that do some sort of work to get an object.
creators (createMyFragment()) that create objects by calling new. Similar to the retrievers.
Getters have whatever visibility you need. Mine are usually public or private.
Retrievers and creators are package private or protected, allowing you to override them in your test packages but not making them generally available. The idea behind these methods is that you can subclass your regular objects with stub objects and inject in known values during testing. You could also just mock out those methods if Mockito mocks/spies are working for you.
Taken in toto, the test would look something like the following.
Here is the fragment from my original question, modified to use the above approach. This is in the normal project:
package org.myexample.fragments
// imports
public class MyFragment extends ListFragment {
private List<ListItem> mList;
void setListValues(List<ListItem> values) {
this.mList = values;
}
List<ListItem> getListValues() {
return this.mList;
}
#Override
public void onCreateView(LayoutInflater i, ViewGroup vg, Bundle b) {
// blah blah blah
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
this.setListValues(this.retrieveListItems());
CustomAdapter adapter = this.createCustomAdapter();
this.setListAdapter(adapter);
}
List<ListItem> retrieveListItems() {
List<Item> result = ListFactory.getListOfDynamicValues();
return result;
}
CustomAdapter createCustomAdapter() {
CustomAdapter result = new CustomAdapter(
this.getActivity();
R.layout.row_layout,
this.getListValues());
return result;
}
}
When I test this object, I want to be able to control what gets passed around. My first thought was to use a Spy, replacing the return values of retrieveListItems() and createCustomAdapter() with my known values. However, like I said above, I wasn't able to get Mockito spies to behave when working with fragments. (Especially ListFragments--I had mixed success with other types, but don't trust it.) So, we are going to subclass this object. In the test project, I have the following. Note that your method visibility in your real class must allow subclasses to override, so it needs to be package private and in the same package or protected. Note that I am overriding the retriever and creator, returning instead static variables that my tests will set.
package org.myexample.fragments
// imports
public class MyFragmentStub extends MyFragment {
public static List<ListItem> LIST = null;
public static CustomAdapter ADAPTER = null;
/**
* Resets the state for the stub object. This should be called
* in the teardown methods of your test classes using this object.
*/
public static void resetState() {
LIST = null;
ADAPTER = null;
}
#Override
List<ListItem> retrieveListItems() {
return LIST_ITEMS;
}
#Override
CustomAdapter createCustomAdapter() {
return CUSTOM_ADAPTER;
}
}
In the same package in my test project I have the actual test of the fragment. Note that while I'm using Robolectric, this should work with whatever test framework you're using. The #Before annotation becomes less useful, as you need to update your static state for individual tests.
package org.myexample.fragments
// imports
#RunWith(RobolectricTestRunner.class)
public class MyFragmentTest {
public MyFragment fragment;
public Activity activity;
#After
public void after() {
// Very important to reset the state of the object under test,
// as otherwise your tests will affect each other.
MyFragmentStub.resetState();
}
private void setupState(List<ListItem> testList, CustomAdapter adapter) {
// Set the state you want the fragment to use.
MyFragmentStub.LIST = testList;
MyFragmentStub.ADAPTER = adapter;
MyFragmentStub stub = new MyFragmentStub();
// Start and attach the fragment using Robolectric.
// This method doesn't call visible() on the activity, though so
// you'll have to do that yourself.
FragmentTestUtil.startFragment(stub);
Robolectric.ActivityController.of(stub.getActivity()).visible();
this.fragment = stub;
this.activity = stub.getActivity();
}
#Test
public void dummyTestWithKnownValues() {
// This is a test that does nothing other than show you how to use
// the stub.
// Create whatever known values you want to test with.
List<ListItem> list = new ArrayList<ListItem>();
CustomAdapter adapter = mock(CustomAdapter.class);
this.setupState(list, adapter);
// android fest assertions
assertThat(this.fragment).isNotNull();
}
}
This is definitely more verbose than using a mocking framework. However, it works even with Android's life cycle. If I'm testing an Activity, I'll also often include a static boolean BUILD_FRAGMENTS variable. If true, I'll go call through to super in the appropriate methods or return a known fragment as appropriate. In this way I'm able to inject my test objects and play nice with the Android life cycle.

Categories

Resources