Instantiate Hilt Worker for Instrumentation Testing? - android-testing

I am having problem trying to initialize my WorkerGetData class for instrumentation testing. I have done the following:
removed the default work initializer in manifest file.
added configuration provider in the Application class.
called WorkManagerTestInitHelper in the Test file.
added kapt "androidx.hilt:hilt-compiler:1.0.0" to app module.
added kapt "com.google.dagger:hilt-compiler:2.42" to app module.
But still get the error java.lang.NoSuchMethodException: com.example.myproject.WorkerGetData.<init> [class android.content.Context, class androidx.work.WorkerParameters].
I am using work manager version 2.7.1.
Code
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
#HiltAndroidApp
class Application : android.app.Application(), Configuration.Provider {
#Inject
lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
override fun onCreate() {
super.onCreate()
}
}
#HiltWorker
class WorkerGetData #AssistedInject constructor(
val repository: MyRepository,
#Assisted val context: Context,
#Assisted workerParameters: WorkerParameters,
): CoroutineWorker(context, workerParameters) {
override suspend fun doWork(): Result {
val data = repository.getData()
return Result.success(data)
}
}
#Test
fun testGetDataWorker() {
val request = OneTimeWorkRequestBuilder<WorkerGetData>()
.build()
val workManager = WorkManager.getInstance(context)
workManager.enqueue(request).result.get() /*<-----------------------error here*/
val workInfo = workManager.getWorkInfoById(request.id).get()
assertThat(workInfo.state).isEqualTo(WorkInfo.State.SUCCEEDED)
}

There was a 6th item which I had forgot to do. I needed to create a WorkerFatory() so that Hilt could correctly inject WorkerGetData constructor. There is a great article from Pietro Maggi on how to do this:
https://medium.com/androiddevelopers/customizing-workmanager-with-dagger-1029688c0978

Related

FirebaseFirestore cannot be provided without an #Inject constructor or an #Provides-annotated method

Im struggling with the following issue and I cant find a solution. I have multi module project. I set up all the modules and dependencies but im still getting this error for firestore: error: [Dagger/MissingBinding] com.google.firebase.firestore
My DI setup is following
FirebaseDiModule(part of the firestore module)
#Module
#InstallIn(SingletonComponent::class)
object FirebaseDiModule {
#Singleton
#Provides
fun provideFirebaseAuth(): FirebaseAuth {
return FirebaseAuth.getInstance()
}
#Singleton
#Provides
fun provideFirestoreFirebase(): FirebaseFirestore {
return FirebaseFirestore.getInstance()
}
#Singleton
#Provides
fun provideUserFirebaseDataSource(
firebaseFirestore: FirebaseFirestore,
firebaseAuth: FirebaseAuth,
): UserFirestoreDataSource {
return UserFirestoreDataSource(
firebaseFirestore = firebaseFirestore,
firebaseAuth = firebaseAuth,
)
}
}
UserFirestoreDataSource (part of the firebase module)
class UserFirestoreDataSource #Inject constructor(
private val firebaseFirestore: FirebaseFirestore,
private val firebaseAuth: FirebaseAuth,
)
Then I have authentication module which is beasicly a feature module contaning jetpack compose and viewmodel's.
The ViewModel i use is set as this:
#HiltViewModel
class OnBoardingViewModel #Inject constructor(
userFirestoreDataSource: UserFirestoreDataSource,
) : ViewModel() {
The authentication module is added to the app module where I use the OnBoardingViewModel and composables.
In the app module I have this:
#HiltAndroidApp
class AppController : Application()
class MainActivity : ComponentActivity()
When I run the project I get the following error:
error: [Dagger/MissingBinding] com.google.firebase.firestore.FirebaseFirestore cannot be provided without an #Inject constructor or an #Provides-annotated method.
public abstract static class SingletonC implements AppController_GeneratedInjector,
But this error only occurs when I add #HiltViewModel annotation to the OnBoardingViewModel. Im really frustrated by this problem and I cant figure it out what am I doing wrong.
There is nothing to try because it's a DI setup.
You need to create a #HiltViewModel class and annotate it with the #HiltViewModel annotation.
here is an example code:
#HiltViewModel
class OnBoardingViewModel #ViewModelInject constructor(
userFirestoreDataSource: UserFirestoreDataSource
) : ViewModel() {
//ViewModel code
}
it's like telling DI (#ViewModelInject) hey this is a ViewModel class inject the necessary dependencies into the ViewModel

