How do I test a Fragment in isolation using Espresso?
Asked Answered
L

6

21

I have a Fragment that I want to test. I created a test Activity to which I add this Fragment and run some Espresso tests.

However, Espresso does not find any of the views inside the Fragment. It dumps the view hierarchy and it is all empty.

I do not want to embed the Fragment in a test Activity. I want to just test the Fragment in isolation. Has anyone done this? Is there a sample that has similar code?

@RunWith(AndroidJUnit4.class)
class MyFragmentTest {

    @Rule
    public ActivityTestRule activityRule = new ActivityTestRule<>(
        TestActivity.class
    );

    @Test
    public void testView() {
        MyFragment myFragment = startMyFragment();
        myFragment.onEvent(new MyEvent());
        // MyFragment has a RecyclerView
        // onEvent is an EventBus callback that contains no data in this test
        // I want the Fragment to display an empty list text and hide the RecyclerView

        onView(withId(R.id.my_empty_text)).check(matches(isDisplayed()));
        onView(withId(R.id.my_recycler)).check(doesNotExist()));
    }

    private MyFragment startMyFragment() {
        FragmentActivity activity = (FragmentActivity) activityRule.getActivity();
        FragmentTransaction transaction = activity.getSupportFragmentManager().beginTransaction();
        MyFragment myFragment = new MyFragment();
        transaction.add(myFragment, "myfrag");
        transaction.commit();
        return myFragment;
    }
}
Lemures answered 18/2, 2016 at 2:2 Comment(5)
Did you try doing any web searches for this? I find lots of examples.Standish
It's always a good idea to show us your code.Mali
I tried to search on the net. I have not been able to find any examples. @DougStevenson, I would appreciate it if you could share what you found.Lemures
Because of the proprietary nature of the app, I am not allowed to freely share the code. But I will try to create a sample.Lemures
I did that. However, all the samples that I find use the ACTUAL parent activity to test a fragment. None of the samples isolate the fragment and test it stand-alone.Lemures
A
7

I will do in following way Create a ViewAction as follows:

public static ViewAction doTaskInUIThread(final Runnable r) {
    return new ViewAction() {
        @Override
        public Matcher<View> getConstraints() {
            return isRoot();
        }

        @Override
        public String getDescription() {
            return null;
        }

        @Override
        public void perform(UiController uiController, View view) {
            r.run();
        }
    };
}

Then use below to launch code which should be run in UI Thread

onView(isRoot()).perform(doTaskInUIThread(new Runnable() {
        @Override
        public void run() {
            //Code to add your fragment or anytask that you want to do from UI Thread
        }
    }));

below is an example of test case adding fragment view hierarchy

    @Test
public void testSelectionOfTagsAndOpenOtherPage() throws Exception{

    Runnable r = new Runnable() {
        @Override
        public void run() {
            //Task that need to be done in UI Thread (below I am adding a fragment)

        }
    };
    onView(isRoot()).perform(doTaskInUIThread(r));

}
Alcalde answered 23/6, 2016 at 11:27 Comment(0)
S
6
public class VoiceFullScreenTest {
    @Rule
    public ActivityTestRule activityRule = new ActivityTestRule<>(
            TestActivity.class);

    @Test
    public void fragment_can_be_instantiated() {
        activityRule.getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                VoiceFragment voiceFragment = startVoiceFragment();
            }
        });
        // Then use Espresso to test the Fragment
        onView(withId(R.id.iv_record_image)).check(matches(isDisplayed()));
    }

    private VoiceFragment startVoiceFragment() {
        TestActivity activity = (TestActivity) activityRule.getActivity();
        FragmentTransaction transaction = activity.getSupportFragmentManager().beginTransaction();
        VoiceFragment voiceFragment = new VoiceFragment();
        transaction.add(voiceFragment, "voiceFragment");
        transaction.commit();
        return voiceFragment;
    }


}

You can start your fragment from UI thread as mentioned above.

Semiotics answered 16/5, 2017 at 9:38 Comment(0)
C
6

You can use the androidx.fragment:fragment-testing library. Launching the fragment in your test method is as simple as:

val fragmentArgs = Bundle()
androidx.fragment.app.testing.launchFragmentInContainer<MyFragment>(fragmentArgs)

You can find more information about this library in the Test your fragments Android Developers' guide.

Compellation answered 30/4, 2019 at 12:8 Comment(0)
F
4

You can use FragmentTestRule.

Instead of the regular ActivityTestRule you must use:

@Rule
public FragmentTestRule<?, FragmentWithoutActivityDependency> fragmentTestRule =
    FragmentTestRule.create(FragmentWithoutActivityDependency.class);

You can find more details in this blog post.

Fantinlatour answered 16/8, 2017 at 8:25 Comment(0)
R
1

You've probably forgot to inject the fragment in the view hierarchy. Try defining the holder container for your fragment in the TestActivity layout (like a FrameLayout with id fragment_container) and then instead of just add(myFragment, "tag"), use the add(R.id.fragment_container, myFragment, "tag") (this method). I guess you could use the replace method with the same signature as well.

