现在我通过混合一些示例发现如何交换 Activity 范围的组件和 Fragment 范围的组件。在这篇文章中,我将向您展示如何做到这两点。但我将更详细地描述如何在 InstrumentationTest 期间交换 Fragment 范围的组件。我的总代码托管在github https://github.com/unlimited101/SwappingTestDoubles。您可以运行MainFragmentTest
类但要注意你必须设置de.xappo.presenterinjection.runner.AndroidApplicationJUnitRunner
作为 Android Studio 中的 TestRunner。
现在我描述很快,如何用假交互器交换交互器。在这个例子中我试图尊重干净的架构 https://github.com/android10/Android-CleanArchitecture越多越好。但它们可能是一些小事情,稍微破坏了这个架构。所以请随意改进。
那么,让我们开始吧。首先你需要一个自己的 JUnitRunner:
/**
* Own JUnit runner for intercepting the ActivityComponent injection and swapping the
* ActivityComponent with the TestActivityComponent
*/
public class AndroidApplicationJUnitRunner extends AndroidJUnitRunner {
@Override
public Application newApplication(ClassLoader classLoader, String className, Context context)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return super.newApplication(classLoader, TestAndroidApplication.class.getName(), context);
}
@Override
public Activity newActivity(ClassLoader classLoader, String className, Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
Activity activity = super.newActivity(classLoader, className, intent);
return swapActivityGraph(activity);
}
@SuppressWarnings("unchecked")
private Activity swapActivityGraph(Activity activity) {
if (!(activity instanceof HasComponent) || !TestActivityComponentHolder.hasComponentCreator()) {
return activity;
}
((HasComponent<ActivityComponent>) activity).
setComponent(TestActivityComponentHolder.getComponent(activity));
return activity;
}
}
In swapActivityGraph()
在运行测试时创建活动之前(!),我为活动创建了一个替代 TestActivityGraph。然后我们必须创建一个TestFragmentComponent
:
@PerFragment
@Component(modules = TestFragmentModule.class)
public interface TestFragmentComponent extends FragmentComponent{
void inject(MainActivityTest mainActivityTest);
void inject(MainFragmentTest mainFragmentTest);
}
该组件位于 Fragment 范围内。它有一个模块:
@Module
public class TestFragmentModule {
@Provides
@PerFragment
MainInteractor provideMainInteractor () {
return new FakeMainInteractor();
}
}
原本的FragmentModule
看起来像这样:
@Module
public class FragmentModule {
@Provides
@PerFragment
MainInteractor provideMainInteractor () {
return new MainInteractor();
}
}
你看我用的是MainInteractor
and a FakeMainInteractor
。他们看起来都是这样的:
public class MainInteractor {
private static final String TAG = "MainInteractor";
public MainInteractor() {
Log.i(TAG, "constructor");
}
public Person createPerson(final String name) {
return new Person(name);
}
}
public class FakeMainInteractor extends MainInteractor {
private static final String TAG = "FakeMainInteractor";
public FakeMainInteractor() {
Log.i(TAG, "constructor");
}
public Person createPerson(final String name) {
return new Person("Fake Person");
}
}
现在我们使用自定义的FragmentTestRule
用于测试独立于生产中包含该片段的活动的片段:
public class FragmentTestRule<F extends Fragment> extends ActivityTestRule<TestActivity> {
private static final String TAG = "FragmentTestRule";
private final Class<F> mFragmentClass;
private F mFragment;
public FragmentTestRule(final Class<F> fragmentClass) {
super(TestActivity.class, true, false);
mFragmentClass = fragmentClass;
}
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
try {
mFragment = mFragmentClass.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
protected void afterActivityLaunched() {
super.afterActivityLaunched();
//Instantiate and insert the fragment into the container layout
FragmentManager manager = getActivity().getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(R.id.fragmentContainer, mFragment);
transaction.commit();
}
public F getFragment() {
return mFragment;
}
}
That TestActivity
很简单:
public class TestActivity extends BaseActivity implements
HasComponent<ActivityComponent> {
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FrameLayout frameLayout = new FrameLayout(this);
frameLayout.setId(R.id.fragmentContainer);
setContentView(frameLayout);
}
}
但现在如何更换组件呢?有几个小技巧可以实现这一点。首先我们需要一个holder类来保存TestFragmentComponent
:
/**
* Because neither the Activity nor the ActivityTest can hold the TestActivityComponent (due to
* runtime order problems we need to hold it statically
**/
public class TestFragmentComponentHolder {
private static TestFragmentComponent sComponent;
private static ComponentCreator sCreator;
public interface ComponentCreator {
TestFragmentComponent createComponent(Fragment fragment);
}
/**
* Configures an ComponentCreator that is used to create an activity graph. Call that in @Before.
*
* @param creator The creator
*/
public static void setCreator(ComponentCreator creator) {
sCreator = creator;
}
/**
* Releases the static instances of our creator and graph. Call that in @After.
*/
public static void release() {
sCreator = null;
sComponent = null;
}
/**
* Returns the {@link TestFragmentComponent} or creates a new one using the registered {@link
* ComponentCreator}
*
* @throws IllegalStateException if no creator has been registered before
*/
@NonNull
public static TestFragmentComponent getComponent(Fragment fragment) {
if (sComponent == null) {
checkRegistered(sCreator != null, "no creator registered");
sComponent = sCreator.createComponent(fragment);
}
return sComponent;
}
/**
* Returns true if a custom activity component creator was configured for the current test run,
* false otherwise
*/
public static boolean hasComponentCreator() {
return sCreator != null;
}
/**
* Returns a previously instantiated {@link TestFragmentComponent}.
*
* @throws IllegalStateException if none has been instantiated
*/
@NonNull
public static TestFragmentComponent getComponent() {
checkRegistered(sComponent != null, "no component created");
return sComponent;
}
}
第二个技巧是使用holder来注册组件before甚至创建了片段。然后我们启动TestActivity
跟我们FragmentTestRule
。现在是第三个技巧,它与时间相关并且并不总是正确运行。Directly启动活动后我们得到Fragment
例如通过询问FragmentTestRule
。然后我们交换组件,使用TestFragmentComponentHolder
并注入片段图。第四个技巧是我们只需等待大约 2 秒即可创建 Fragment。在 Fragment 中我们进行组件注入onViewCreated()
。因为那时我们不会过早注入组件,因为onCreate()
and onCreateView()
之前被调用过。所以这是我们的MainFragment
:
public class MainFragment extends BaseFragment implements MainView {
private static final String TAG = "MainFragment";
@Inject
MainPresenter mainPresenter;
private View view;
// TODO: Rename and change types and number of parameters
public static MainFragment newInstance() {
MainFragment fragment = new MainFragment();
return fragment;
}
public MainFragment() {
// Required empty public constructor
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//((MainActivity)getActivity()).getComponent().inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
view = inflater.inflate(R.layout.fragment_main, container, false);
return view;
}
public void onClick(final String s) {
mainPresenter.onClick(s);
}
@Override
public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
getComponent().inject(this);
final EditText editText = (EditText) view.findViewById(R.id.edittext);
Button button = (Button) view.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
MainFragment.this.onClick(editText.getText().toString());
}
});
mainPresenter.attachView(this);
}
@Override
public void updatePerson(final Person person) {
TextView textView = (TextView) view.findViewById(R.id.textview_greeting);
textView.setText("Hello " + person.getName());
}
@Override
public void onDestroy() {
super.onDestroy();
mainPresenter.detachView();
}
public interface OnFragmentInteractionListener {
void onFragmentInteraction(Uri uri);
}
}
我之前描述的所有步骤(第二个到第四个技巧)都可以在@Before
带注释的setUp()
-方法在此MainFragmentTest
class:
public class MainFragmentTest implements
InjectsComponent<TestFragmentComponent>, TestFragmentComponentHolder.ComponentCreator {
private static final String TAG = "MainFragmentTest";
@Rule
public FragmentTestRule<MainFragment> mFragmentTestRule = new FragmentTestRule<>(MainFragment.class);
public AndroidApplication getApp() {
return (AndroidApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
}
@Before
public void setUp() throws Exception {
TestFragmentComponentHolder.setCreator(this);
mFragmentTestRule.launchActivity(null);
MainFragment fragment = mFragmentTestRule.getFragment();
if (!(fragment instanceof HasComponent) || !TestFragmentComponentHolder.hasComponentCreator()) {
return;
} else {
((HasComponent<FragmentComponent>) fragment).
setComponent(TestFragmentComponentHolder.getComponent(fragment));
injectFragmentGraph();
waitForFragment(R.id.fragmentContainer, 2000);
}
}
@After
public void tearDown() throws Exception {
TestFragmentComponentHolder.release();
mFragmentTestRule = null;
}
@SuppressWarnings("unchecked")
private void injectFragmentGraph() {
((InjectsComponent<TestFragmentComponent>) this).injectComponent(TestFragmentComponentHolder.getComponent());
}
protected Fragment waitForFragment(@IdRes int id, int timeout) {
long endTime = SystemClock.uptimeMillis() + timeout;
while (SystemClock.uptimeMillis() <= endTime) {
Fragment fragment = mFragmentTestRule.getActivity().getSupportFragmentManager().findFragmentById(id);
if (fragment != null) {
return fragment;
}
}
return null;
}
@Override
public TestFragmentComponent createComponent(final Fragment fragment) {
return DaggerTestFragmentComponent.builder()
.testFragmentModule(new TestFragmentModule())
.build();
}
@Test
public void testOnClick_Fake() throws Exception {
onView(withId(R.id.edittext)).perform(typeText("John"));
onView(withId(R.id.button)).perform(click());
onView(withId(R.id.textview_greeting)).check(matches(withText(containsString("Hello Fake"))));
}
@Test
public void testOnClick_Real() throws Exception {
onView(withId(R.id.edittext)).perform(typeText("John"));
onView(withId(R.id.button)).perform(click());
onView(withId(R.id.textview_greeting)).check(matches(withText(containsString("Hello John"))));
}
@Override
public void injectComponent(final TestFragmentComponent component) {
component.inject(this);
}
}
除了时间问题。此测试在我的环境中运行,在 API 级别为 23 的模拟 Android 上进行了 10 次测试中的 10 次。在运行 Android 6 的真实三星 Galaxy S5 Neo 设备上进行了 10 次测试中的 9 次。
正如我上面所写,您可以从以下位置下载整个示例github https://github.com/unlimited101/SwappingTestDoubles如果您找到解决时间问题的方法,请随意改进。
就是这样!