如何使用Dagger 2在活动或片段范围内交换测试双打? [英] How can I swap test doubles at the scope of an Activity or a Fragment using Dagger 2?

查看:85
本文介绍了如何使用Dagger 2在活动或片段范围内交换测试双打?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当心!我已删除此问题中提到的旧存储库.请参阅我对问题的答案,以寻求可能的解决方案,并随时进行改进!

我指的是我的帖子此处.现在我走得更远.我还指的是我在github项目中的两个分支:

I am refering to my post here. Now I came a little further. I am also refering to my two branches within my github Project:

  • 实验性[分行号1](存储库已删除)
  • 实验性[分行号2](存储库已删除)

在以前的文章中,我尝试在Instrumentation Test中将组件交换为test-component.现在,如果我有ApplicationComponent,它在单例范围内,则可以正常工作.但是如果我的ActivityComponent具有自定义的@PerActivity范围,则起作用.问题不是不是范围,而是将组件交换到TestComponent.

In the old post I tried to swap components to test-components within an Instrumentation Test. This works now if I have an ApplicationComponent, being in singleton scope. But it does not work if I have an ActivityComponent with a self defined @PerActivity scope. The problem is not the scope but the swapping of the Component to the TestComponent.

我的ActivityComponent有一个ActivityModule:

@PerActivity
@Component(modules = ActivityModule.class)
public interface ActivityComponent {
    // TODO: Comment this out for switching back to the old approach
    void inject(MainFragment mainFragment);
    // TODO: Leave that for witching to the new approach
    void inject(MainActivity mainActivity);
}

ActivityModule提供了MainInteractor

@Module
public class ActivityModule {
    @Provides
    @PerActivity
    MainInteractor provideMainInteractor () {
        return new MainInteractor();
    }
}

我的TestActivityComponent使用TestActivityModule:

@PerActivity
@Component(modules = TestActivityModule.class)
public interface TestActivityComponent extends ActivityComponent {
    void inject(MainActivityTest mainActivityTest);
}

TestActvityModule提供了FakeInteractor:

@Module
public class TestActivityModule {
    @Provides
    @PerActivity
    MainInteractor provideMainInteractor () {
        return new FakeMainInteractor();
    }
}

我的MainActivity具有getComponent()方法和setComponent()方法.使用后者,您可以将组件交换到仪器测试"中的测试组件.这是活动:

My MainActivity has a getComponent() method and a setComponent() method. With the latter you can swap the component to a test component within the Instrumentation Test. Here is the activity:

public class MainActivity extends BaseActivity implements MainFragment.OnFragmentInteractionListener {


    private static final String TAG = "MainActivity";
    private Fragment currentFragment;
    private ActivityComponent activityComponent;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initializeInjector();


        if (savedInstanceState == null) {
            currentFragment = new MainFragment();
            addFragment(R.id.fragmentContainer, currentFragment);
        }

    }

    private void initializeInjector() {
        Log.i(TAG, "injectDagger initializeInjector()");

        activityComponent = DaggerActivityComponent.builder()
                .activityModule(new ActivityModule())
                .build();
        activityComponent.inject(this);
    }

    @Override
    public void onFragmentInteraction(final Uri uri) {

    }

    ActivityComponent getActivityComponent() {
        return activityComponent;
    }

    @VisibleForTesting
    public void setActivityComponent(ActivityComponent activityComponent) {
        Log.w(TAG, "injectDagger Only call this method to swap test doubles");
        this.activityComponent = activityComponent;
    }
} 

如您所见,此活动使用MainFragment.在片段的onCreate()中注入了组分:

As you see this activity uses a MainFragment. In onCreate() of the fragment the component is injected:

public class MainFragment extends BaseFragment implements MainView {

    private static final String TAG = "MainFragment";
    @Inject
    MainPresenter mainPresenter;
    private View view;

    public MainFragment() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "injectDagger onCreate()");
        super.onCreate(savedInstanceState);
        // TODO: That approach works