How to switch my Worker to TestWorker with Hilt?

I am doing end-to-end test which use WorkManager in my Application. My DelegatingWorkerFactory() is hilt injected and creates the different factories, which in turn create the different ListenableWorkers in my app.
For testing, I would like to bind to these workers to the test version i.e. TestListenableWorker(). What is the best way to do this using Hilt?
Appreciate there could be XY problem here and there might be a different approach to achieve this. Essentially I just want to seamlessly switch to TestListenableWorkers with hilt injection.
#Singleton
class MyDelegatingWorkerFactory #Inject constructor(
myRepository: MyRepository
): DelegatingWorkerFactory() {
init {
addFactory(WorkerGetDataFactory(myRepository))
addFactory(WorkerOnReceiverFactory(myRepository))
addFactory(WorkerUpdateStateFactory(myRepository))
}
}
class WorkerGetDataFactory(
private val myRepository: MyRepository
): WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters,
): ListenableWorker? { /*<---- how can I use Hilt to implement TestListenableWorker instance*/
return when (workerClassName) {
WorkerGetData::class.java.name ->
WorkerGetData(myRepository, appContext, workerParameters, )
else ->
null
}
}
}

Facing issues with adding sharedPreferences and the sharedPrefrencesEditor to Koin module

I recently got to know about Koin.
I was trying to migrate my current project from Dagger to Koin.
In doing so, I faced an issue with injecting sharedPreferences and sharedPreferences editor in the activities.
Following is the code I used in Dagger to inject sharedPreferences and sharedPreferences editor ->
#Provides
#AppScope
fun getSharedPreferences(context: Context): SharedPreferences =
context.getSharedPreferences("default", Context.MODE_PRIVATE)
#SuppressLint("CommitPrefEdits")
#Provides
#AppScope
fun getSharedPrefrencesEditor(context: Context): SharedPreferences.Editor =
getSharedPreferences(context).edit()
I tried to convert the above mentioned code to Koin like this ->
val appModule = module {
val ctx by lazy{ androidApplication() }
single {
ctx.getSharedPreferences("default", Context.MODE_PRIVATE)
}
single {
getSharedPreferences(ctx).edit()
}
}
I also tried to implement it this way ->
val appModule = module {
single {
androidApplication().getSharedPreferences("default", Context.MODE_PRIVATE)
}
single {
getSharedPreferences(androidApplication()).edit()
}
}
Now I inject the dependencies in my activity like this ->
val sharedPreferences: SharedPreferences by inject()
val sharedPreferencesEditor: SharedPreferences.Editor by inject()
But as soon as I launch my app and try to use them, I am not able to read or write anything to the preferences.
I am a bit confused as to what is wrong with the code.
Kindly help me figure this out.
I figured out a way to handle this.
Hope this helps someone looking for the same issue.
Here is the solution to the problem:
The koin module definition will be like this ->
val appModule = module {
single{
getSharedPrefs(androidApplication())
}
single<SharedPreferences.Editor> {
getSharedPrefs(androidApplication()).edit()
}
}
fun getSharedPrefs(androidApplication: Application): SharedPreferences{
return androidApplication.getSharedPreferences("default", android.content.Context.MODE_PRIVATE)
}
Just to be clear that the above code is in the file modules.kt
Now you can easily inject the created instances like ->
private val sharedPreferences: SharedPreferences by inject()
private val sharedPreferencesEditor: SharedPreferences.Editor by inject()
Make sure the above instances are val instead of var otherwise the inject() method will not work as this is lazy injection.

#Resource Injections in Weld SE

