编辑:小心!我已经删除了这个问题中提到的旧存储库。看看我自己对这个问题的回答,找到一个可能的解决方案,并随时改进它!
我指的是我在这里的帖子。现在我再往前走一点。我也指的是我的github项目中的两个分支:
- 实验[分支号][1](库已删除)
- 实验[分支号][2](库已删除)
在旧的帖子中,我试图在仪器测试中交换组件来测试组件。如果我有一个ApplicationComponent
,在单例范围内,这是有效的。但它确实不工作,如果我有一个自定义的@PerActivity
范围的ActivityComponent
。问题是不是作用域,而是将Component转换为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()
方法。使用后者,您可以将组件交换为Instrumentation test中的测试组件。下面是活动:
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()
片段中,组件被注入:
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"))));
}
}
活动测试运行,但使用了错误的Component
。这是因为在交换组件之前运行活动和片段onCreate()
。
您可以看到,当我将ApplicationComponent
绑定到应用程序类时,我有一个注释的旧方法。这是有效的,因为我可以在启动活动之前构建依赖项。但是现在使用ActivityComponent
,我必须在初始化注入器之前启动活动。否则我就不能设置
mActivity.setActivityComponent(mTestActivityComponent);
因为mActivity
是空的,它会在注入器初始化后启动该活动。(见MainActivityTest
)
那么我如何拦截MainActivity
和MainFragment
来使用TestActivityComponent
呢?
现在,通过混合一些示例,我发现了如何交换活动范围的组件和片段范围的组件。在这篇文章中,我将向你展示如何做到这两点。但是我将更详细地描述如何在InstrumentationTest期间交换片段作用域的组件。我的全部代码都托管在github上。你可以运行MainFragmentTest
类,但要注意,你必须将de.xappo.presenterinjection.runner.AndroidApplicationJUnitRunner
设置为Android Studio中的TestRunner。
现在我简要地描述如何用假交互器交换交互器。在这个例子中,我尽量尊重干净的架构。但它们可能是一些小事情,会稍微破坏这个架构。所以你可以自由地改进。
我们开始吧。首先你需要一个自己的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()
中,我在运行测试时创建活动之前(!)为活动创建了一个替代TestActivityGraph。然后我们必须创建一个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的Fragment:
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
:
/**
* 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来注册组件。然后我们用FragmentTestRule
启动TestActivity
。现在是第三个技巧,它依赖于时间,并不总是正确运行。直接在启动活动后,我们通过请求FragmentTestRule
获得Fragment
实例。然后我们使用TestFragmentComponentHolder
交换组件并注入Fragment图。第四个技巧是,我们只需要等待大约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
类的@Before
注释的setUp()
-方法中找到:
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);
}
}
除了时间问题。这个测试在我的环境中运行了10次,在API Level 23的模拟Android上运行了10次。在搭载安卓6系统的真正三星Galaxy S5 Neo设备上,10次测试中有9次运行良好。
就像我上面写的,你可以从github下载整个例子,如果你找到一个解决时间问题的方法,你可以自由地改进。
就是这样!