Reputation: 682
Hi I have a Class that implements an abstraction with a parameter of type Foo and is being provided through an EntryPointAccessors directly into the abstract class. Something like this
SomethingImportantBuilderImpl(
...
): AbstractSomethingImportantBuilder(
foo: Foo = EntryPointAccessors.fromApplication(applicationContext, FooEntryPoint::class.java).provideFoo()
)
Everything is working as expected but now how do I provide the Foo through EntryPointAccessor to the SomethingImportantBuilder in a unit test?
Everytime test is ran it says the next:
Could not find an Application in the given context: null
java.lang.IllegalStateException: Could not find an Application in the given context: null
at dagger.hilt.android.internal.Contexts.getApplication(Contexts.java:42)
at dagger.hilt.android.EntryPointAccessors.fromApplication(EntryPointAccessors.java:37)
Any help/suggestion? How can I provide this dependency in unit test? Also using Mockk.
Edit 1: If I try to mock it like this:
every { EntryPointAccessors.fromApplication(any(), FooEntryPoint::class.java).provideFoo() } returns mockk(relaxed = true)
throws the next error: android.content.Context.getApplicationContext()Landroid/content/Context;
Edit 2: Tried to use Mockk so I mock the FooEntryPoint interface where hilt implements under the hood this entry point in a Singleton/Context interface. not working as it doesn't initialize the context:
private lateinit var context: Application
@Before
fun setup(){
context = mockk(moreInterfaces = arrayOf(FooEntryPoint::class, GeneratedComponent::class), relaxed = true)
}
@Test
fun useContext(){
println(context) //using context throws the error saying that lateinit var is not initialized.
}
Error lateinit property context has not been initialized kotlin.UninitializedPropertyAccessException: lateinit property context has not been initialized
Still the same problem. How do I provide the applicationContext for the EntryPoint?
Upvotes: 2
Views: 2373
Reputation: 2584
I am able to do it in unit test and not instrumentation test. In this case you need to mock applicationContext
and EntryPointAccessors.fromApplication
You didn't post your whole code so not sure where your applicationContext came from, suppose you are using dependency injection from the constructor, you need to pass in a mocked applicationContext
instance built using mockk<Context>()
After that, the key to mock EntryPointAccessors
is to use mockkStatic
mockkStatic(EntryPointAccessors::class)
every { EntryPointAccessors.fromApplication(any(), FooEntryPoint::class.java) } returns mockk(relaxed = true) {
every { provideFoo() } returns mockk() // Further provide customized 'Foo' instance if needed
}
and you should be good to go.
Upvotes: 3
Reputation: 5700
You are facing two different issues:
ad 1) I am 90% sure that you cannot use Hilt in UnitTests, but I will happily update this answer if someone provides differnt information. Why is it not possible to use Hilt in UnitTests? For the same reason as number 2 (see below), since Hilt is tightly coupled to the AndroidFramework.
ad 2) This is definitely not possible and actually the main difference between UnitTests and instrumentation tests on Android: The latter are running within an Android environment (real device, emulator, etc.). Because of that it is possible to test components of your code that are using Android classes. Most famously ApplicationContext
, Context
, Activity
and Fragment
. UnitTests on the other hand are raw logic-tests and unrelated to any Android framework components. No Android class is needed for UnitTest. No Android class is possible for UnitTests.
So in your case, you want to use Hilt for a test that needs the ApplicationContext injected. Both are strong indicators that you have to do that in an instrumentation test. Here is a step-by-step instruction how to setup hilt for instrumentation tests as described in this video:
https://www.youtube.com/watch?v=nOp_CEP_EjM
androidTestImplementation "com.google.dagger:hilt-android-testing:2.38.1"
kaptAndroidTest "com.google.dagger:hilt-android-comiler:2.38.1"
AndroidJUnitRunner
with a small custom Runner. To do so, create this HiltTestRunner
:import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
/app/build.gradle
and exchange this line:testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
with the class reference of the HiltTestRunner
you just created:
testInstrumentationRunner "your.package.HiltTestRunner"
That's it basically. Now you can create a new Module in your test folders just like you would do in the normal app folder. For example:
@Module
@InstallIn(SingletonComponent::class)
class TestDatabaseModule {
@Provides
@Named("test_database")
fun provideDatabase(@ApplicationContext applicationContext: Context): MyDatabase {
return Room.inMemoryDatabaseBuilder(applicationContext, MyDatabase::class.java)
.allowMainThreadQueries()
.build()
}
}
To inject this database in your TestCode, you need to create a HiltAndroidRule
and call hiltRule.inject()
in the @Before
method:
@HiltAndroidTest
class UserDaoTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@get:Rule
val testCoroutineRule = TestCoroutineRule()
@Inject
lateinit var database: MyDatabase
private lateinit var sut: UserDao
@Before
fun createDb() {
hiltRule.inject()
sut = database.userDao()
}
@After
@Throws(IOException::class)
fun closeDb() {
database.clearAllTables()
database.close()
}
@Test
@Throws(Exception::class)
fun insertAndGetUserFromDb_success() = testCoroutineRule.runBlockingTest {
// Given a database with 1 user
val newUser = UserFakes.get()
sut.insert(newUser)
// When fetching the user
sut.get().test {
// Then the user fields should match
val user = awaitItem()
assert(user.id == newUser.id)
assert(user.name == newUser.name)
}
}
}
Additionsl treat: I am using Turbine to simplify testing Flows. This is the part in the code above where I call .test {}
on a Flow and awaitItem()
to wait for the result of the Flow. See the Turbine readme.md for more information.
Upvotes: 0