blob: 7c3427919a8dbb25fb640b9bda9aa3a6b3a2bea2 [file] [log] [blame]
/*
* 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<Integer> {
@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<T> 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<TestActivity> 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<DoubleInjectActivity> 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 extends Fragment> T setupFragment(
Class<? extends FragmentActivity> 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();
}
}