//        ((AndroidApplication)((MainActivity) getActivity()).getApplication()).getApplicationComponent().inject(this);
        // TODO: This approach is NOT working, see MainActvityTest
        ((MainActivity) getActivity()).getActivityComponent().inject(this);
    }
}

然后在测试中,将ActivityComponent替换为TestApplicationComponent:

And then in the test I swap the ActivityComponent with the TestApplicationComponent:

public class MainActivityTest{

    @Rule
    public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule(MainActivity.class, true, false);

    private MainActivity mActivity;
    private TestActivityComponent mTestActivityComponent;

    // TODO: That approach works
//    private TestApplicationComponent mTestApplicationComponent;
//
//    private void initializeInjector() {
//        mTestApplicationComponent = DaggerTestApplicationComponent.builder()
//                .testApplicationModule(new TestApplicationModule(getApp()))
//                .build();
//
//        getApp().setApplicationComponent(mTestApplicationComponent);
//        mTestApplicationComponent.inject(this);
//    }

    // TODO: This approach does NOT work because mActivity.setActivityComponent() is called after MainInteractor has already been injected!
    private void initializeInjector() {
        mTestActivityComponent = DaggerTestActivityComponent.builder()
                .testActivityModule(new TestActivityModule())
                .build();

        mActivity.setActivityComponent(mTestActivityComponent);
        mTestActivityComponent.inject(this);
    }

    public AndroidApplication getApp() {
        return (AndroidApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
    }
    // TODO: That approach works

//    @Before
//    public void setUp() throws Exception {
//
//        initializeInjector();
//        mActivityRule.launchActivity(null);
//        mActivity = mActivityRule.getActivity();
//    }

    // TODO: That approach does not works because mActivity.setActivityComponent() is called after MainInteractor has already been injected!
    @Before
    public void setUp() throws Exception {
        mActivityRule.launchActivity(null);
        mActivity = mActivityRule.getActivity();
        initializeInjector();
    }


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

}

运行活动"测试,但使用了错误的Component.这是因为活动和片段onCreate()是在交换组件之前运行的.

The Activity test runs but the wrong Component is used. This is because activities and fragments onCreate() is run before the component is swapped.

如您所见,如果将ApplicationComponent绑定到应用程序类,则我有一个注释过的旧方法.之所以可行,是因为我可以在开始活动之前建立依赖关系.但是现在使用ActivityComponent,我必须在初始化注入器之前启动该活动.因为否则我无法设置

As you can see I have an commented old approach were I bind an ApplicationComponent to the application class. This works because I can build the dependency before starting the activity. But now with the ActivityComponent I have to launch the activity before initializing the injector. Because otherwise I could not set

mActivity.setActivityComponent(mTestActivityComponent);

因为mActivity为空,如果将在初始化注射器后启动活动,则为. (请参见MainActivityTest)

because mActivity would be null if would launch the activity after the initialization of the injector. (See MainActivityTest)

那我该如何拦截MainActivityMainFragment以使用TestActivityComponent?

So how could I intercept the MainActivity and the MainFragment to use the TestActivityComponent?

推荐答案

现在,我通过混合一些示例来了解如何交换活动范围的组件和片段范围的组件.在这篇文章中,我将向您展示如何做到这两个.但是,我将更详细地描述如何在InstrumentationTest期间交换片段范围的组件.我的总代码托管在 github 上.您可以运行MainFragmentTest类,但要注意,必须在Android Studio中将de.xappo.presenterinjection.runner.AndroidApplicationJUnitRunner设置为TestRunner.

Now I found out by mixing some examples how to exchange an Activity-scoped component and a Fragment-scoped component. In this post I will show you how to do both. But I will describe in more detail how to swap a Fragment-scoped component during an InstrumentationTest. My total code is hosted on github. You can run the MainFragmentTest class but be aware that you have to set de.xappo.presenterinjection.runner.AndroidApplicationJUnitRunner as TestRunner in Android Studio.

现在,我简短地描述如何通过伪交互器交换交互器.在示例中,我尝试尽可能尊重干净的架构.但是它们可能是一些细小的东西,会稍微破坏该体系结构.因此,随时进行改进.

Now I describe shortly what to do to swap an Interactor by a Fake Interactor. In the example I try to respect clean architecture as much as possible. But they may be some small things which break this architecture a bit. So feel free to improve.

那么,让我们开始吧.首先,您需要一个自己的JUnitRunner:

So, let's start. At first you need an own 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;
    }
}

