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/
Related
I am developing an Android application using Kotlin. I am writing integration tests for my application. Now I am having a problem mocking an injected dependency class that is injected using the Dagger 2 test if a method of the mocked object is called. Following is my code.
I have the TestAppComponent class with the following code
#Singleton
#Component(modules = [ TestAppModule::class ])
interface TestAppComponent
{
fun inject(app: ApplicationController)
fun inject(app: MockApplicationController)
}
As you can see in the class, I have two inject methods. One for the ApplicationController which basically is the application class used for the actual application. Another one for MockAppllication controller which is also the application class but it is used for the tests.
Following is the implementation of the ApplicationController class
open class ApplicationController: Application()
{
lateinit var appComponent: AppComponent
private fun initDagger(app: ApplicationController): AppComponent = DaggerAppComponent
.builder()
.appModule(AppModule(app)).build()
#Inject
lateinit var fileService: IFileService
override fun onCreate() {
super.onCreate()
appComponent = initDagger(this)
instance = this
this.appComponent.inject(this)
}
}
As you can see in the code, I am initializing the Dagger AppComponent class in the onCreate method of the application class. Then inject the dependency class.
This is the implementation of my MockApplicationController class which is used for the tests.
class MockApplicationController: ApplicationController()
{
private fun initDagger(app: MockApplicationController): AppComponent = DaggerTestAppComponent
.builder()
.testAppModule(TestAppModule(app))
.build()
override fun onCreate() {
super.onCreate()
//override the dagger app component appComponent
this.appComponent = initDagger(this)
instance = this
this.appComponent.inject(this)
}
}
This is my activity class.
class MainActivity: AppCompatActivity()
{
override fun onCreate(savedInstanceState: Bundle?) {
//rest of the code
button_save_file.setOnClickListener {
//rest of the code
ApplicationController.instance.fileService.saveFile(filePath)
}
}
}
As you can see, basically, what I am doing within the activity class is that I am just calling the saveFile of the IFileService interface.
Following is the definition of the IFileService interface
interface IFileService
{
fun saveFile(path: String)
}
There are two classes that are implementing the IFileService. One is FakeFileService class which will be used for the tests and the other one is FileService class which will be used for the actual application.
Following is the implementation of FakeFileService class
class FakeFileService: IFileService
{
fun saveFile(path: String)
{
//literally, this is doing nothing since we will only test that if the method is called
}
}
I also created two classes for the Dagger app modules. One for the tests and the other one for the actual application.
Following is the implementation of the TestAppModule class which will be used for tests.
#Module
open class TestAppModule (private val app: Application) {
private var fileService: IFileService? = null
#Singleton
#Provides
fun provideContext(): Context = app
//to override injecting the Mockito mock instance
fun injectFileService(fileService: IFileService) {
this.fileService = fileService
}
#Singleton
#Provides
open fun fileService(): IFileService {
if (this.fileService != null) {
return this.fileService as IFileService
}
return FakeFileService()
}
}
Note the injectFileService method. I created that method to inject the mock object/ instance mocked using Mockito within the tests.
This is my test class
#RunWith(AndroidJUnit4::class)
#LargeTest
class CameraTest: TestBuilder()
{
#get:Rule
var mainActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule<MainActivity>(CameraActivity::class.java, true, false)
private lateinit var fileServiceMock: IFileIOService
#Before
fun setUp() {
this.fileServiceMock = mock(IFileService::class.java)
MockitoAnnotations.initMocks(this)
val instrumentation = InstrumentationRegistry.getInstrumentation()
val app = instrumentation.targetContext.applicationContext as MockApplicationController
var testModule = TestAppModule(app)
testModule.injectFileService(this.fileServiceMock)
app.appComponent = DaggerTestAppComponent
.builder()
.testAppModule(testModule)
.build()
app.appComponent.inject(app)
}
#Test
fun itSavesFileWhenButtonIsClicked() {
Intents.init()
var targetContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
var intent: Intent = Intent(targetContext, MainActivity::class.java)
this.mainActivityRule.launchActivity(intent)
onView(withId(R.id.button_save_file)).perform(click())
verify(this.fileServiceMock).saveFile(any())
Intents.release()
}
}
As you can see, in my test class, I try to inject the mocked version of IFileService object before the test is run. Then in my test, I am asserting that the method is executed. When I run the test it is complaining that "zero interactions with this mock". How can I fix it? What is wrong with my code and how can I fix it?
This is my ApplicationController class
open class ApplicationController: Application()
{
lateinit var appComponent: AppComponent
private fun initDagger(app: ApplicationController): AppComponent = DaggerAppComponent
.builder()
.appModule(AppModule(app)).build()
#Inject
lateinit var fileService: IFileService
override fun onCreate() {
super.onCreate()
appComponent = initDagger(this)
instance = this
this.appComponent.inject(this)
}
}
When I put the loggin in each onCreate method of application controller classes and #Before method, they are called in the following order.
ApplicationController
MockApplicationController
The code to modify the MockApplicationController with the #Before method
In my Android project and in my app module, I have a Login screen. I want to provide its view model via dagger. However, it is always null although I clearly defined how to generate it in the module class. This is my code:
class AuthViewModel(
private val firebaseAuth: FirebaseAuth,
private val logger: Logger
) {
....
}
This is the module object.
#Module
object AuthModule {
#Provides
#JvmStatic
fun provideLogger(): Logger = getLogger() // It creates a Logger object forsure. I confirm it doesn't return null.
#Provides
#JvmStatic
fun provideViewModel(firebaseAuth: FirebaseAuth, logger: Logger) = AuthViewModel(firebaseAuth, logger)
#Provides
#JvmStatic
fun provideFirebaseAuth() = FirebaseAuth.getInstance()
}
this is the component
#FeatureScope
#Component(modules = [AuthModule::class])
interface AuthComponent {
#Component.Factory
interface Factory {
fun create(
#BindsInstance context: Context
): AuthComponent
}
}
This is how I inject it into my activity.
class AuthActivity : AppCompatActivity() {
#Inject lateinit var vm: AuthViewModel
#Inject lateinit var logger: Logger
companion object {
private val TAG = AuthActivity::class.java.simpleName
fun startActivity(ctx: Context) {
val intent = Intent(ctx, AuthActivity::class.java)
ctx.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DaggerAuthComponent.factory()
.create(this)
logger.logDebug("test") // <==== Crashes here because logger is null
}
}
Application crashes at the marked line above because logger is null. I debugged the app and notices the ViewModel is null, too.
I don't know what is the problem exactly but I replaced Factory with Builder and my problem fixed.
You are welcome to tell me what is the problem in my original code and I will more than happy to accept your answer. Thank you.
#FeatureScope
#Component(modules = [AuthModule::class])
interface AuthComponent {
#Component.Builder
interface Builder {
fun build(): AuthComponent
#BindsInstance fun activity(context: Context): Builder
}
fun inject(activity: AuthActivity)
}
I am currently learning how to inject into Android apps with Dagger 2. I wrote a very basic code, but it refuses to work. My goals is it to inject the MainActicity as it should be. It builds
My code:
class MainActivity : AppCompatActivity() {
#Inject lateinit var info: Info
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
txt_view.text = info.textInformation
}
}
class Info {val textInformation = "You are able to read this" }
#Module
class InfoModule{
#Provides
fun info ():Info{
return Info()
}
}
class CustomApp : Application (),HasActivityInjector{
#Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
override fun activityInjector(): AndroidInjector<Activity> {
return dispatchingAndroidInjector
}
}
#Component(modules = arrayOf(AndroidInjectionModule::class,
ActivityModule::class))
interface ApplicationComponent{
fun inject(application: CustomApp)
}
#Module
abstract class ActivityModule{
#ContributesAndroidInjector(modules = arrayOf(InfoModule::class))
abstract fun contributeInfoActivityInjector():MainActivity
}
The answer is that I forgot to add this code to the CustomApp class, and .CustomApp to the manifest file
override fun onCreate() {
super.onCreate()
initDi()
}
private fun initDi() {
DaggerApplicationComponent.builder().build().inject(this)
}
I have a hard time figuring out how to set up Dagger component hierarchy to match my vision. Any help would be greatly appreciated and please feel free to criticise the vision if you find any holes in it.
Let's get to the point.
I'm trying to redesing part of my application related to user registration. Since registration is a self contained domain I assume that it's a good candidate for a separate dagger component(RegistrationSubcomponent). Within that registration domain I will show an activity(RegistrationActivity) and on top of it few fragments(RegistrationFragment1, RegistrationFragment2, ...). I already have an application component(AppComponent) with the life scope of an application which should be a parent for RegistrationSubcomponent. AppComponent is used to inject dependecies into other activites excluding RegistrationActivity but also to a common activity(CommonActivity) which is a parent for every activity in my app including RegistrationActivity. To inject dependencies into RegistrationActivity i want to use RegistrationSubcomponent.
The RegistrationSubcomponent should be instantiated shortly before displaying RegistrationActivity and should live until after the RegistrationActivity returns. I dont't want to bind RegistrationSubcomponent lifecycle with RegistrationActivity lifecycle. Mayby in this case it may have sense but in future I will probably would like to create components for other app domains which may not neceserralily match the lifecycle of activities they encompass.
In general the problem is injection of dependecies specfied in RegistrationComponent into RegistrationActivity as well as to RegistrationFragments.
The problem is that the app crashes when navigating to RegistrationActivity with following error:
Unable to start activity ComponentInfo{pl.kermit.diproblem/pl.kermit.diproblem.activities.RegistrationActivity}: java.lang.IllegalArgumentException: No injector factory bound for Class
Minimal project presenting the problem can be found at https://github.com/mariola3000/DIProblemStack01
Below I put the most important parts of code
// APP COMPONENT
#Singleton
#Component(modules = [AndroidInjectionModule::class, AppModule::class, AppAndroidBindingModule::class,
AndroidSupportInjectionModule::class])
interface AppComponent {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
fun build(): AppComponent
}
fun inject(application: Application)
}
#Module(subcomponents = [RegistrationComponent::class])
class AppModule {
#Provides
fun provideGlobalDependency(): GlobalDependency {
return GlobalDependency("GlobalDependency")
}
#Provides
fun provideGlobal2Dependency(): Global2Dependency {
return Global2Dependency("Global2Dependency")
}
}
#Module
abstract class AppAndroidBindingModule {
#PerActivity
#ContributesAndroidInjector()
abstract fun bindMainActivity(): MainActivity
}
// REGISTRATION COMPONENT
#PerRegistration
#Subcomponent(modules = [RegistrationModule::class, RegistrationAndroidBindingModule::class])
interface RegistrationComponent {
#Subcomponent.Builder
interface Builder {
fun requestModule(module: RegistrationModule): Builder
fun build(): RegistrationComponent
}
}
#Module
class RegistrationModule {
#PerRegistration
#Provides
fun provideRegistrationDependency(): RegistrationDependency {
return RegistrationDependency("RegistrationDependency")
}
#PerRegistration
#Provides
fun provideRegistrationFragmentDependency(): RegistrationFragmentDependency {
return RegistrationFragmentDependency("RegistrationFragmentDependency")
}
}
#Module
abstract class RegistrationAndroidBindingModule {
#PerActivity
#ContributesAndroidInjector
abstract fun bindRegistrationActivity(): RegistrationActivity
#PerFragment
#ContributesAndroidInjector
abstract fun bindRegistrationFragment(): RegistrationFragment
}
// ACTIVITIES
abstract class CommonActivity : DaggerAppCompatActivity(){
#Inject
lateinit var globalDependency: GlobalDependency
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
Toast.makeText(this, "global message: " + globalDependency.message, Toast.LENGTH_LONG).show()
}
}
class MainActivity : CommonActivity() {
#set:Inject
lateinit var global2Dependency: Global2Dependency
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val text: TextView = findViewById(R.id.main_text)
text.text = global2Dependency.message
val button: Button = findViewById(R.id.main_button)
button.setOnClickListener {
startActivity(Intent(this, RegistrationActivity::class.java))
}
}
}
class RegistrationActivity : CommonActivity() {
#Inject
lateinit var registrationDependency: RegistrationDependency
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_registration)
val registrationMessage = findViewById<TextView>(R.id.registration_text)
registrationMessage.text = registrationDependency.message
val transaction = this.supportFragmentManager.beginTransaction()
val fragment = RegistrationFragment()
transaction.replace(R.id.registration_fragment_container, fragment)
transaction.addToBackStack(fragment.javaClass.simpleName)
transaction.commit()
}
}
I am a newbie to android and trying to use Dagger2. I spend whole night and still dont know why my dagger does not provide presenter. Here are my code (I use Kotlin)
AppComponent
#Singleton
#Component(modules = arrayOf(PresenterModule::class))
interface AppComponent {
fun inject(target: SplashActivity)
}
PresenterModule
#Module
class PresenterModule {
#Provides
#Singleton
fun provideSplashPresenter(): SplashPresenter {
return SplashPresenter()
}
}
App
class App: Application() {
companion object {
lateinit var appComponent: AppComponent
}
override fun onCreate() {
super.onCreate()
appComponent = initDagger()
}
private fun initDagger(): AppComponent {
return DaggerAppComponent.create()
}
}
This is the presenter
class SplashPresenter: BasePresenterImpl<SplashContract.View>(), SplashContract.Presenter {
override fun performToast(mess: String) {
logi("abc", "performToast")
logi("abc", "mess: " + mess)
mView?.showLoading()
if (mess.isNullOrBlank()) {
mView?.showTosat("this is empty mess") ?: logi("abc", "null")
} else {
mView?.showTosat(mess) ?: logi("abc", "null")
}
mView?.hideLoading()
}
}
And finally, this is my SplashActivity
class SplashActivity : BaseActivity(), SplashContract.View {
#Inject
lateinit var presenter: SplashPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
presenter.attachView(this)
//TODO: check log in
//TODO: If logged in => start main screen
//TODO: If not logged in => load login activity
button.setOnClickListener{
presenter.performToast(editText.text.toString())
logi("abc", "perform clicked")
}
}
}
When I run these code, I got this error
Lateinit property presenter has not been initialized, which means that "Inject" does not work
Since you're not using constructor injection here (which you can't, because you don't 'own' the activity's constructor) Dagger does not 'know' that it has to inject something into your Activity.
You have to manually inject like this:
(applicationContext as App).appComponent.inject(this)
in your SplashActivity's onCreate() method (before using the presenter, of course).
Second, your presenter needs a constructor that tells Dagger how to construct/'build' the presenter, which means a constructor annotated with the #Inject annotation, so:
class SplashPresenter #Inject constructor(): BasePresenterImpl<SplashContract.View>(), SplashContract.Presenter
You forgot to inject the SplashActivity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
App.appComponent.inject(this)
setContentView(R.layout.activity_splash)
...