/* * Copyright (C) 2020 The Dagger Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package dagger.hilt.android; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import android.annotation.TargetApi; import android.app.Activity; import android.app.IntentService; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import android.util.AttributeSet; import android.widget.LinearLayout; import androidx.test.ext.junit.runners.AndroidJUnit4; import dagger.Module; import dagger.Provides; import dagger.hilt.InstallIn; import dagger.hilt.android.components.ActivityComponent; import dagger.hilt.android.components.FragmentComponent; import dagger.hilt.android.components.ServiceComponent; import dagger.hilt.android.testing.HiltAndroidRule; import dagger.hilt.android.testing.HiltAndroidTest; import dagger.hilt.android.testing.HiltTestApplication; import dagger.hilt.components.SingletonComponent; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.concurrent.atomic.AtomicLong; import javax.inject.Inject; import javax.inject.Qualifier; import javax.inject.Singleton; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.Config; @HiltAndroidTest @RunWith(AndroidJUnit4.class) // Robolectric requires Java9 to run API 29 and above, so use API 28 instead @Config(sdk = Build.VERSION_CODES.P, application = HiltTestApplication.class) public final class InjectionTest { private static final String APP_BINDING = "APP_BINDING"; private static final String ACTIVITY_BINDING = "ACTIVIY_BINDING"; private static final String FRAGMENT_BINDING = "FRAGMENT_BINDING"; private static final String SERVICE_BINDING = "SERVICE_BINDING"; @Retention(RUNTIME) @Qualifier @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) @interface ApplicationLevel {} @Retention(RUNTIME) @Qualifier @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) @interface ActivityLevel {} @Retention(RUNTIME) @Qualifier @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) @interface FragmentLevel {} @Retention(RUNTIME) @Qualifier @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) @interface ServiceLevel {} /** Application level bindings */ @Module @InstallIn(SingletonComponent.class) static final class AppModule { @Provides @ApplicationLevel static String providesAppBinding() { return APP_BINDING; } @Provides @Singleton static AtomicLong provideCounter() { return new AtomicLong(); } @Provides static Long provideCount(AtomicLong counter) { return counter.incrementAndGet(); } } /** Activity level bindings */ @Module @InstallIn(ActivityComponent.class) static final class ActivityModule { @Provides @ActivityLevel static String providesActivityBinding() { return ACTIVITY_BINDING; } } /** Fragment level bindings */ @Module @InstallIn(FragmentComponent.class) static final class FragmentModule { @Provides @FragmentLevel static String providesFragmentBinding() { return FRAGMENT_BINDING; } } /** Service level bindings */ @Module @InstallIn(ServiceComponent.class) static final class ServiceModule { @Provides @ServiceLevel static String providesServiceBinding() { return SERVICE_BINDING; } } /** Hilt Activity */ @AndroidEntryPoint(FragmentActivity.class) public static final class TestActivity extends Hilt_InjectionTest_TestActivity { @Inject @ApplicationLevel String appBinding; @Inject @ActivityLevel String activityBinding; boolean onCreateCalled; @Override public void onCreate(Bundle onSavedInstanceState) { assertThat(appBinding).isNull(); assertThat(activityBinding).isNull(); super.onCreate(onSavedInstanceState); assertThat(appBinding).isEqualTo(APP_BINDING); assertThat(activityBinding).isEqualTo(ACTIVITY_BINDING); onCreateCalled = true; } } /** Non-Hilt Activity */ public static final class NonHiltActivity extends FragmentActivity {} /** Hilt Fragment */ @AndroidEntryPoint(Fragment.class) public static final class TestFragment extends Hilt_InjectionTest_TestFragment { @Inject @ApplicationLevel String appBinding; @Inject @ActivityLevel String activityBinding; @Inject @FragmentLevel String fragmentBinding; boolean onAttachContextCalled; boolean onAttachActivityCalled; @Override public void onAttach(Context context) { preInjectionAssert(); super.onAttach(context); postInjectionAssert(); onAttachContextCalled = true; } @Override public void onAttach(Activity activity) { preInjectionAssert(); super.onAttach(activity); postInjectionAssert(); onAttachActivityCalled = true; } private void preInjectionAssert() { assertThat(appBinding).isNull(); assertThat(activityBinding).isNull(); assertThat(fragmentBinding).isNull(); } private void postInjectionAssert() { assertThat(appBinding).isEqualTo(APP_BINDING); assertThat(activityBinding).isEqualTo(ACTIVITY_BINDING); assertThat(fragmentBinding).isEqualTo(FRAGMENT_BINDING); } } /** Non-Hilt Fragment */ public static final class NonHiltFragment extends Fragment {} /** Hilt extends parameterized fragment. */ @AndroidEntryPoint(ParameterizedFragment.class) public static final class TestParameterizedFragment extends Hilt_InjectionTest_TestParameterizedFragment { @Inject @ApplicationLevel String appBinding; @Inject @ActivityLevel String activityBinding; @Inject @FragmentLevel String fragmentBinding; boolean onAttachContextCalled; boolean onAttachActivityCalled; @Override public void onAttach(Context context) { preInjectionAssert(); super.onAttach(context); postInjectionAssert(); onAttachContextCalled = true; } @Override public void onAttach(Activity activity) { preInjectionAssert(); super.onAttach(activity); postInjectionAssert(); onAttachActivityCalled = true; } private void preInjectionAssert() { assertThat(appBinding).isNull(); assertThat(activityBinding).isNull(); assertThat(fragmentBinding).isNull(); } private void postInjectionAssert() { assertThat(appBinding).isEqualTo(APP_BINDING); assertThat(activityBinding).isEqualTo(ACTIVITY_BINDING); assertThat(fragmentBinding).isEqualTo(FRAGMENT_BINDING); } } /** Non-Hilt parameterized fragment */ public static class ParameterizedFragment extends Fragment {} /** Hilt View */ @AndroidEntryPoint(LinearLayout.class) public static final class TestView extends Hilt_InjectionTest_TestView { @Inject @ApplicationLevel String appBinding; @Inject @ActivityLevel String activityBinding; TestView(Context context) { super(context); } TestView(Context context, AttributeSet attrs) { super(context, attrs); } TestView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @TargetApi(21) TestView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } } /** Hilt View (With Fragment bindings) */ @WithFragmentBindings @AndroidEntryPoint(LinearLayout.class) public static final class TestViewWithFragmentBindings extends Hilt_InjectionTest_TestViewWithFragmentBindings { @Inject @ApplicationLevel String appBinding; @Inject @ActivityLevel String activityBinding; @Inject @FragmentLevel String fragmentBinding; TestViewWithFragmentBindings(Context context) { super(context); } } @AndroidEntryPoint(Service.class) public static final class TestService extends Hilt_InjectionTest_TestService { @Inject @ApplicationLevel String appBinding; @Inject @ServiceLevel String serviceBinding; @Override public IBinder onBind(Intent intent) { return null; } } @AndroidEntryPoint(IntentService.class) public static final class TestIntentService extends Hilt_InjectionTest_TestIntentService { private static final String NAME = "TestIntentServiceName"; @Inject @ApplicationLevel String appBinding; @Inject @ServiceLevel String serviceBinding; TestIntentService() { super(NAME); } @Override public void onHandleIntent(Intent intent) {} } @AndroidEntryPoint(BroadcastReceiver.class) public static final class TestBroadcastReceiver extends Hilt_InjectionTest_TestBroadcastReceiver { @Inject @ApplicationLevel String appBinding; Intent lastIntent = null; @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); lastIntent = intent; } } @AndroidEntryPoint(BaseBroadcastReceiver.class) public static final class TestBroadcastReceiverWithBaseImplementingOnReceive extends Hilt_InjectionTest_TestBroadcastReceiverWithBaseImplementingOnReceive { @Inject @ApplicationLevel String appBinding; Intent baseLastIntent = null; @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); baseLastIntent = intent; } } abstract static class BaseBroadcastReceiver extends BroadcastReceiver { Intent lastIntent = null; @Override public void onReceive(Context context, Intent intent) { lastIntent = intent; } } @Rule public final HiltAndroidRule rule = new HiltAndroidRule(this); @Inject @ApplicationLevel String appBinding; @Before public void setup() { rule.inject(); } @Test public void testAppInjection() throws Exception { assertThat(appBinding).isEqualTo(APP_BINDING); } @Test public void testActivityInjection() throws Exception { ActivityController controller = Robolectric.buildActivity(TestActivity.class); assertThat(controller.get().onCreateCalled).isFalse(); controller.create(); assertThat(controller.get().onCreateCalled).isTrue(); } @Test public void testFragmentInjection() throws Exception { TestFragment fragment = new TestFragment(); assertThat(fragment.onAttachContextCalled).isFalse(); assertThat(fragment.onAttachActivityCalled).isFalse(); setupFragment(TestActivity.class, fragment); assertThat(fragment.onAttachContextCalled).isTrue(); assertThat(fragment.onAttachActivityCalled).isTrue(); } @Test public void testParameterizedFragmentInjection() throws Exception { TestParameterizedFragment fragment = new TestParameterizedFragment(); assertThat(fragment.onAttachContextCalled).isFalse(); assertThat(fragment.onAttachActivityCalled).isFalse(); setupFragment(TestActivity.class, fragment); assertThat(fragment.onAttachContextCalled).isTrue(); assertThat(fragment.onAttachActivityCalled).isTrue(); } @Test public void testViewNoFragmentBindingsWithActivity() throws Exception { TestActivity activity = Robolectric.setupActivity(TestActivity.class); TestView view = new TestView(activity); assertThat(view.appBinding).isEqualTo(APP_BINDING); assertThat(view.activityBinding).isEqualTo(ACTIVITY_BINDING); } @Test public void testViewNoFragmentBindingsWithFragment() throws Exception { TestFragment fragment = setupFragment(TestActivity.class, new TestFragment()); TestView view = new TestView(fragment.getContext()); assertThat(view.appBinding).isEqualTo(APP_BINDING); assertThat(view.activityBinding).isEqualTo(ACTIVITY_BINDING); } @Test public void testViewNoFragmentBindingsWithFragment_secondConstructor() throws Exception { TestFragment fragment = setupFragment(TestActivity.class, new TestFragment()); TestView view = new TestView(fragment.getContext(), /* attrs= */ null); assertThat(view.appBinding).isEqualTo(APP_BINDING); assertThat(view.activityBinding).isEqualTo(ACTIVITY_BINDING); } @Test public void testViewNoFragmentBindingsWithFragment_thirdConstructor() throws Exception { TestFragment fragment = setupFragment(TestActivity.class, new TestFragment()); TestView view = new TestView(fragment.getContext(), /* attrs= */ null, /* defStyleAttr= */ 0); assertThat(view.appBinding).isEqualTo(APP_BINDING); assertThat(view.activityBinding).isEqualTo(ACTIVITY_BINDING); } @Test @Config(sdk = 21) public void testViewNoFragmentBindingsWithFragment_fourthConstructor_presentOnTwentyOne() throws Exception { TestFragment fragment = setupFragment(TestActivity.class, new TestFragment()); TestView view = new TestView( fragment.getContext(), /* attrs= */ null, /* defStyleAttr= */ 0, /* defStyleRes= */ 0); assertThat(view.appBinding).isEqualTo(APP_BINDING); assertThat(view.activityBinding).isEqualTo(ACTIVITY_BINDING); } @Test @Config(sdk = 19) public void testViewNoFragmentBindingsWithFragment_fourthConstructor_notPresentOnTwenty() { TestFragment fragment = setupFragment(TestActivity.class, new TestFragment()); assertThrows( NoSuchMethodError.class, () -> new TestView( fragment.getContext(), /* attrs= */ null, /* defStyleAttr= */ 0, /* defStyleRes= */ 0)); } @Test public void testServiceInjection() throws Exception { TestService testService = Robolectric.setupService(TestService.class); assertThat(testService.appBinding).isEqualTo(APP_BINDING); assertThat(testService.serviceBinding).isEqualTo(SERVICE_BINDING); } @Test public void testIntentServiceInjection() throws Exception { TestIntentService testIntentService = Robolectric.setupService(TestIntentService.class); assertThat(testIntentService.appBinding).isEqualTo(APP_BINDING); assertThat(testIntentService.serviceBinding).isEqualTo(SERVICE_BINDING); } @Test public void testBroadcastReceiverInjection() throws Exception { TestBroadcastReceiver testBroadcastReceiver = new TestBroadcastReceiver(); Intent intent = new Intent(); testBroadcastReceiver.onReceive(getApplicationContext(), intent); assertThat(testBroadcastReceiver.appBinding).isEqualTo(APP_BINDING); assertThat(testBroadcastReceiver.lastIntent).isSameInstanceAs(intent); } @Test public void testBroadcastReceiverWithBaseImplementingOnReceiveInjection() throws Exception { TestBroadcastReceiverWithBaseImplementingOnReceive testBroadcastReceiver = new TestBroadcastReceiverWithBaseImplementingOnReceive(); Intent intent = new Intent(); testBroadcastReceiver.onReceive(getApplicationContext(), intent); assertThat(testBroadcastReceiver.appBinding).isEqualTo(APP_BINDING); assertThat(testBroadcastReceiver.lastIntent).isSameInstanceAs(intent); assertThat(testBroadcastReceiver.baseLastIntent).isSameInstanceAs(intent); } @Test public void testViewWithFragmentBindingsWithFragment() throws Exception { TestFragment fragment = setupFragment(TestActivity.class, new TestFragment()); Context fragmentContext = fragment.getContext(); TestViewWithFragmentBindings view = new TestViewWithFragmentBindings(fragmentContext); assertThat(view.appBinding).isEqualTo(APP_BINDING); assertThat(view.activityBinding).isEqualTo(ACTIVITY_BINDING); assertThat(view.fragmentBinding).isEqualTo(FRAGMENT_BINDING); } @Test public void testViewWithFragmentBindingsFailsWithActivity() throws Exception { TestActivity activity = Robolectric.setupActivity(TestActivity.class); try { new TestViewWithFragmentBindings(activity); fail("Expected test to fail but it passes!"); } catch (IllegalStateException e) { assertThat(e) .hasMessageThat() .contains( "@WithFragmentBindings Hilt view must be attached to an @AndroidEntryPoint Fragment"); } } @Test public void testFragmentAttachedToNonHiltActivityFails() throws Exception { NonHiltActivity activity = Robolectric.setupActivity(NonHiltActivity.class); try { activity .getSupportFragmentManager() .beginTransaction() .add(new TestFragment(), null) .commitNow(); fail("Expected test to fail but it passes!"); } catch (IllegalStateException e) { assertThat(e) .hasMessageThat() .contains("Hilt Fragments must be attached to an @AndroidEntryPoint Activity"); } } @Test public void testViewAttachedToNonHiltActivityFails() throws Exception { NonHiltActivity activity = Robolectric.setupActivity(NonHiltActivity.class); try { new TestView(activity); fail("Expected test to fail but it passes!"); } catch (IllegalStateException e) { assertThat(e) .hasMessageThat() .contains("Hilt view must be attached to an @AndroidEntryPoint Fragment or Activity"); } } @Test public void testViewAttachedToNonHiltFragmentFails() throws Exception { NonHiltActivity activity = Robolectric.setupActivity(NonHiltActivity.class); NonHiltFragment fragment = new NonHiltFragment(); activity.getSupportFragmentManager().beginTransaction().add(fragment, null).commitNow(); Context nonHiltContext = fragment.getContext(); try { new TestView(nonHiltContext); fail("Expected test to fail but it passes!"); } catch (IllegalStateException e) { assertThat(e) .hasMessageThat() .contains("Hilt view must be attached to an @AndroidEntryPoint Fragment or Activity"); } } @Test public void testViewAttachedToApplicationContextFails() throws Exception { try { new TestView(getApplicationContext()); fail("Expected test to fail but it passes!"); } catch (IllegalStateException e) { assertThat(e) .hasMessageThat() .contains( "Hilt view cannot be created using the application context. " + "Use a Hilt Fragment or Activity context"); } } /** Hilt Activity that manually calls inject(). */ @AndroidEntryPoint(FragmentActivity.class) public static final class DoubleInjectActivity extends Hilt_InjectionTest_DoubleInjectActivity { @Inject Long counter; @Override public void onCreate(Bundle onSavedInstanceState) { inject(); super.onCreate(onSavedInstanceState); } } @Test public void testActivityDoesNotInjectTwice() throws Exception { ActivityController controller = Robolectric.buildActivity(DoubleInjectActivity.class); controller.create(); assertThat(controller.get().counter).isEqualTo(1L); } /** Hilt Fragment that manually calls inject(). */ @AndroidEntryPoint(Fragment.class) public static final class DoubleInjectFragment extends Hilt_InjectionTest_DoubleInjectFragment { @Inject Long counter; @Override public void onAttach(Context context) { inject(); super.onAttach(context); } @Override public void onAttach(Activity activity) { inject(); super.onAttach(activity); } } @Test public void testFragmentDoesNotInjectTwice() throws Exception { DoubleInjectFragment fragment = setupFragment(TestActivity.class, new DoubleInjectFragment()); assertThat(fragment.counter).isEqualTo(1L); } /** Hilt View that manually calls inject(). */ @AndroidEntryPoint(LinearLayout.class) public static final class DoubleInjectView extends Hilt_InjectionTest_DoubleInjectView { @Inject Long counter; DoubleInjectView(Context context) { super(context); inject(); } DoubleInjectView(Context context, AttributeSet attrs) { super(context, attrs); inject(); } DoubleInjectView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); inject(); } @TargetApi(21) DoubleInjectView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); inject(); } } @Test public void testViewDoesNotInjectTwice() throws Exception { TestActivity activity = Robolectric.setupActivity(TestActivity.class); DoubleInjectView view = new DoubleInjectView(activity); assertThat(view.counter).isEqualTo(1L); } /** Hilt Service that manually calls inject(). */ @AndroidEntryPoint(Service.class) public static final class DoubleInjectService extends Hilt_InjectionTest_DoubleInjectService { @Inject Long counter; @Override public void onCreate() { inject(); super.onCreate(); } @Override public IBinder onBind(Intent intent) { return null; } } @Test public void testServiceDoesNotInjectTwice() throws Exception { DoubleInjectService testService = Robolectric.setupService(DoubleInjectService.class); assertThat(testService.counter).isEqualTo(1L); } /** Hilt BroadcastReceiver that manually calls inject(). */ @AndroidEntryPoint(BroadcastReceiver.class) public static final class DoubleInjectBroadcastReceiver extends Hilt_InjectionTest_DoubleInjectBroadcastReceiver { @Inject Long counter; @Override public void onReceive(Context context, Intent intent) { inject(context); super.onReceive(context, intent); } } @Test public void testBroadcastReceiverDoesNotInjectTwice() throws Exception { DoubleInjectBroadcastReceiver testBroadcastReceiver = new DoubleInjectBroadcastReceiver(); Intent intent = new Intent(); testBroadcastReceiver.onReceive(getApplicationContext(), intent); assertThat(testBroadcastReceiver.counter).isEqualTo(1L); } private static T setupFragment( Class activityClass, T fragment) { FragmentActivity activity = Robolectric.setupActivity(activityClass); attachFragment(activity, fragment); return fragment; } private static void attachFragment(FragmentActivity activity, Fragment fragment) { activity.getSupportFragmentManager().beginTransaction().add(fragment, "").commitNow(); } }