我有一个想要测试的Fragment。我创建了一个测试活动,向其中添加此片段并运行一些 Espresso 测试。
但是,Espresso 在 Fragment 内找不到任何视图。它转储视图层次结构并且全部为空。
我不想将片段嵌入到测试活动中。我只想单独测试片段。有人这样做过吗?有类似代码的示例吗?
@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;
}
}
我会按照以下方式做 创建一个ViewAction如下:
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();
}
};
}
然后使用下面的代码来启动应该在 UI 线程中运行的代码
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
}
}));
下面是添加片段视图层次结构的测试用例示例
@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));
}
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;
}
}
您可以如上所述从 UI 线程启动片段。
您可以使用
androidx.fragment:fragment-testing
库。在测试方法中启动片段非常简单:
val fragmentArgs = Bundle()
androidx.fragment.app.testing.launchFragmentInContainer<MyFragment>(fragmentArgs)
您可以在测试您的片段Android开发人员指南中找到有关此库的更多信息。
您可以使用FragmentTestRule。
您必须使用:
而不是常规的
ActivityTestRule
@Rule
public FragmentTestRule<?, FragmentWithoutActivityDependency> fragmentTestRule =
FragmentTestRule.create(FragmentWithoutActivityDependency.class);
您可以在这篇博文中找到更多详细信息。
您可能忘记将片段注入视图层次结构中。尝试在
TestActivity
布局中为片段定义支架容器(例如 id 为 FrameLayout
的 fragment_container
),然后使用 add(myFragment, "tag")
(此方法)而不是仅使用
add(R.id.fragment_container, myFragment, "tag")
。我想您也可以使用具有相同签名的 replace
方法。
我不想将片段嵌入到测试活动中。我只想 单独测试 Fragment。有人这样做过吗?有没有 有类似代码的示例吗?
保持片段隔离是正确的方法。但不幸的是,如果没有宿主,片段就无法存在。正如 @Adil Hussain 在 上面 的回答,
androidx.fragment:fragment-testing
库提供了合适的工具来解决此问题。
androidx.fragment.app.testing.FragmentScenario
类包含一些重载的launchInContainer()
静态方法
它将片段附加到特殊的空活动的根视图控制器。此活动专为测试目的而设计,使您无需创建自己的测试活动、将其注册到应用程序的清单中等。
在我的项目中,我实现了支持类
FragmentScenarioRule.java
(如下所示),它将 org.junit.rules.ExternalResource
扩展为 JUnit4 测试规则(以防万一,此实现类似于 ActivityTestRule
中的 androidx.test:rules
类)图书馆)。
我不需要通过片段的构造函数注入任何依赖项,因此我只需使用默认构造函数来创建片段。但是您可以改进此实现以提供您自己的规则
FragmentFactory
。但我需要执行一些初始操作,因此我向此规则添加了拦截生命周期事件并执行适当回调的功能。
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);
}
}
};
}
}
用途:
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("")));
}
}