Barrufet
Barrufet

Reputation: 682

Dagger hilt testing EntryPointAccessors

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

Answers (2)

Bruce
Bruce

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

muetzenflo
muetzenflo

Reputation: 5700

You are facing two different issues:

  1. Using Hilt in an UnitTest
  2. Need to inject an applicationContext in a UnitTest

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

  1. add the following gradle dependencies:
androidTestImplementation "com.google.dagger:hilt-android-testing:2.38.1" 
kaptAndroidTest "com.google.dagger:hilt-android-comiler:2.38.1"
  1. To use Hilt, you need to swap out the 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)
    }
}
  1. To tell gradle about the new test runner, open your /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

Related Questions