Rainmaker answered 18/2, 2016 at 8:39 Comment(1)
I tried this method (using add(id, fragment, tag)) but that did not help. I suspect that this is because of multiple threads interacting. Espresso somehow does not like this.Lemures
A
0

I do not want to embed the Fragment in a test Activity. I want to just test the Fragment in isolation. Has anyone done this? Is there a sample that has similar code?

Staying the fragment isolated is the right approach. But unfortunately, the fragment can't exist without its host. As @Adil Hussain answered above, the androidx.fragment:fragment-testing library provides suitable tools for resolving this issue.

For testing a fragment's user interface, the androidx.fragment.app.testing.FragmentScenario class contains a few overloaded launchInContainer() static methods that attaches the fragment to a special empty activity's root view controller. This activity is designed especially for testing purpose which saves you from creating your own test activity, registering it in the app's manifest, etc.

In my project, I implemented supporting class FragmentScenarioRule.java (as below) that extends org.junit.rules.ExternalResource as a JUnit4 test rule (just in case, this implementation is an analogue of the ActivityTestRule class from the androidx.test:rules library).

I didn't need to inject any dependencies via fragment's constructor, so I just use default constructor to create my fragments. But you can evolve this implementation to provide the rule your own FragmentFactory. But I needed to do some initial actions, so I added to this rule the ability to intercept lifecycle events and perform appropriate callbacks.

FragmentScenarioRule.java:

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import androidx.core.util.Supplier;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentFactory;
import androidx.fragment.app.testing.FragmentScenario;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;

import org.junit.rules.ExternalResource;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public final class FragmentScenarioRule<T extends Fragment> extends ExternalResource {
    @NonNull
    private final List<LifecycleEventObserver> mLifecycleObservers = new ArrayList<>();
    @NonNull
    private final Supplier<FragmentScenario<T>> mScenarioSupplier;
    @Nullable
    private FragmentScenario<T> mScenario;

    public FragmentScenarioRule(@NonNull final Class<T> fragmentClass) {
        mScenarioSupplier = () -> FragmentScenario.launchInContainer(
                fragmentClass,
                null,
                getFragmentFactory(fragmentClass)
        );
    }

    @NonNull
    public FragmentScenarioRule<T> registerCallback(@NonNull final Lifecycle.Event event,
                                                    @NonNull final Consumer<? super T> callback) {
        mLifecycleObservers.add((lifecycleOwner, lifecycleEvent) -> {
            if (lifecycleEvent == event) {
                //noinspection unchecked
                callback.accept((T) lifecycleOwner);
            }
        });

        return this;
    }

    @Override
    protected void before() {
        mScenario = mScenarioSupplier.get();
    }

    @Override
    protected void after() {
        Objects.requireNonNull(mScenario).close();
    }

    @NonNull
    public FragmentScenario<T> getScenario() {
        return Objects.requireNonNull(mScenario);
    }

    @NonNull
    private FragmentFactory getFragmentFactory(@NonNull final Class<T> fragmentClass) {
        return new FragmentFactory() {
            @NonNull
            @Override
            public Fragment instantiate(@NonNull final ClassLoader classLoader,
                                        @NonNull final String className) {
                Class<? extends Fragment> requeredClass = loadFragmentClass(classLoader, className);

                if (requeredClass != fragmentClass) {
                    return super.instantiate(classLoader, className);
                }

                try {

                    Fragment fragment = fragmentClass.getDeclaredConstructor().newInstance();

                    for (LifecycleEventObserver observer : mLifecycleObservers) {
                        fragment.getLifecycle().addObserver(observer);
                    }

                    return fragment;

                } catch (ReflectiveOperationException cause) {
                    throw new RuntimeException(cause);
                }
            }
        };
    }
}

Usage:

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.junit.Assert.assertEquals;

import android.widget.TextView;

import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.testing.FragmentScenario;
import androidx.lifecycle.Lifecycle;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Objects;

import you.packagename.R;

@RunWith(AndroidJUnit4.class)
public class MyFragmentTest {
    @Rule
    public FragmentScenarioRule<MyFragment> init = new FragmentScenarioRule<>(MyFragment.class)
            .registerCallback(
                    Lifecycle.Event.ON_CREATE, // or another available event
                    fragment -> {
                        // do something you needed with the fragment instance here

                        // or

                        FragmentActivity activity = fragment.requireActivity();
                        // do something you needed with the fragment's host activity here
                    }
            );

    @Test
    public void testMyFragment() {
        FragmentScenario<MyFragment> scenario = init.getScenario();

        // any time you have access to the fragment instance in the following way
        scenario.onFragment(fragment -> {
            // call any methods of your fragment instance or/and test its state here
            fragment.onEvent(new MyEvent());
        });

        // also you have access to the fragment's views
        onView(withId(R.id.my_empty_text)).check(matches(isDisplayed()));
        onView(withId(R.id.my_recycler)).check(doesNotExist());

        // you can even move your fragment instance to the other state
        scenario.moveToState(Lifecycle.State.STARTED);
        // or recreate it
        scenario.recreate();

        // and check something after it too
        onView(withId(R.id.my_empty_text)).check(matches(withText("")));
    }
}
Assailant answered 19/3, 2024 at 13:18 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.