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("")));
}
}