julioribeiro
julioribeiro

Reputation: 1713

Android Testing Repository with mock and Coroutines returns NullPointerException

I kinda confused on how am I supposed to test this

I have this repository class:


class AuthenticationBox(
    private val dataMapper: AuthenticationDataMapper,
    private val dataSource: AuthenticationDataSource
) : BaseRepository<UserAuthentication>(), AuthenticationRepository {

override suspend fun fetchAuthenticationFromServer(
        username: String,
        password: String
    ): ResourceResult<UserAuthentication>? {
        var result: ResourceResult<UserAuthentication>? = null

        withContext(Dispatchers.IO) {
            try {
                val response = dataSource
                    .fetchAuthenticationFromServerAsync(username, password).await()

                if (response.hasErrors()) {
                    result =
                        ResourceResult.Error(ApolloException(response.errors()[0].message()))

                } else {
                    response.data()?.let {
                        val authorization = dataMapper.toDomain(it.loginResponse)

                        result = ResourceResult.Success(authorization)
                    }

                }

            } catch (error: ApolloException) {
                result = handleAuthenticationError(error)
            }
        }
        return result

    }


}

This line calls a datasource method that returns a Deferred<Response> object and awaits for it val response = dataSource.fetchAuthenticationFromServerAsync(username, password).await()

When I try to test it I always get a NullPointerException exactly on this line
This is my Test class:

class AuthenticationBoxTest {

    lateinit var repository: AuthenticationRepository

    var dataSourceMock: AuthenticationDataSource = mock()
    var dataMapperMock: AuthenticationDataMapper = mock()


   @Before
    fun setup() {
        repository = AuthenticationBox(
            dataMapper = dataMapperMock,
            dataSource = dataSourceMock
        )
    }

     @Test
    fun testFromServer() {
        val username = "username"
        val password = "password"
        runBlocking {
            repository.fetchAuthenticationFromServer(username, password)
        }
    }

}

The log output is:

java.lang.NullPointerException
    at br.com.ampli.authentication.repository.AuthenticationBox$fetchAuthenticationFromServer$2.invokeSuspend(AuthenticationBox.kt:45)
    at |b|b|b(Coroutine boundary.|b(|b)
    at br.com.ampli.authentication.repository.AuthenticationBox.fetchAuthenticationFromServer(AuthenticationBox.kt:42)
    at br.com.ampli.authentication.repository.AuthenticationBoxTest$testFromServer$1.invokeSuspend(AuthenticationBoxTest.kt:126)
Caused by: java.lang.NullPointerException
    at br.com.ampli.authentication.repository.AuthenticationBox$fetchAuthenticationFromServer$2.invokeSuspend(AuthenticationBox.kt:45)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:740)


All my other functions on this repository class is suspended functions and all uses withContext(Dispatchers.IO) and I am able to test them all(any of those calls another class). This is the only one that's giving me this error.

Thanks in advance for any insight on this.

Upvotes: 3

Views: 3225

Answers (2)

Javier Ant&#243;n
Javier Ant&#243;n

Reputation: 665

val deferred: Deferred = mock()

@Before
fun setup() {
    doNothing().whenever(deferred.await())
    whenever(dataSource.fetchAuthenticationFromServerAsync()).doReturn(deferred)

    repository = AuthenticationBox(
        dataMapper = dataMapperMock,
        dataSource = dataSourceMock
    )
}

Then in your test you can do things like:

@Test
fun testFromServer() {
    val username = "username"
    val password = "password"
    runBlocking {
        repository.fetchAuthenticationFromServer(username, password)
    }
    verify(dataSource).fetchAuthenticationFromServerAsync() // Verify fun called
}

You have to mock almost all the mocks behaviour. If I'm honest, I don't know if this is the intended behaviour of the mocks or is a bug in kotlin that does not automatically mock the internal functions.

Upvotes: 0

alaeri
alaeri

Reputation: 344

There might be an error with your mock(). Are you sure the datasource mock has an implementation that returns a deferred for the method?

.fetchAuthenticationFromServerAsync(username, password)

This medium post has an example with a mocked method returning a deferred which would give something like this for your use case:

//a small helper function first
fun <T> T.toDeferred() = GlobalScope.async { this@toDeferred }

val authResult = dummyAuthDataSourceResult  //dummy result
val mockedDatasource = mock<AuthenticationDataSource> {    
      on { fetchAuthenticationFromServerAsync() } doReturn authResult.toDeferred()
}

now you should be able to safely call:

mockedDatasource.fetchAuthenticationFromServerAsync().await()

Upvotes: 1

Related Questions