Search code examples
androiddependency-injectionmockitojunit4dagger-2

Dagger 2 - Inject fields in activity


Before I start, I've read a lot of tutorials but each of them contains info about old dagger - using @builder which is now deprecated. I'm using @Factory

What I have?

class LoginActivity : AppCompatActivity() {

@Inject
lateinit var authService: AuthService

override fun onCreate(savedInstanceState: Bundle?) {
    AndroidInjection.inject(this)
....
}
}
//----------------
@Singleton
@Component(modules = [TestAppModule::class])
interface TestApplicationComponent : AndroidInjector<TestMyApplication> {
    @Component.Factory
    abstract class Builder : AndroidInjector.Factory<TestMyApplication>
}
//----------------
class TestMyApplication : MyApplication() {
    override fun onCreate() {
        super.onCreate()
        JodaTimeAndroid.init(this)
    }
    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        return DaggerTestApplicationComponent.factory().create(this)
    }
}
//----------------
@Singleton
open class AuthService @Inject constructor(
    @AppContext val context: Context, private val authRemoteDataSource: AuthRemoteDataSource
) {
...
}
//----------------
class MockRunner : AndroidJUnitRunner() {
    override fun onCreate(arguments: Bundle?) {
        StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().permitAll().build())
        super.onCreate(arguments)
    }

    override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
        return super.newApplication(cl, TestMyApplication::class.qualifiedName, context)
    }
}

Notes:

  1. I show you, constructor in AuthService because it has more than 0 args
  2. Mock runner applies my TestMyApplication class

And TestClass

@RunWith(AndroidJUnit4::class)
    class LoginActivityTest {

@Mock
lateinit var mockAuthService: AuthService

@Rule
@JvmField
val activityRule = ActivityTestRule<LoginActivity>(LoginActivity::class.java, false, false)

@Before
fun beforeEach() {
    MockitoAnnotations.initMocks(this)
    Mockito.doReturn(NOT_SIGNED).`when`(mockAuthService).getUserSignedStatus(ArgumentMatchers.anyBoolean())
    println(mockAuthService.getUserSignedStatus(true)) //test
}

@Test
fun buttonLogin() {
    activityRule.launchActivity(Intent())
    onView(withText("Google")).check(matches(isDisplayed()));
}
}

What do I want? - In the simplest way attach mocked AuthService to LoginActivity

What I've got? Error:

While calling method: android.content.Context.getSharedPreferences In line:

Mockito.doReturn(NOT_SIGNED).`when`(mockAuthService).getUserSignedStatus(ArgumentMatchers.anyBoolean())

Method getSharedPreferences is called in real method getUserSignedStatus. So now, I'm getting an error because Mockito.when calls the real function which is public. I think, the second problem will be that mocked AuthService is not injected to LoginActivity


Solution

  • So you should probably provide the AuthService through a module, one for the normal app and one for the android test, which supplies the mocked version. That would mean removing the Dagger annotations from the AuthService class. I don't use Component.Factory but this example should be enough to for you to use as a guide.

    In androidTest folder :

    Create test module :

        // normal app should include the module to supply this dependency
        @Module object AndroidTestModule {
    
            val mock : AuthService = Mockito.mock(AuthService::class.java)
    
            @Provides
            @Singleton
            @JvmStatic
            fun mockService() : AuthService =  mock
    
        }
    

    Create test component :

    @Component(modules = [AndroidTestModule::class])
    @Singleton
    interface AndroidTestComponent : AndroidInjector<AndroidTestApp> {
    
        @Component.Builder interface Builder {
    
            @BindsInstance fun app(app : Application) : Builder
    
            fun build() : AndroidTestComponent
        }
    }
    

    Create test app :

    class AndroidTestApp : DaggerApplication() {
    
        override fun onCreate() {
            super.onCreate()
    
            Timber.plant(Timber.DebugTree())
        }
    
        override fun applicationInjector(): AndroidInjector<out DaggerApplication> =
                DaggerAndroidTestAppComponent.builder().app(this).build()
    }
    

    then the runner :

    class AndroidTestAppJunitRunner : AndroidJUnitRunner() {
    
        override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
            return super.newApplication(cl, AndroidTestApp::class.java.canonicalName, context)
        }
    }
    

    include in android closure in Gradle :

    testInstrumentationRunner "com.package.name.AndroidTestAppJunitRunner"

    add these deps :

    kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion"
    kaptAndroidTest "com.google.dagger:dagger-android-processor:$daggerVersion"
    
    androidTestImplementation "org.mockito:mockito-android:2.27.0"
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test:rules:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    

    then a test :

    @RunWith(AndroidJUnit4::class) class LoginActivityTest {
    
        @Rule
        @JvmField
        val activityRule = ActivityTestRule<LoginActivity>(LoginActivity::class.java, false, false)
    
        @Before
        fun beforeEach() {
    Mockito.doReturn(NOT_SIGNED).`when`(AndroidTestModule.mock).getUserSignedStatus(ArgumentMatchers.anyBoolean()
        }
    
        @Test
        fun buttonLogin() {
            activityRule.launchActivity(Intent())
            onView(withText("Google")).check(matches(isDisplayed()));
        }
    }
    

    Your dependency will then supplied through the generated test component graph to LoginActivity