Hilt - Unit Tests - android

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

Related

Instrumented test

I am trying to run a test:
#HiltAndroidTest
class ActionDaoTest {
#get : Rule
var hiltRule = HiltAndroidRule(this)
#get : Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#Inject
#Named("test_db")
lateinit var database: MyDatabase
private lateinit var actionDao: ActionDao
#Before
fun setup() {
hiltRule.inject()
actionDao = database.actionDao()
}
#After
fun teardown(){
database.close()
}
#Test
fun insert_assetTrue() = runTest{
val action = ActionEntity("name","description", LocalDate.now())
actionDao.insert(action)
val actionList= actionDao.selectAll().first()
assertThat(actionList).contains(action)
}
}
I get an error:
java.lang.NoSuchMethodError: No virtual method find(Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/internal/ThreadSafeHeapNode; in class Lkotlinx/coroutines/internal/ThreadSafeHeap; or its super classes
I downgraded
'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
to
'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
and now it works...
The find method is defined in the coroutine-core module. In my case, I missed to add that module in my dependencies. As soon as I set it up like this, it worked:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'

Testing WorkManager with custom initializer and Hilt

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)
}
}

Android Room test with Hilt: UserDao cannot be provided without an #Provides-annotated method

I'm trying to test Room Database. For this I need an in memory instance of database. I'm using hilt for DI.
So, i have an app Module in app package:
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Singleton
#Provides
fun provideDatabase(
#ApplicationContext context: Context,
) = Room.databaseBuilder(
context.applicationContext,
UserDatabase::class.java,
"user"
).build()
#Singleton
#Provides
fun provideDao(db: UserDatabase) = db.getUserDao()
}
I have created TestRunner for Hilt and also added it in gradle.
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?,
): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
Here is gradle
defaultConfig {
applicationId "package"
minSdk 24
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner = "package.HiltTestRunner"
}
Here is my TestAppModule
#Module
#TestInstallIn(
components = [SingletonComponent::class],
replaces = [AppModule::class]
)
object TestAppModule {
#Provides
fun provideInMemoryDb(#ApplicationContext context: Context) =
Room.inMemoryDatabaseBuilder(
context,
UserDatabase::class.java
).allowMainThreadQueries().build()
}
And my test class
#OptIn(ExperimentalCoroutinesApi::class)
#SmallTest
#HiltAndroidTest
class UserDaoTest {
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
#get:Rule
var hiltRule = HiltAndroidRule(this)
#Inject
lateinit var database: UserDatabase
private lateinit var dao: UserDao
#Before
fun setup() {
hiltRule.inject()
dao = database.getUserDao()
}
#Test
fun insertUser() {
runTest {
val user = User(0, "login", "", "", "", 1)
dao.saveUser(user)
val dbUser = dao.getUser()
assertThat(dbUser, equalTo(user))
}
}
}
So, when I run the test I'm getting the error that UserDao cannot be provided without #Provides, but i'm not even injecting it in my test class. Can anyone please clarify this?
I noticed that it works just fine if change my TestAppModule like that:
#Module
#InstallIn(SingletonComponent::class)
object TestAppModule {
#Provides
#Named("test_db")
fun provideInMemoryDb(#ApplicationContext context: Context) =
Room.inMemoryDatabaseBuilder(
context,
UserDatabase::class.java
).allowMainThreadQueries().build()
}
Basically, here I'm not using TestInstallIn and added a named annotation to function.
It is because Hilt doesn’t know which database it should inject.
In your "AppModule" and "TestAppModule" you provide the database from the same class (UserDatabase ). So as you mentioned you should use #Named annotation to tell Hilt which database you want to be injected.

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:
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

Android Mockito kotlin.UninitializedPropertyAccessException: lateinit property dataManager has not been initialized

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

Categories

Resources