如何使用 Dagger 2 在 Activity 或 Fragment 的范围内交换测试替身?
How can I swap test doubles at the scope of an Activity or a Fragment using Dagger 2?
编辑:小心!我已经删除了这个问题中引用的旧存储库。请参阅我自己对问题的回答以获得可能的解决方案,并随时改进它!
我指的是我的 post here。现在我走得更远了。我还指的是我 github 项目中的两个分支:
- 实验性[分行号。 1](存储库已删除)
- 实验性[分行号。 2](存储库已删除)
在旧的 post 中,我尝试在 Instrumentation 测试中将组件交换为测试组件。如果我有一个 ApplicationComponent
,这在单例范围内就可以了。但是,如果我有一个具有自定义 @PerActivity
范围的 ActivityComponent
,它确实 not 工作。问题是不是范围,而是将组件交换到测试组件。
我的 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()
方法。使用后者,您可以将组件交换为 Instrumentation 测试中的测试组件。这是 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;
}
}
如您所见,activity 使用了 MainFragment
。在片段的 onCreate()
中注入了组件:
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
交换:
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"))));
}
}
Activity测试运行s但使用了错误的Component
。这是因为在交换组件之前,活动和片段 onCreate()
是 运行。
如您所见,我将一个 ApplicationComponent
绑定到应用程序 class。这是有效的,因为我可以在启动 activity 之前构建依赖关系。但是现在有了 ActivityComponent
,我必须在初始化注入器之前启动 activity。因为否则我无法设置
mActivity.setActivityComponent(mTestActivityComponent);
因为如果在注入器初始化后启动 activity,mActivity
将为 null。 (参见 MainActivityTest
)
那我怎么截取 MainActivity
和 MainFragment
来使用 TestActivityComponent
?
现在我通过混合一些示例发现了如何交换 Activity 范围的组件和片段范围的组件。在此 post 中,我将向您展示如何做到这两点。但我将更详细地描述如何在 InstrumentationTest 期间交换片段范围的组件。我的总代码托管在 github 上。您可以 运行 MainFragmentTest
class 但请注意,您必须在 Android Studio 中将 de.xappo.presenterinjection.runner.AndroidApplicationJUnitRunner
设置为 TestRunner。
现在我将简要描述如何用假交互器交换交互器。在示例中,我尽量尊重 clean architecture。但它们可能是一些小东西,稍微破坏了这个架构。所以请随时改进。
那么,让我们开始吧。首先你需要一个自己的 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 图(!) Activity 是在测试 运行 之前创建的.然后我们必须创建一个 TestFragmentComponent
:
@PerFragment
@Component(modules = TestFragmentModule.class)
public interface TestFragmentComponent extends FragmentComponent{
void inject(MainActivityTest mainActivityTest);
void inject(MainFragmentTest mainFragmentTest);
}
此组件位于片段范围内。它有一个模块:
@Module
public class TestFragmentModule {
@Provides
@PerFragment
MainInteractor provideMainInteractor () {
return new FakeMainInteractor();
}
}
原来的FragmentModule
是这样的:
@Module
public class FragmentModule {
@Provides
@PerFragment
MainInteractor provideMainInteractor () {
return new MainInteractor();
}
}
你看我用了一个MainInteractor
和一个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
来测试独立于生产中包含它的 Activity 的片段:
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);
}
}
但是现在如何交换组件?有几个小技巧可以实现这一点。首先我们需要一个持有人 class 来持有 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
。现在是第三个技巧,它依赖于时间并且并不总是 运行 正确。 直接启动activity后,我们通过询问FragmentTestRule
获得Fragment
实例。然后我们使用 TestFragmentComponentHolder
交换组件并注入片段图。第四个技巧是我们只等待大约 2 秒来创建片段。在 Fragment 中,我们在 onViewCreated()
中进行组件注入。因为那样我们就不会提前注入组件,因为之前调用了 onCreate()
和 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);
}
}
我之前描述的所有步骤(第二到第四个技巧)都可以在这个 MainFragmentTest
class 中的 @Before
注释 setUp()
-Method 中找到:
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);
}
}
除了时间问题。这个测试 运行s 在我的环境中的 10 次测试 运行s 中的 10 次测试 运行s 在模拟 Android 上 API 级别 23。它 运行s 在 9在具有 Android 6 的真实三星 Galaxy S5 Neo 设备上进行 10 次测试 运行。
正如我上面所写,您可以从 github 下载整个示例,如果您找到解决时间问题的方法,请随时进行改进。
就是这样!
编辑:小心!我已经删除了这个问题中引用的旧存储库。请参阅我自己对问题的回答以获得可能的解决方案,并随时改进它!
我指的是我的 post here。现在我走得更远了。我还指的是我 github 项目中的两个分支:
- 实验性[分行号。 1](存储库已删除)
- 实验性[分行号。 2](存储库已删除)
在旧的 post 中,我尝试在 Instrumentation 测试中将组件交换为测试组件。如果我有一个 ApplicationComponent
,这在单例范围内就可以了。但是,如果我有一个具有自定义 @PerActivity
范围的 ActivityComponent
,它确实 not 工作。问题是不是范围,而是将组件交换到测试组件。
我的 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()
方法。使用后者,您可以将组件交换为 Instrumentation 测试中的测试组件。这是 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;
}
}
如您所见,activity 使用了 MainFragment
。在片段的 onCreate()
中注入了组件:
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
交换:
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"))));
}
}
Activity测试运行s但使用了错误的Component
。这是因为在交换组件之前,活动和片段 onCreate()
是 运行。
如您所见,我将一个 ApplicationComponent
绑定到应用程序 class。这是有效的,因为我可以在启动 activity 之前构建依赖关系。但是现在有了 ActivityComponent
,我必须在初始化注入器之前启动 activity。因为否则我无法设置
mActivity.setActivityComponent(mTestActivityComponent);
因为如果在注入器初始化后启动 activity,mActivity
将为 null。 (参见 MainActivityTest
)
那我怎么截取 MainActivity
和 MainFragment
来使用 TestActivityComponent
?
现在我通过混合一些示例发现了如何交换 Activity 范围的组件和片段范围的组件。在此 post 中,我将向您展示如何做到这两点。但我将更详细地描述如何在 InstrumentationTest 期间交换片段范围的组件。我的总代码托管在 github 上。您可以 运行 MainFragmentTest
class 但请注意,您必须在 Android Studio 中将 de.xappo.presenterinjection.runner.AndroidApplicationJUnitRunner
设置为 TestRunner。
现在我将简要描述如何用假交互器交换交互器。在示例中,我尽量尊重 clean architecture。但它们可能是一些小东西,稍微破坏了这个架构。所以请随时改进。
那么,让我们开始吧。首先你需要一个自己的 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 图(!) Activity 是在测试 运行 之前创建的.然后我们必须创建一个 TestFragmentComponent
:
@PerFragment
@Component(modules = TestFragmentModule.class)
public interface TestFragmentComponent extends FragmentComponent{
void inject(MainActivityTest mainActivityTest);
void inject(MainFragmentTest mainFragmentTest);
}
此组件位于片段范围内。它有一个模块:
@Module
public class TestFragmentModule {
@Provides
@PerFragment
MainInteractor provideMainInteractor () {
return new FakeMainInteractor();
}
}
原来的FragmentModule
是这样的:
@Module
public class FragmentModule {
@Provides
@PerFragment
MainInteractor provideMainInteractor () {
return new MainInteractor();
}
}
你看我用了一个MainInteractor
和一个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
来测试独立于生产中包含它的 Activity 的片段:
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);
}
}
但是现在如何交换组件?有几个小技巧可以实现这一点。首先我们需要一个持有人 class 来持有 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
。现在是第三个技巧,它依赖于时间并且并不总是 运行 正确。 直接启动activity后,我们通过询问FragmentTestRule
获得Fragment
实例。然后我们使用 TestFragmentComponentHolder
交换组件并注入片段图。第四个技巧是我们只等待大约 2 秒来创建片段。在 Fragment 中,我们在 onViewCreated()
中进行组件注入。因为那样我们就不会提前注入组件,因为之前调用了 onCreate()
和 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);
}
}
我之前描述的所有步骤(第二到第四个技巧)都可以在这个 MainFragmentTest
class 中的 @Before
注释 setUp()
-Method 中找到:
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);
}
}
除了时间问题。这个测试 运行s 在我的环境中的 10 次测试 运行s 中的 10 次测试 运行s 在模拟 Android 上 API 级别 23。它 运行s 在 9在具有 Android 6 的真实三星 Galaxy S5 Neo 设备上进行 10 次测试 运行。
正如我上面所写,您可以从 github 下载整个示例,如果您找到解决时间问题的方法,请随时进行改进。
就是这样!