I am trying to implement instrumented test for my custom Work Manager which uses Hilt's #AssistedInject.
My Work Manager performs perfect in an app but when I am trying to test it according to Google's work manager integration test guide I'm getting a error:
WM-WorkerFactory: java.lang.NoSuchMethodException: com.android.wmapp.data.SyncWorker. [class android.content.Context, class androidx.work.WorkerParameters]
I've turned off default initialization in AndroidManifest.xml:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
Implemented Configuration.Provider in my Application class:
#HiltAndroidApp
class MyApplication : Application(), Configuration.Provider {
#Inject
lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder().setWorkerFactory(workerFactory)
.setMinimumLoggingLevel(android.util.Log.DEBUG).build()
}
}
Set up my Worker class:
#HiltWorker
class SyncWorker #AssistedInject constructor(
#Assisted applicationContext: Context,
#Assisted workerParams: WorkerParameters,
private val someRepository: SomeRepository,
private val dispatchers: Dispatchers,
) : CoroutineWorker(applicationContext, workerParams) {
override suspend fun doWork(): Result {
val result = withContext(dispatchers.IO) {
someRepository.synchronize()
}
return if (result) Result.success() else Result.retry()
}
}
My library's test build.gradle configuration:
// Test
testImplementation 'junit:junit:4.+'
androidTestImplementation "androidx.work:work-testing:2.7.1"
androidTestImplementation "com.google.dagger:hilt-android-testing:2.43"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:2.43"
kaptAndroidTest 'com.google.dagger:hilt-compiler:2.43'
kaptAndroidTest 'androidx.hilt:hilt-compiler:1.0.0'
androidTestImplementation "androidx.test:runner:1.4.0"
androidTestImplementation "androidx.test:rules:1.4.0"
androidTestImplementation 'androidx.hilt:hilt-work:1.0.0'
implementation 'androidx.test.ext:junit-ktx:1.1.3'
My instrumented test:
#HiltAndroidTest
class SyncWorkerTest {
#get:Rule
val hiltRule = HiltAndroidRule(this)
private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
#Before
fun setUp() {
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
#Test
fun testDoWork() {
val request = SyncWorker.startSyncJob()
val workManager = WorkManager.getInstance(context)
val testDriver = WorkManagerTestInitHelper.getTestDriver(context)!!
workManager.enqueueUniqueWork(
SyncWorker.SyncWorkName,
ExistingWorkPolicy.REPLACE,
request,
)
val preRunWorkInfo = workManager.getWorkInfoById(request.id).get()
Assert.assertEquals(WorkInfo.State.ENQUEUED, preRunWorkInfo.state)
testDriver.setAllConstraintsMet(request.id)
}
}
Implemented my own JUnitRunner and specified it in my module's build.gradle:
class YALTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
testInstrumentationRunner "com.android.wmapp.YALTestRunner"
What have I missed with my WorkManager's test implementation?
I think you have the wrong config in the manifets. Please check here. It should be a bit different for version 2.6 and later:
https://developer.android.com/topic/libraries/architecture/workmanager/advanced/custom-configuration#remove-default
I would say that the issue might be caused by missing HiltWorkerFactory in configuration object provided to initializeTestWorkManager method. Please try to modify your instrumented test:
#HiltAndroidTest
class SyncWorkerTest {
#Inject
lateinit var workerFactory: HiltWorkerFactory
#get:Rule
val hiltRule = HiltAndroidRule(this)
private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
#Before
fun setUp() {
hiltRule.inject()
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.setWorkerFactory(workerFactory)
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
#Test
fun testDoWork() {
val request = SyncWorker.startSyncJob()
val workManager = WorkManager.getInstance(context)
val testDriver = WorkManagerTestInitHelper.getTestDriver(context)!!
workManager.enqueueUniqueWork(
SyncWorker.SyncWorkName,
ExistingWorkPolicy.REPLACE,
request,
)
val preRunWorkInfo = workManager.getWorkInfoById(request.id).get()
Assert.assertEquals(WorkInfo.State.ENQUEUED, preRunWorkInfo.state)
testDriver.setAllConstraintsMet(request.id)
}
}
Related
I'm facing a problem with work manager. I want to pass a dao to work manager constructor and follow all the implementations but can't access the dao.
java.lang.NoSuchMethodException: com.itmedicus.pdmderma.worker.DermaWorker. [class android.content.Context, class androidx.work.WorkerParameters]
Worker Class
#HiltWorker
class DermaWorker #AssistedInject constructor(
#Assisted appContext: Context,
#Assisted params: WorkerParameters,
private val dermaDao: DermaDao
): CoroutineWorker ( appContext,params)
{
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
try {
DamiData.addDermaList()
val list = DamiData.dermaList
for (derma in list) {
dermaDao.insertDermaContent(derma)
}
Result.success()
} catch (ex: Exception) {
Result.failure()
}
}
}
Application Class
#HiltAndroidApp
class PdmDerma : Application(), Configuration.Provider {
#Inject
lateinit var workerFactory: HiltWorkerFactory
companion object {
lateinit var appContext: Context
}
override fun onCreate() {
super.onCreate()
appContext = applicationContext
plant(Timber.DebugTree())
}
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder().setWorkerFactory(workerFactory)
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
}
}
Manifest
<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>
HiltWorkerModule
#Module
#InstallIn(SingletonComponent::class)
object WorkManagerInitializer : Initializer<WorkManager> {
#Provides
#Singleton
override fun create(#ApplicationContext context: Context): WorkManager {
val configuration = Configuration.Builder().build()
WorkManager.initialize(context, configuration)
return WorkManager.getInstance(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> {
// No dependencies on other libraries.
return emptyList()
}
}
Build.gradle
//Dagger Hilt
implementation 'com.google.dagger:hilt-android:2.42'
kapt 'com.google.dagger:hilt-compiler:2.42'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'androidx.hilt:hilt-work:1.0.0'
implementation "androidx.startup:startup-runtime:1.1.1"
// work manager
implementation "androidx.work:work-runtime-ktx:2.7.1"
Where is your factory class? Like this?
#AssistedFactory
interface Factory {
fun create(appContext: Context, params: WorkerParameters, dermaDao: DermaDao): DermaWorker
}
}
Yes.I solved the issue. I'm trying to init workmanager from a viewmodel.In the viewmodel constructor, I'm passing private val workManager : WorkManager.But in this process, I can't init the workmanager.So I'm keeping the AndroidViewModel for context & manually creating the workmanager dependency. Here is my solving code.
#HiltViewModel
class DermaViewModel #Inject constructor(
private val dermaRepository: DermaRepository,
application
) : AndroidViewModel(context) {
init {
initWorkManager()
}
private fun initWorkManager() {
val request: WorkRequest = OneTimeWorkRequestBuilder<DermaWorker>()
.build()
val workManager = WorkManager.getInstance(context)
workManager.enqueue(request)
}
}
I would like to start using Hilt in my test.
Gradle:
android{
...
defaultConfig{
...
testInstrumentationRunner "com.rachapps.myfitapp.HiltTestRunner"
}
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.28-alpha'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.38.1'
...
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
test class:
#SmallTest
#HiltAndroidTest
class BodyPartDaoTest {
#get : Rule
var hiltRule = HiltAndroidRule(this)
#get : Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#Inject
#Named("test_db")
lateinit var database: MyFitDatabase
private lateinit var bodyPartDao: BodyPartDao
private lateinit var exerciseDao: ExerciseDao
#Before
fun setup() {
hiltRule.inject()
bodyPartDao = database.bodyPartDao()
exerciseDao = database.exerciseDao()
}
...
}
Module:
#Module
#InstallIn(SingletonComponent::class)
object TestAppModule {
#Provides
#Named("test_db")
fun provideInMemoryDb(#ApplicationContext context: Context) : MyFitDatabase {
return Room.inMemoryDatabaseBuilder(context, MyFitDatabase::class.java).allowMainThreadQueries().build()
}
}
While executing test I receive an error:
C:\Radek\Android\MyFitApp\app\build\generated\source\kapt\debugAndroidTest\com\rachapps\myfitapp\data\dao\BodyPartDaoTest_TestComponentDataSupplier.java:14: error: BodyPartDaoTest_TestComponentDataSupplier is not abstract and does not override abstract method get() in TestComponentDataSupplier
public final class BodyPartDaoTest_TestComponentDataSupplier extends TestComponentDataSupplier {
^
C:\Radek\Android\MyFitApp\app\build\generated\source\kapt\debugAndroidTest\com\rachapps\myfitapp\data\dao\BodyPartDaoTest_TestComponentDataSupplier.java:15: error: get() in BodyPartDaoTest_TestComponentDataSupplier cannot override get() in TestComponentDataSupplier
protected TestComponentData get() {
^
return type TestComponentData is not compatible with Map<Class<?>,TestComponentData>
2 errors
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:compileDebugAndroidTestJavaWithJavac'.
I am not sure as to what is the reason for the exception, but the way you are providing in-memory DB to the test, is not what the documentation says you should do.
Try replacing the binding, and see what happens.
Try with keeping Hilt library versions same
//Hilt testing
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.37'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.37'
//hilt dependency
kapt "com.google.dagger:hilt-compiler:2.37"
implementation("com.google.dagger:hilt-android:2.37")
I had that problem because of outdated library: com.google.dagger:hilt-android-testing
I am trying to test Worker but i can't inject it
Error while gradle building:
Dagger does not support injecting #AssistedInject type,
com.hussien.quoty.sync.NotificationWorker. Did you mean to inject its assisted factory type instead?
public com.hussien.quoty.sync.NotificationWorker notificationWorker;
Test Class
#ExperimentalCoroutinesApi
#MediumTest
#RunWith(AndroidJUnit4::class)
#HiltAndroidTest
class NotificationWorkerTest {
private lateinit var notificationManager: NotificationManager
#get:Rule
var hiltRule = HiltAndroidRule(this)
//NOTE: HERE AM TRYING TO INJECT IT
#Inject
lateinit var notificationWorker: NotificationWorker
#Before
fun setUp() {
hiltRule.inject()
val context = ApplicationProvider.getApplicationContext<Context>()
notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
#Test
fun doWork() = runTest {
notificationWorker.doWork()
assertThat(
notificationManager.activeNotifications.map { it.id },
hasItem(NotificationUtils.QUOTES_NOTIFICATION_ID)
)
}
}
Worker Class
#HiltWorker
class NotificationWorker #AssistedInject constructor(
#Assisted context: Context,
#Assisted workerParams: WorkerParameters,
private val repository: QuotesRepository,
private val settingsDataStoreManager: SettingsDataStoreManager,
private val notificationUtils: NotificationUtils,
): CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val settings = settingsDataStoreManager.settingsPreferencesFlow.first()
val randomQuote = repository.getRandomQuote(settings.tags, settings.languages)
notificationUtils.sendQuoteNotification(randomQuote)
return Result.success()
}
companion object {
const val TAG = "notification_worker"
}
}
When I try to run an ActivityScenario in my application that contains a WorkManager I get the following error on start:
java.lang.IllegalStateException: WorkManager is not initialized properly. You have explicitly disabled WorkManagerInitializer in your manifest, have not manually called WorkManager#initialize at this point, and your Application does not implement Configuration.Provider.
Using the WorkManagerTestInitHelper from the work-test artifact doesnt help either.
The WorkManager is defined like this:
#Provides
#Singleton
fun provideWorkmanager(#ApplicationContext context: Context) = WorkManager.getInstance(context)
This is my test atm:
#HiltAndroidTest
#RunWith(AndroidJUnit4::class)
class LoginTest {
#get:Rule(order = 0)
var hiltRule = HiltAndroidRule(this)
#get:Rule(order = 1)
val activityRule = ActivityScenarioRule(MainActivity::class.java)
#Before
fun before() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
#Test
fun test() {
...
}
}
This is because #get:Rule executes before the #Before does, as per the Google Documentation:
This rule provides functional testing of a single activity. The activity under test is launched before each test annotated with #Test and before any method annotated with #Before. It's terminated after the test is completed and all methods annotated with #After are finished. To access the activity under test in your test logic, provide a callback runnable to ActivityScenarioRule.getScenario().onActivity().
In order to fix this, you would need to initialise the WorkManager in the test with WorkManagerTestInitHelper before you try to launch the activity.
To do this, you should avoid using ActivityScenarioRule and use ActivityScenario instead, you can do something like this:
#HiltAndroidTest
#RunWith(AndroidJUnit4::class)
class LoginTest {
private lateinit var scenario: ActivityScenario<MainActivity>
#get:Rule
var hiltRule = HiltAndroidRule(this)
#Before
fun before() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
scenario = launchActivity()
}
#Test
fun test() {
scenario.moveToState(Lifecycle.State.CREATED).onActivity {
activity -> // do some test with the activity
}
}
}
To take advantage of ActivitScenarioRule and ensure that the WorkManager is initialized first I created a custom JUnit rule.
class WorkManagerRule : TestRule {
override fun apply(base: Statement?, description: Description?): Statement {
return object : Statement() {
override fun evaluate() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
try {
base?.evaluate()
} finally {
Log.d("WorkManagerRule", "Do some teardown")
}
}
}
}
#RunWith(AndroidJUnit4::class)
#HiltAndroidTest
class MyTest {
#get:Rule(order = 0)
var hiltRule = HiltAndroidRule(this)
#get:Rule(order = 1)
var workMgrRule = WorkManagerRule()
#get:Rule(order = 2)
var activityRule = ActivityScenarioRule(MainActivity::class.java)
#Before
fun init() {
hiltRule.inject()
}
//Your test code here...
}
I used the instructions for creating JUnit rules found here.
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