My Android App was built on Single Activity, multiple fragments based model.
I need to do unit testing for the app. I could write unit testcases for app which contains all activities using ActivityInstrumentationTestCase2 JUnit but not for app which contains fragments.
Please suggest the way to write JUnit testcases for fragments.
Thank you
See Android: Testing fragments
Copied for your reading pleasure with edits made for getFragmentManager() vs getSupportFragmentManager() and android:exported="false":
If you want to test a fragment in isolation, you need to create a Test FragmentActivity so your test can use that. The test activity will look something like this. Remember to declare it in your application’s manifest:
public class TestFragmentActivity extends FragmentActivity {
#Override
protected void onCreate(Bundle arg0) {
super.onCreate(arg0);
setContentView(R.layout.activity_fortests);
}
}
Layout:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:id="#+id/activity_test_fragment_linearlayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
/>
</RelativeLayout>
AndroidManifest:
...
<activity
android:name="your.package.name.TestFragmentActivity"
android:exported="false" />
...
Then in your test project, you can have a class like this to start the fragment:
public class FrameworkObjectsGeneratorFragmentTest
extends ActivityInstrumentationTestCase2<TestFragmentActivity> {
private TestFragmentActivity mActivity;
public FrameworkObjectsGeneratorFragmentTest() {
super(TestFragmentActivity.class);
}
#Override
protected void setUp() throws Exception {
super.setUp();
mActivity = getActivity();
}
private Fragment startFragment(Fragment fragment) {
FragmentTransaction transaction = mActivity.getFragmentManager().beginTransaction();
transaction.add(R.id.activity_test_fragment_linearlayout, fragment, "tag");
transaction.commit();
getInstrumentation().waitForIdleSync();
Fragment frag = mActivity.getFragmentManager().findFragmentByTag("tag");
return frag;
}
public void testFragment() {
FrameworkObjectsGeneratorFragment fragment = new FrameworkObjectsGeneratorFragment() {
//Override methods and add assertations here.
};
Fragment frag = startFragment(fragment);
}
}
The startFragment() method adds a fragment you specify to the ViewGroup in the TestActivity.
The good thing about testing fragments, as opposed to Activities, is that you can extends the Fragment to override protected fields and methods within which you can add assertions.
NOTE: Call getSupportFragmentManager() if you are using the support library.
Related
I am using FragmentTransaction.replace() to swap fragments in and out.
The app starts up first time with no problem.
An IllegalStateException is thrown when rotating the device because of a conflict between the savedInstanceState
and commiting a new fragment transaction.
No AsyncTask is involved.
One StackOverflow question suggests to put the setContentView() call
in onResumeFragments(), but this seems to have no effect. Same with onPostResume().
Another StackOverflow question says to override onConfigurationChanged(). This works in that sense that it the exception doesn't occur
because the Activity is not restarted. However, this prevents fragments that have different portrait
and landscape layouts from switching between these layouts. Calling setContentView() in onConfigurationChanged()
causes a similar error (IllegalArgumentException: Binary XML file line #25: Duplicate id 0x12345678, tag null, or parent id with another fragment)
Using fragmentTransaction.commitAllowingStateLoss() instead of .commit() causes IllegalStateException: Activity has been destroyed.
How do I get this to work?
More exception info:
java.lang.RuntimeException: Unable to start
activity ComponentInfo{myapp/myap.MainActivity}:
android.view.InflateException: Binary XML file line #25: Error
inflating class fragment at
myapp.MainActivity.onResumeFragments(MainActivity.java:450)
Caused by: java.lang.IllegalStateException: Can not perform this action after
onSaveInstanceState at > android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1533)
at
myapp.fragments.FragmentChange.onFragmentChange(FragmentChange.java:128)
at
myapp.MainActivity.onNavigationDrawerItemSelected(MainActivity.java:490)
at
myapp.fragments.NavigationDrawerFragment.selectItem(NavigationDrawerFragment.java:197)
at
myapp.fragments.NavigationDrawerFragment.onCreate(NavigationDrawerFragment.java:78)
at myapp.MainActivity.onResumeFragments(MainActivity.java:450)
The sequence in the code upon rotating the device is:
MainActivity.onPause()
MainActivity.saveInstanceState()
NavigationDrawerFragment.onSaveInstanceState()
MainActivity.onStop()
MainActivity.onDestroy()
MainActivity.onCreate()
super.onCreate(savedInstanceState);
MainActivity.onResumeFragments()
setContentView()
NavigationDrawerFragment.onCreate()
MainActivity.onNavigationDrawerItemSelected()
fragmentTransaction.commit();
MainActivity:
public class MainActivity extends AppCompatActivity implements
NavigationDrawerFragment.NavigationDrawerCallbacks {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
}
#Override
public void onNavigationDrawerItemSelected(int position) {
...
FragmentChangeEvent fragmentChangeEvent = new FragmentChangeEvent(null);
FragmentChange fragmentChange = FragmentChange.getInstance( getSupportFragmentManager());
fragmentChange.onFragmentChange(fragmentChangeEvent);
...
}
#Override
public void onSaveInstanceState(Bundle outState) {
outState.putString("WORKAROUND_FOR_BUG_19917_KEY", "WORKAROUND_FOR_BUG_19917_VALUE");
super.onSaveInstanceState(outState);
}
#Override
public void onResumeFragments() {
super.onResumeFragments();
// causes onNavigationDrawerItemSelected() to be called, exception thrown
setContentView(myapp.R.layout.activity_main);
mNavigationDrawerFragment = (NavigationDrawerFragment)
getSupportFragmentManager().findFragmentById(myapp.R.id.navigation_drawer);
mNavigationDrawerFragment.setUp( // Set up the drawer
myapp.R.id.navigation_drawer,
(DrawerLayout) findViewById(myapp.R.id.drawer_layout));
}
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if current fragment is "Individual" { // pseudocode
setContentView(R.layout.activity_main); // causes IllegalArgumentException
}
}
}
NavigationDrawerFragment
public class NavigationDrawerFragment extends Fragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
mUserLearnedDrawer = sp.getBoolean(PREF_USER_LEARNED_DRAWER, false);
if (savedInstanceState != null) {
mCurrentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION);
mFromSavedInstanceState = true;
}
// Select either the default item (0) or the last selected item.
selectItem(mCurrentSelectedPosition);
}
private void selectItem(int position) {
mCurrentSelectedPosition = position;
if (mDrawerListView != null) {
mDrawerListView.setItemChecked(position, true);
}
if (mDrawerLayout != null) {
mDrawerLayout.closeDrawer(mFragmentContainerView);
}
if (mCallbacks != null) {
// calls MainActivity.onNavigationDrawerItemSelected()
mCallbacks.onNavigationDrawerItemSelected(position);
}
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(STATE_SELECTED_POSITION, mCurrentSelectedPosition);
}
}
FragmentChange
public class FragmentChange implements FragmentChangeListener {
public static FragmentChange getInstance(FragmentManager fragmentManager) {
if (instance == null) {
instance = new FragmentChange(fragmentManager);
}
return instance;
}
// constructor
private FragmentChange(FragmentManager fragmentManager) {
mFragmentManager = fragmentManager;
}
#Override
public void onFragmentChange(FragmentChangeEvent fragmentChangeEvent) {
...
mPosition = fragmentChangeEvent.getPosition();
FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
Fragment fragment = EmployeesVerticalFragment.newInstance();
fragmentTransaction.replace(myapp.R.id.container, fragment);
fragmentTransaction.commit(); // IllegalState exception here
...
}
}
A greatly reduced form of the project on github which reproduces the IllegalStateException:
Some comments:I can see you have gone "above and beyond" trying to fix it (and have scanned stackoverflow for many tips).
Your code works the first time (even if you are in landscape b4 starting app).
The rotation fails to inflate the fragment (second time around) (in activity_main), and also reports the "commit after save" error (that's weird,how on Earth are we getting 2 fatal errors ? maybe on another thread, or ""save" is killing the inflator, but how can it carry on?).
NavigationDrawerFragment has a LOT of important code in it (that normally goes in the Activity).
Analysis:
Your MainActivity class extends AppCompatActivity:
public class MainActivity extends AppCompatActivity implements
NavigationDrawerFragment.NavigationDrawerCallbacks {
Normally you extend FragmentActivity:
public class MainActivity extends FragmentActivity implements
NavigationDrawerFragment.NavigationDrawerCallbacks {
This is because you inflate activity_main, which contains a fragment.
setContentView(com.example.replacefragments.R.layout.activity_main);
Inheritance:
Activity <- FragmentActivity <- AppCompatActivity <- ActionBarActivity
'<-' means inheritance here.
One reason why you would need to consider FragmentActivity specifically is if you want to use nested fragments (a fragment holding another fragment), as that was not supported in native fragments until API Level 17.
I think this is what you are doing here (in activity_main):
<fragment android:id="#+id/navigation_drawer
Here is a good template app that should make your life easier:
android-sliding-menu-using-navigation-drawer/
FIXING IT:
Replace the fragment in activity_main.xml with the ListView.
I have discarded NavigationDrawerFragment, and injected some code from the template above, then re-introduced bit-by-bit your code from NavigationDrawerFragment, until it falls over.
It now works with rotation, but not as you or I would like (preserving fragment state, NavigationDrawerFragment some functionality), so I'm still working on it.
protraitlandscape after rotation
Hot tip:
The .commit sounds final does it not ? Sounds as if it should synchronous? Neither are true.
The .commit puts the transaction in a pending queue, to truly execute it you need:
getSupportFragmentManager().executePendingTransactions();
Here's a link to the error log of the posted code on rotation:
link error log
Method 1. Avoid config changes. Add android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard" in you manifest activity.
<activity
android:name=".ui.accounts.YouActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard"
android:theme="#style/AppTheme.NoActionBar" />
Method 2.
a. First of all do not miss place code from default places like setContentView() in onConfigurationChanged().
b. Replace fragment with tag or by id in xml
c. Don't replace again if fragment found by id or tag.
d. Make sure you are dismissing any dailog inside fragment onPause.
The FragmentManager is an
Interface for interacting with Fragment objects inside of an Activity.
It strikes me as a particular bad idea to have save it in a static field and reuse and old FragmentManager for a new activity. This will necessarily lead to Activity has been destroyed, when the new activity interact with the manager from the old activity.
In your code, replace
FragmentChange.getInstance(getFragmentManager());
by
new FragmentChange(getFragmentManager());
So i have a FragmentPagerAdapater called SectionsPagerAdapter and a fragment called TeamFragment where I display data from a specific team. So basically I don't want to create different fragments for each team. That is an overkill. I just want 1 fragment which basically connects to the backend then collects the data based on the team then displays that data. But I dont know how to pass the Team name(a string type) from SectionsPagerAdapter to the TeamFragment so that in TeamFragment, I can easily know what to retrieve from the backend. My backend in parse.com. Please help me figure this out and learn. Thanks
So this is was solved my problem. In my sectionsPagerAdapter class i had the below code
Bundle args = new Bundle();
args.putString("TeamName", team);
TeamFragment teamFragment = new TeamFragment();
teamFragment.setArguments(args);
In onCreateView of my TeamFragment, i had the following
Bundle bundle = this.getArguments();
mTeam = bundle.getString("TeamName");
hope this can help someone else. Thanks
Communicating data into fragments is typically done through a simple setter function that is called by the activity that instantiates or contains the fragment:
public class MyActivity extends FragmentActivity {
#Override
protected void onCreate(Bundled savedInstanceState) {
// ...
TeamFragment fragment =
(TeamFragment) (getSupportFragmentManager().findFragmentById(fragmentId));
fragment.setTeamName(teamName);
// ...
}
For communicating data back to the activity, is typically done using a fragment-specific "Listener" interface. This listener can be attached using the same method (by calling a method on the fragment in the parent activity to register the listener) or it can be done by requiring that the parent Activity implement the listener interface, and casting the parent activity to this listener interface in onAttach() (though the latter approach is not as clean of an approach). Example:
public class MyActivity extends FragmentActivity {
#Override
protected void onCreate(Bundled savedInstanceState) {
// ...
TeamFragment fragment =
(TeamFragment) (getSupportFragmentManager().findFragmentById(fragmentId));
fragment.setTeamName(teamName);
fragment.setTeamSelectedListener(new TeamSelectedListenerImpl());
// ...
}
Or:
public class TeamFragment extends Fragment {
public interface TeamSelectedListener {
// ...
}
// ...
#Override
protected void onAttach(Activity activity) {
teamSelectedListener = (TeamSelectedListener) activity;
}
// ...
}
public class MyActivity
extends FragmentActivity
implements TeamFragment.TeamSelectedListener {
// ...
}
Hello my Android application is using fragments. I am wondering if there is a way I can use the idea of the onBackPressed() method in my app with fragments. I have previous and next buttons, but as of now I am just creating new fragments and replacing, and none of the data gets saved. Is there a way to save my data/go back once I have gone forward?
The concept of Fragment is different of Activity.
One Activity could have a many Fragments, read that:
A Fragment represents a behavior or a portion of user interface in an
Activity. You can combine multiple fragments in a single activity to
build a multi-pane UI and reuse a fragment in multiple activities. You
can think of a fragment as a modular section of an activity, which has
its own lifecycle, receives its own input events, and which you can
add or remove while the activity is running (sort of like a "sub
activity" that you can reuse in different activities).
See more here: http://developer.android.com/guide/components/fragments.html
SOLUCTION
So if you wanna handle the onBackPressed behavior in you Fragment you could do that:
package com.example.stackoverflowsandbox;
import android.app.Activity;
import android.app.Fragment;
public class MainActivity extends Activity {
public class MyFragment extends Fragment {
public void onBackPressed() {
// your code here...
}
}
private MyFragment myFragment;
#Override
public void onBackPressed() {
// super.onBackPressed(); // comment to not back
this.myFragment.onBackPressed(); // the onBackPressed method of Fragment is a custom method
}
}
Sorry, do not know if I understand your question, but if the idea and have control of direct backbutton in its fragment, and from it to perform some task of data persistence, you can add your fragment to control stack FragmentManager, the as follows.
FragmentManager fm = getSupportFragmentManager();
MyFragment mMyFragment = new MyFragment();
fm.beginTransaction()
.add(mMyFragment, "mMyFragment")
.addToBackStack( null )
.commit();
In the fragment you need to implement the interface OnBackStackChangedListener
In Fragment:
public class MyFragment extends Fragment implements OnBackStackChangedListener {
#Override
public void onBackStackChanged() {
//your code here
}
}
If you just keep the values
public class MyFragment extends Fragment {
String valeu;
#Override
public void onCreate( final Bundle savedInstanceState ) {
super.onCreate( savedInstanceState );
if ( savedInstanceState != null ) {
this.valeu = savedInstanceState.getString( "key" );
}
}
#Override
public void onSaveInstanceState( final Bundle outState ) {
super.onSaveInstanceState( outState );
outState.putString( "key", "Your content" );
}
}
while moving to front fragment, save the state of previous fragment using onSaveInstanceState()
while moving back restore the state in onCreate() or onCreateView() in the previous fragment
as the title says, I'm trying to figure out which one is the best way to inject a dependency in a Fragment.
I want to be independent from external frameworks like RoboGuice etc.
Now, in the simplest way possible, I have an interface that abstracts some kind of logic, and, from an Activity, I want to inject an implementation of this interface. I know that I have to provide a default constructor for my fragment, since the system might need to recreate the fragment at some point, and that the usual way to create a new instance of the fragment is to provide static method that handles the creation like this:
public static Fragment newInstance() {
final Bundle bundle = new Bundle();
...
final Fragment fragment = new MyFragment();
fragment.setArguments(bundle);
return fragment;
}
How can I pass my dependency to the fragment? Should I make it implement the Parcelable or Serializable interfaces and then pack it in the Bundle? Is there some other way to achieve the result?
A simple solution is to declare an interface which is declaring the Dependencies required for the Fragment. Then let the Context of the Fragment implement this interface, and poll the dependencies when needed from the Context.
Contract:
public interface MyDependencies {
MyDep getDep();
}
Activity:
public MyActivity extends Activity implements MyDependencies {
#Override
public MyDep getDep(){
return createMyDependencyOrGetItFromASingletonOrGetItFromApplication()
}
}
Fragment:
public void onActivityCreated(Bundle b){
super.onActivityCreated(b)
if (getActivity() instanceOf MyDependencies) {
MyDep dep = ((MyDependencies) getActivity).getDep();
} else {
throw new RuntimeException("Context does not support the Fragment, implement MyDependencies")
}
}
So, in fact, there is no unnecessary coupling to the Activity because the contract is defined by an interface.
Why don't you grab the dependency from your activity?
public void onActivityCreated( Bundle b){
super.onActivityCreated(b)
DependencyClass c = ((MyActivity)getActivity()).getDependency();
}
If you can't pass in the dependency via the constructor (if you need a default constructor), and you don't want to use a dependency injection lib like Dagger or RoboGuice, the other classic way is to a setter to "inject" the dependency.
Fragment MyFragment {
Depend mDepend;
....
public void setDepend(Depend depend) {
mDepend = depend;
}
}
Then in your activity you can inject the dependency in the onCreate method.
so something like this in your activity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_map);
MapFragment wrapperFragment = new WrapperFragment();
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
.add(R.id.map_container, wrapperFragment).commit();
// find the fragment
// call the setter
}
}
I have Activity that contains few Fragments, now I would like to test one of this Fragment but I would like to separate test and test only core functionality of selected Fragment not bothering what is happening in main Activity.
My idea is to create a mock Activity which will just add Fragment in onCreate() method. Then I will make some tests. But I would not like to include mock Activity to my main project, I would rather include it to test project. So I did something like this:
I have created MockActivity:
public final class ActivityMock extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentTransaction t = getFragmentManager().beginTransaction();
MyFragment f = new MyFragment();
t.add(f, "MY_FRAGMENT");
t.commit();
}
}
I want to test it like this:
public final class MyFragmentTest extends
ActivityInstrumentationTestCase2<ActivityMock> {
public MyFragmentTest() {
super(ActivityMock.class);
}
public void testSomething() {
ActivityMock mActivity = getActivity();
//do some assertions
}
}
The problem is that I I get error:
java.lang.RuntimeException: Unable to resolve activity for: Intent {
act=android.intent.action.MAIN flg=0x10000000
cmp=com.example/.test.ActivityMock }
Ok next I tried to modify test project AndroidManifest.xml
<activity android:name=".ActivityMock" />
But I got same error. I think that is because anyway during test run main project is searched for ActivityMock. So I tried to add
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.example.test" />
I don know if it is a good idea, but main thought is that test project will be able to test (instrument) itself. But now I get:
junit.framework.AssertionFailedError: Exception in constructor: testSomething
(java.lang.NoClassDefFoundError: com.example.test.ActivityMock
So I think that modified AndroidManifest.xml worked but still ActivityMock class is being searched in main project, though it is in test project.
I assume that getActivity() method always look for activity class in main project.
Does anybody tried to test Fragment this way and was able to create Activity mock?
Cheers