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:
I show you, constructor in AuthService because it has more than 0 args
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
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
Related
I am working on instrument test in android studio.
My app is Dynamic Feature Module use both dagger and hilt to provide dependency.
I am following this doc
https://developer.android.com/training/dependency-injection/hilt-testing#replace-binding
As it said I need to Replace a binding but when on build I got error
com.xxxxxxx.CartRepository cannot be provided without an #Provides-annotated method.
public abstract static class SingletonC implements CheckoutFragmentTest_GeneratedInjector,
This is my dagger hilt version
"com.google.dagger:hilt-android:2.38.1"
"com.google.dagger:hilt-compiler:2.38.1"
"com.google.dagger:hilt-android-testing:2.38.1"
"com.google.dagger:hilt-android-compiler:2.38.1"
"androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02"
"androidx.hilt:hilt-compiler:1.0.0-alpha02"
"androidx.hilt:hilt-common:1.0.0-alpha02"
Here is how I provide dependency in production code is feature module
Inject.kt file in feature cart module
//Inject.kt file in feature cart module
internal fun inject(fragment: CheckoutFragment) {
DaggerCartComponent
.factory()
.create(
fragment,
coreComponent(fragment),
appComponent(fragment)
)
.inject(fragment)
}
private fun appComponent(fragment: Fragment): AppComponent =
EntryPointAccessors.fromActivity(
fragment.requireActivity(),
AppComponent::class.java
)
private fun coreComponent(fragment: Fragment): CoreComponent =
EntryPointAccessors.fromApplication(
fragment.requireActivity().applicationContext,
CoreComponent::class.java
)
feature cart module component
//feature cart module component
#Component(
dependencies = [CoreComponent::class, AppComponent::class],
modules = [
CartViewModelModule::class,
CartDataModule::class
]
)
interface CartComponent {
fun inject(fragment: CartFragment)
fun inject(fragment: CheckoutFragment)
fun inject(fragment: CartNavigationFragment)
fun inject(fragment: PickupInformationFragment)
#Component.Factory
interface Factory {
fun create(
#BindsInstance fragment: Fragment,
coreComponent: CoreComponent,
appComponent: AppComponent
): CartComponent
}
}
CartDataModule
#[Module InstallIn(FragmentComponent::class)]
abstract class CartDataModule {
#Binds
abstract fun provideCartRepository(cartRepositoryImpl: CartRepositoryImpl) : CartRepository
#Binds
abstract fun provideCheckOutRepository(checkoutRepositoryImpl: CheckoutRepositoryImpl) : CheckoutRepository
#Binds
abstract fun provideMainRepository(mainRepositoryImpl: MainRepositoryImpl): MainRepository
}
I use this as a factory to create my viewmodel that hold my repository
class DFMSavedStateViewModelFactory(
owner: SavedStateRegistryOwner,
defaultArgs: Bundle?,
private val delegateFactory: SavedStateViewModelFactory,
private val viewModelFactories: #JvmSuppressWildcards Map<String, Provider<ViewModelAssistedFactory<out ViewModel>>>,
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
#SuppressLint("RestrictedApi")
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
val factoryProvider = viewModelFactories[modelClass.name]
?: return delegateFactory.create("$KEY_PREFIX:$key", modelClass)
#Suppress("UNCHECKED_CAST")
return factoryProvider.get().create(handle) as T
}
companion object {
private const val KEY_PREFIX = "androidx.hilt.lifecycle.HiltViewModelFactory"
}
}
And Here is my code under androidTest folder
CheckoutFragmentTest.kt
#ExperimentalCoroutinesApi
#UninstallModules(
CartDataModule::class,
)
#HiltAndroidTest
class CheckoutFragmentTest {
#Module
#InstallIn(FragmentComponent::class)
abstract class TestModule1 {
#Binds
#Singleton
abstract fun provideCartRepository(cartRepositoryImpl: FakeCartRepositoryImpl): CartRepository
#Binds
#Singleton
abstract fun provideCheckOutRepository(checkoutRepositoryImpl: FakeCheckoutRepositoryImpl): CheckoutRepository
}
#get:Rule
var hiltRule = HiltAndroidRule(this)
#get:Rule
var activityScenarioRule = activityScenarioRule<MainActivity>()
#Before
fun init() {
hiltRule.inject()
}
#Test
fun testCheckoutUICase1() {
launchFragmentInHiltContainer<CheckoutFragment> {
}
}
}
And this FakeCartDataModule that assume to replace CartDataModule in production code
#Module
#TestInstallIn(
components = [FragmentComponent::class],
replaces = [CartDataModule::class]
)
abstract class FakeCartDataModule {
#Binds
abstract fun provideCartRepository(cartRepositoryImpl: FakeCartRepositoryImpl): CartRepository
#Binds
abstract fun provideCheckOutRepository(checkoutRepositoryImpl: FakeCheckoutRepositoryImpl): CheckoutRepository
}
Does anyone know why I cannot provide FakeCartDataModule to run my androidTest?
Despite of I already following along the doc
I'm new using Dagger2 (I always used Koin) and I'm trying to implement a simple sample but I don't really know what I'm missing. This is what I got so far.
app.gradle:
ext.daggerVersion = '2.23.2'
implementation "com.google.dagger:dagger:$daggerVersion"
implementation "com.google.dagger:dagger-android-support:$daggerVersion"
kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
AppModule.kt:
#Module
class AppModule {
#Provides
#Singleton
fun provideApplication(app: App): Application = app
#Provides
#Singleton
fun provideTestOperator(testOperator: TestOperator) = testOperator
#Provides
#Singleton
fun provideTestClass(testClass: TestClass) = testClass
}
AppComponent.kt:
#Singleton
#Component(modules = [
AndroidInjectionModule::class,
AppModule::class
])
interface AppComponent : AndroidInjector<App> {
#Component.Builder
interface Builder {
#BindsInstance
fun application(app: App): Builder
fun build(): AppComponent
}
}
TestClass.kt & TestOperator.kt in the same file:
class TestClass #Inject constructor(private val testOperator: TestOperator) {
fun getRandomValueFromCTest(): Int = testOperator.generateRandomNumber()
}
class TestOperator #Inject constructor() {
fun generateRandomNumber(): Int = Random.nextInt()
}
App.kt:
class App : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.builder().application(this#App).build()
}
}
MainActivity.kt:
class MainActivity : AppCompatActivity() {
#Inject
lateinit var testClass: TestClass
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onResume() {
super.onResume()
val x = testClass.getRandomValueFromCTest()
}
}
Error: testClass == null
AppModule.kt: Provide the application context. No need to write #singleton #provides for your Test* classes (will see why)
#Module
class AppModule {
#Provides
#Singleton
fun provideApplication(app: App): Context = app.applicationContext
}
AppComponent.kt: #Component.Builder is deprecated IIRC. Use #Component.Factory. And replace AndroidInjectionModule::class with AndroidSupportInjectionModule::class since we are using dagger-android-support and android's *Compat* stuff. Refer a new module here called ActivityModule::class.
#Singleton
#Component(modules = [
ActivityModule::class
AndroidSupportInjectionModule::class,
AppModule::class
])
interface AppComponent : AndroidInjector<App> {
#Component.Factory
abstract class Factory : AndroidInjector.Factory<App>
}
TestClass.kt & TestOperator.kt: Since you were providing singletons by writing #singleton and #provides method, I assume you want them to be singletons. Just annotate the class definition with #Singleton and dagger will take care of it. No need to write #Provides methods.
#Singleton
class TestClass #Inject constructor(private val testOperator: TestOperator) {
fun getRandomValueFromCTest(): Int = testOperator.generateRandomNumber()
}
#Singleton
class TestOperator #Inject constructor() {
fun generateRandomNumber(): Int = Random.nextInt()
}
App.kt: Using factory instead of builder since #Component.Builder is deprecated.
class App : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.factory().create(this)
}
}
ActivityModule.kt: Provide a module to dagger to create your activities.
#Module
interface ActivityModule {
#ContributesAndroidInjector
fun provideMainActivity(): MainActivity
}
MainActivity.kt: Finally, extend from DaggerAppCompatActivity.
class MainActivity : DaggerAppCompatActivity() {
#Inject
lateinit var testClass: TestClass
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onResume() {
super.onResume()
val x = testClass.getRandomValueFromCTest()
}
}
I believe this should run without issues. For more reference you could look into this sample and the new simpler docs at dagger.dev/android
You are missing the actual injection call.
class MainActivity : AppCompatActivity() {
#Inject
lateinit var testClass: TestClass
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
MainActivity should extends DaggerActivity, not AppCompatActivity
I am trying to set up a project with dagger 2.21 and these are my classes
BaseApp
class BaseApp : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.builder().create(this)
}
}
App Component
#Singleton
#Component(
modules = [
AndroidSupportInjectionModule::class,
AppModule::class,
NetworkModule::class,
ActivityBuilder::class]
)
interface AppComponent: AndroidInjector<BaseApp> {
#Component.Builder
abstract class Builder : AndroidInjector.Builder<BaseApp>() {}
}
Base Activity
abstract class BaseActivity: DaggerAppCompatActivity(), BaseFragment.Callback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onFragmentAttached() {
}
override fun onFragmentDetached(tag: String) {
}
fun isNetworkConnected(): Boolean {
return NetManager(applicationContext).isConnectedToInternet
}
}
Base Fragment
abstract class BaseFragment: DaggerFragment() {
abstract fun layoutId(): Int
public interface Callback {
fun onFragmentAttached()
fun onFragmentDetached(tag: String)
}
}
Flight Activity
class FlightPricesActivity : BaseActivity() {
#Inject
lateinit var flightPricesFragment: FlightPricesFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.flight_price_list)
addFragment(flightPricesFragment, R.id.flightPricesContainer)
}
}
Flight Fragment
class FlightPricesFragment: BaseFragment(), IFlightPricesView {
override fun layoutId() = R.layout.flight_prices_fragment
override fun showFlightPrices(flightPrices: FlightPricesResults) {
// connect to recycleview
}
override fun loadingStarted() {
}
override fun loadingFailed(errorMessage: String?) {
}
}
Activity module
#Module
internal class FlightPricesActivityModule {
#Provides
fun provideFlightPricesPresenter(flightPricesApi: FlightPricesApi) : IFlightPricesPresenter {
return FlightPricesPresenter(flightPricesApi)
}
#Provides
fun provideFlightPricesApi(retrofit: Retrofit) : FlightPricesApi {
return retrofit.create(FlightPricesApi::class.java)
}
#Provides
fun provideFlightPricesRVAdapter() : FlightPricesRVAdapter {
return FlightPricesRVAdapter()
}
}
Fragment Module Provider
#Module
public abstract class FlightPricesFragmentProvider {
#ContributesAndroidInjector(modules = [FlightPricesFragmentModule::class])
abstract fun provideFlightPricesFragment(): FlightPricesFragment
}
Fragment module
#Module
class FlightPricesFragmentModule {
#Provides
#FragmentScope
fun provideFlightPricesFragment() : FlightPricesFragment {
return FlightPricesFragment()
}
}
Activity Builder
#Module
public abstract class ActivityBuilder {
#ContributesAndroidInjector(modules = [FlightPricesActivityModule::class, FlightPricesFragmentProvider::class])
public abstract fun bindFlightPricesActivity(): FlightPricesActivity
}
Dagger 2.21 depenedancies
implementation 'com.google.dagger:dagger:2.21'
annotationProcessor 'com.google.dagger:dagger-compiler:2.21'
implementation 'com.google.dagger:dagger-android:2.21'
implementation 'com.google.dagger:dagger-android-support:2.21'
annotationProcessor 'com.google.dagger:dagger-android-processor:2.21'
kapt "com.google.dagger:dagger-compiler:2.21"
kapt "com.google.dagger:dagger-android-processor:2.21"
this is stack trace error(updated):
/app/build/tmp/kapt3/stubs/debug/com/reza/skyscannertest/di/component/AppComponent.java:8: error: [Dagger/MissingBinding] com.reza.skyscannertest.ui.flightPrices.view.FlightPricesFragment cannot be provided without an #Inject constructor or an #Provides-annotated method. This type supports members injection but cannot be implicitly provided.
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.reza.skyscannertest.BaseApp> {
^
A binding with matching key exists in component: com.reza.skyscannertest.di.module.flightPrices.FlightPricesFragmentProvider_ProvideFlightPricesFragment.FlightPricesFragmentSubcomponent
com.reza.skyscannertest.ui.flightPrices.view.FlightPricesFragment is injected at
com.reza.skyscannertest.ui.flightPrices.view.FlightPricesActivity.flightPricesFragment
com.reza.skyscannertest.ui.flightPrices.view.FlightPricesActivity is injected at
dagger.android.AndroidInjector.inject(T) [com.reza.skyscannertest.di.component.AppComponent → com.reza.skyscannertest.di.module.ActivityBuilder_BindFlightPricesActivity.FlightPricesActivitySubcomponent]
I try to provide flightFragment but look like not going there.
I should add constructor for the my fragment as dagger looking for constructor to build provider
class FlightPricesPresenter #Inject constructor(flightPricesApi: FlightPricesApi) : IFlightPricesPresenter {
thanks my 3 days reading docs but it was worth to have dagger 2.21 on board in replacement of dagger 2.10 :)
I constantly get kotlin.UninitializedPropertyAccessException: lateinit property xxx has not been initialized in my Mockito test. But the app works just fine. Note: I don't want to inject presenter into activity. Thanks in advance!
Here's my Activity:
class CreateAccountActivity : AppCompatActivity(), CreateAccountView {
private var presenter: CreateAccountPresenter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_create_account)
presenter = CreateAccountPresenter()
((application) as CariumApp).getDaggerComponent().inject(presenter!!)
presenter?.attachView(this)
}
And here's my Presenter:
class CreateAccountPresenter {
private var view: CreateAccountView? = null
#Inject
lateinit var dataManager: DataManager
fun attachView(view: CreateAccountView) {
this.view = view
dataManager.getServiceDocuments(true, object : GetServiceDocumentsListener {
// ...
})
}
Here's my DataManager:
interface DataManager {
fun getServiceDocuments(latest: Boolean, listener: GetServiceDocumentsListener)
}
and AppDataManager:
Singleton
class AppDataManager #Inject constructor(context: Context) : DataManager {
// ...
}
and finally my test that's failing:
class CreateAccountPresenterTest {
val mockDataManager: DataManager = mock()
val mockCreateAccountView: CreateAccountView = mock()
private val createAccountPresenter = CreateAccountPresenter()
#Test
fun getServiceDocuments() {
doAnswer {
val args = it.arguments
(args[1] as GetServiceDocumentsListener).onError()
null
}.`when`(mockDataManager).getServiceDocuments(Mockito.anyBoolean(), anyOrNull())
createAccountPresenter.attachView(mockCreateAccountView)
verify(mockCreateAccountView).hideLoadingDialog()
}
}
gradle file:
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.22.0'
testImplementation "org.mockito:mockito-inline:2.22.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.0.0-RC1"
implementation 'com.google.dagger:dagger:2.16'
kapt 'com.google.dagger:dagger-compiler:2.16'
My module class:
#Module
open class MyModule(private var context: Context) {
#Provides
open fun provideContext(): Context {
return context
}
#Provides
#Singleton
internal fun provideDataManager(appDataManager: AppDataManager): DataManager {
return appDataManager
}
}
Actual error is kotlin.UninitializedPropertyAccessException: lateinit property dataManager has not been initialized
You are not assigning your mock to the field. Assign it in your test method. Before calling attachView()
createAccountPresenter.dataManager = mockDataManager
Where do you have DataManager #Provides method? Dagger recognizes #Inject constructor inside AppDataManager but cannot recognize it as interface. Create Module for Dagger that is abstract and uses #Binds
https://proandroiddev.com/dagger-2-annotations-binds-contributesandroidinjector-a09e6a57758f
I am new to unit testing in Android and have gone through several tutorials to get myself familiar with mockito and robolectric.
My app is using Dagger 2 to inject my EventService into my MainActivity. For my MainActivityUnitTest, I have set up a TestServicesModule to provide a mocked version of EventService so that I can use Robolectric to run unit tests against my MainActivity
I'm having an issue getting the ServiceCallback on my EventService.getAllEvents(callback: ServiceCallback) to execute in the unit test. I have verified in the #Setup of my MainActivityUnitTest class that the EventService is being injected as a mocked object. I have gone through several tutorials and blog posts and as far as I can tell, I am doing everything correctly. The refreshData() function in MainActivity is getting called successfully, and I can see that the call to eventsService.getAllEvents(callback) is being executed. But the doAnswer {} lambda function is never getting executed.
Here's my relevant code:
AppComponent.kt
#Singleton
#Component(modules = [
AppModule::class,
ServicesModule::class,
FirebaseModule::class
])
interface AppComponent {
fun inject(target: MainActivity)
}
ServicesModule.kt
#Module
open class ServicesModule {
#Provides
#Singleton
open fun provideEventService(db: FirebaseFirestore): EventsService {
return EventsServiceImpl(db)
}
}
EventsService.kt
interface EventsService {
fun getAllEvents(callback: ServiceCallback<List<Event>>)
fun getEvent(id: String, callback: ServiceCallback<Event?>)
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
#Inject lateinit var eventsService: EventsService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
(application as App).appComponent.inject(this)
...
}
override fun onStart() {
super.onStart()
refreshData()
}
eventsService.getAllEvents(object: ServiceCallback<List<Event>> {
override fun onCompletion(result: List<Event>) {
viewModel.allEvents.value = result
loading_progress.hide()
}
})
}
Now we get into the tests:
TestAppComponent.kt
#Singleton
#Component(modules = [
TestServicesModule::class
])
interface TestAppComponent : AppComponent {
fun inject(target: MainActivityUnitTest)
}
TestServicesModule.kt
#Module
class TestServicesModule {
#Provides
#Singleton
fun provideEventsService(): EventsService {
return mock()
}
}
MainActivityUnitTest.kt
#RunWith(RobolectricTestRunner::class)
#Config(application = TestApp::class)
class MainActivityUnitTest {
#Inject lateinit var eventsService: EventsService
#Before
fun setup() {
val testComponent = DaggerTestAppComponent.builder().build()
testComponent.inject(this)
}
#Test
fun givenActivityStarted_whenLoadFailed_shouldDisplayNoEventsMessage() {
val events = ArrayList<Event>()
doAnswer {
//this block is never hit during debug
val callback: ServiceCallback<List<Event>> = it.getArgument(0)
callback.onCompletion(events)
}.whenever(eventsService).getAllEvents(any())
val activity = Robolectric.buildActivity(MainActivity::class.java).create().start().visible().get()
val noEventsView = activity.findViewById(R.id.no_events) as View
//this always evaluates to null because the callback is never set from the doAnswer lambda
assertThat(callback).isNotNull()
verify(callback)!!.onCompletion(events)
assertThat(noEventsView.visibility).isEqualTo(View.VISIBLE)
}
}
Edit: Adding App and TestApp
open class App : Application() {
private val TAG = this::class.qualifiedName
lateinit var appComponent: AppComponent
override fun onCreate() {
super.onCreate()
appComponent = initDagger(this)
}
open fun initDagger(app: App): AppComponent {
return DaggerAppComponent.builder().appModule(AppModule(app)).build()
}
}
class TestApp : App() {
override fun initDagger(app: App): AppComponent {
return DaggerTestAppComponent.builder().build()
}
}
It looks like you're using a different component to inject your test and activity. As they're different components I suspect you are using 2 different instances of the eventsService.
Your test uses a local DaggerTestAppComponent.
#Inject lateinit var eventsService: EventsService
#Before
fun setup() {
val testComponent = DaggerTestAppComponent.builder().build()
testComponent.inject(this)
}
While your Activity uses the appComponent from the application.
class MainActivity : AppCompatActivity() {
#Inject lateinit var eventsService: EventsService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
(application as App).appComponent.inject(this)
...
}
To overcome this you may consider adding a test version of your application class, this would allow you to replace the AppComponent in your application with your TestAppComponent. Robolectric should allow you to create a test application as follows: http://robolectric.org/custom-test-runner/