swapActivityGraph()中,我在运行测试之前创建Activity之前(!)创建了Activity的替代TestActivityGraph.然后,我们必须创建一个TestFragmentComponent:

In swapActivityGraph() I create an alternative TestActivityGraph for the Activity before(!) the Activity is created when running the test. Then we have to create a TestFragmentComponent:

@PerFragment
@Component(modules = TestFragmentModule.class)
public interface TestFragmentComponent extends FragmentComponent{
    void inject(MainActivityTest mainActivityTest);

    void inject(MainFragmentTest mainFragmentTest);
}

此组件位于片段范围内.它具有一个模块:

This component lives in a Fragment-scope. It has a module:

@Module
public class TestFragmentModule {
    @Provides
    @PerFragment
    MainInteractor provideMainInteractor () {
        return new FakeMainInteractor();
    }
}

原始的FragmentModule看起来像这样:

@Module
public class FragmentModule {
    @Provides
    @PerFragment
    MainInteractor provideMainInteractor () {
        return new MainInteractor();
    }
}

您看到我使用了MainInteractorFakeMainInteractor.它们都看起来像这样:

You see I use a MainInteractor and a FakeMainInteractor. They both look like that:

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来测试片段,而该片段独立于生产环境中包含它的活动:

Now we use a self-defined FragmentTestRule for testing the Fragment independent from the Activity which contains it in production:

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;
    }
}

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:

But now how to swap the components? There are several small tricks to achieve that. At first we need a holder class for holding the 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;
    }
}

第二个技巧是甚至在创建片段之前,使用持有人在之前注册组件.然后,用FragmentTestRule启动TestActivity.现在来了第三个技巧,它与时间有关,并且并不总是能正确运行.启动活动后,直接直接,我们通过询问FragmentTestRule来获得Fragment实例.然后,我们使用TestFragmentComponentHolder交换组件并注入Fragment图.第四招是我们只需要等待大约2秒钟即可创建Fragment.在Fragment中,我们在onViewCreated()中进行组分注入.因为那样的话我们就不会提早注入该组件,因为onCreate()onCreateView()之前已经被调用过.这是我们的MainFragment:

The second trick is to use the holder to register the component before the fragment is even created. Then we launch the TestActivity with our FragmentTestRule. Now comes the third trick which is timing-dependent and does not always run correctly. Directly after launching the activity we get the Fragment instance by asking the FragmentTestRule. Then we swap the component, using the TestFragmentComponentHolder and inject the Fragment graph. The forth trick is we just wait for about 2 seconds for the Fragment to be created. And within the Fragment we make our component injection in onViewCreated(). Because then we don't inject the component to early because onCreate() and onCreateView() are called before. So here is our 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);
    }
}

我之前介绍的所有步骤(第二到第四招)都可以在该类MainFragmentTest中的@Before带注释的setUp()-方法中找到:

And all the steps (second to forth trick) which I described before can be found in the @Before annotated setUp()-Method in this 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的真实Samsung Galaxy S5 Neo设备上,在10项测试中有9进行了测试.

Except from the timing problem. This test runs in my environment in 10 of 10 test runs on an emulated Android with API Level 23. And it runs in 9 of 10 test runs on a real Samsung Galaxy S5 Neo device with Android 6.

正如我上面所写的,您可以从 github 下载整个示例,如果您愿意,可以随时进行改进找到一种解决小计时问题的方法.

As I wrote above you can download the whole example from github and feel free to improve if you find a way to fix the little timing problem.

就是这样!

这篇关于如何使用Dagger 2在活动或片段范围内交换测试双打?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