I have a bean with #Resource-annotated field:
#ApplicationScoped
open class UtilProducer {
...
#Resource(lookup = "java:jboss/datasources/mj2")
private lateinit var dataSource: DataSource
...
And I want to make Weld inject something into this dataSource field.
I tried to add my own implementation of ResourceInjectionServices - a MyResourceInjectionServices, but it doesn't seemed that it tries even to instantiate my class
val weld = Weld()
.disableDiscovery()
.addPackages(true, UtilProducer::class.java)
.addPackages(true, CDIViewProvider::class.java)
.addBeanClass(MyResourceInjectionServices::class.java)
How should I configure Weld SE to inject #Resource-annotated fields?
Finally I've found in documentation that in such cases I need to subclass a Weld object. and override a createDeployment method:
public class MyWeld extends Weld {
protected Deployment createDeployment(ResourceLoader resourceLoader, CDI11Bootstrap bootstrap) {
return super.createDeployment(new MyResourceLoader(), bootstrap);
}
}
In my case in Kotlin in looks:
val weld = object : Weld() {
override fun createDeployment(resourceLoader: ResourceLoader?, bootstrap: CDI11Bootstrap?): Deployment {
val deployment = super.createDeployment(resourceLoader, bootstrap)
deployment.services.add(ResourceInjectionServices::class.java, MyResourceInjectionServices())
return deployment
}
}.apply {
disableDiscovery()
addPackages(true, UtilProducer::class.java)
addPackages(true, CDIViewProvider::class.java)
}

Bind list of objects using Guice + Kotlin

I'm writing a JavaFX application in Kotlin with the following controller definition:
class MainController {
#Inject private lateinit var componentDescriptors: List<ComponentDescriptor>
/* More code goes here */
}
I'm using Guice for Dependency management. And I'm trying to inject the list of class instances loaded via java.util.ServiceLoader. My problem is to define a binding that will inject the list of loaded object instances into the declared field. I tried annotation based provisioning:
internal class MyModule: AbstractModule() {
override fun configure() { }
#Provides #Singleton
fun bindComponentDescriptors(): List<ComponentDescriptor> =
ServiceLoader.load(ComponentDescriptor::class.java).toList()
}
and multibinding extension (switched List to Set in field definition of corse):
internal class MyModule: AbstractModule() {
override fun configure() {
val componentDescriptorBinder = Multibinder.newSetBinder(binder(), ComponentDescriptor::class.java)
ServiceLoader.load(ComponentDescriptor::class.java).forEach {
componentDescriptorBinder.addBinding().toInstance(it)
}
}
}
but both of these approaches leads to the same error:
No implementation for java.util.List<? extends simpleApp.ComponentDescriptor> was bound.
while locating java.util.List<? extends simpleApp.ComponentDescriptor>
for field at simpleApp.MainController.componentDescryptors(MainController.kt:6)
while locating simpleApp.MainController
1 error
at com.google.inject.internal.InjectorImpl.getProvider(InjectorImpl.java:1042)
at com.google.inject.internal.InjectorImpl.getProvider(InjectorImpl.java:1001)
at com.google.inject.internal.InjectorImpl.getInstance(InjectorImpl.java:1051)
at com.gluonhq.ignite.guice.GuiceContext.getInstance(GuiceContext.java:46)
at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:929)
at javafx.fxml.FXMLLoader$InstanceDeclarationElement.processAttribute(FXMLLoader.java:971)
at javafx.fxml.FXMLLoader$Element.processStartElement(FXMLLoader.java:220)
at javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:744)
at javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2707)
at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2527)
... 12 more
I'm starting to suspect that it somehow related to Kotlin gerenic variance and Guice strict type checking. But I don't know how to declare the binding so Guice will know what to inject into this field.
Yes, it happens because of variance but there's a way to make it work.
class MainController {
#JvmSuppressWildcards
#Inject
private lateinit var componentDescriptors: List<ComponentDescriptor>
}
By default Kotlin generates List<? extends ComponentDescriptor> signature for the componentDescriptors field. The #JvmSuppressWildcards makes it generate a simple parameterized signature List<ComponentDescriptor>.
#Michael gives the correct answer and explanation. Here's an example of one strategy for unit testing a Set multibinding for those that like to test their modules:
class MyModuleTest {
#JvmSuppressWildcards
#Inject
private lateinit var myTypes: Set<MyType>
#Before fun before() {
val injector = Guice.createInjector(MyModule())
injector.injectMembers(this)
}
#Test fun multibindings() {
assertNotNull(myTypes)
assertTrue(myTypes.iterator().next() is MyType)
}
}
#Michael comment is working. If you want to do the injection in constructor, you need do something like
class MainController #Inject consturctor(
private var componentDescriptors: List<#JvmSuppressWildcards ComponentDescriptor>
) {}

Resources