Blog Infos
Author
Published
Topics
,
Published

Testing is an essential part of the software development process. A unit test generally exercises the functionality of the smallest possible unit of code (which could be a method or class) in a repeatable way. You should add unit tests when you need to verify the logic of a specific code in your app.

The scope of this article is to explain the testing of functions with listeners and functions taking lambda as arguments. If you want to learn the basics of testing, there are some good tutorials available:

Note: In this article, mockk is used for mocks; the same concepts will also apply to other mocking tools.

Consider a function that has a listener like getCurrentLocation in the below gist.

class CurrentLocationCoroutineWrapperImpl(
private val fusedLocationProviderClient: FusedLocationProviderClient
) {
@RequiresPermission(
anyOf =
[
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
]
)
suspend fun getCurrentLocation(priority: Int) =
suspendCancellableCoroutine { cont ->
fusedLocationProviderClient.getCurrentLocation(
priority, null
).apply {
addOnSuccessListener { location: Location? ->
if (location == null) {
cont.resumeWithException(LocationNotFoundException())
} else {
cont.resume(location)
}
}
}
}
}

The unit test for the getCurrentLocation function in which the location returned successfully (i.e. the addOnSuccessListener gets called) is :

class CurrentLocationCoroutineWrapperImplTest {
private val fusedLocationProviderClient: FusedLocationProviderClient = mockk(relaxed = true)
private lateinit var currentLocationCoroutineWrapperImpl: CurrentLocationCoroutineWrapperImpl
@Before
fun before() {
currentLocationCoroutineWrapperImpl =
CurrentLocationCoroutineWrapperImpl(fusedLocationProviderClient)
}
@Test
fun `getCurrentLocation with locationPriority success`() =
runTest {
val locationTaskMock: Task<Location> = mockk(relaxed = true)
val locationMock: Location = mockk()
every {
fusedLocationProviderClient.getCurrentLocation(
Priority.PRIORITY_HIGH_ACCURACY,
any()
)
} returns locationTaskMock
val slot = slot<OnSuccessListener<Location>>()
every { locationTaskMock.addOnSuccessListener(capture(slot)) } answers {
slot.captured.onSuccess(locationTaskMock.result)
locationTaskMock
}
every { locationTaskMock.result } returns locationMock
val result =
currentLocationCoroutineWrapperImpl.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY)
assertEquals(locationMock, result)
verify {
fusedLocationProviderClient.getCurrentLocation(
Priority.PRIORITY_HIGH_ACCURACY,
any()
)
}
}
}

Let’s understand the test. whenever getCurrentLocation will be called, addOnSuccessListener should be invoked, to do that we have to use argument captor.

ArgumentCaptor allows us to capture an argument passed to a method to inspect it. This is especially useful when we can’t access the argument outside of the method we’d like to test.

There are two ways to capture arguments: using CapturingSlot<TYPE> and using MutableListOf<TYPE>.

CapturingSlot allows to capture only one value, for our case, this will work if you have multiple values you can use MutableListOf<TYPE>.

val slot = slot<OnSuccessListener<Location>>()
every { locationTaskMock.addOnSuccessListener(capture(slot))}
answers { slot.captured.onSuccess(locationTaskMock.result)                       locationTaskMock           
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Intro to unit testing coroutines with Kotest & MockK

In this workshop, you’ll learn how to test coroutines effectively using the Kotest and MockK libraries, ensuring your app handles concurrent tasks efficiently and with confidence.
Watch Video

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michalik
Kotlin GDE

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michalik
Kotlin GDE

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michali ...
Kotlin GDE

Jobs

This creates a slot and a mock and set expected behavior following way: in case locationTaskMock.addOnSuccessListener is called, then OnSuccessListener is captured to the slot locationTaskMock.result is answered.

val result =                currentLocationCoroutineWrapperImpl.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY)

Now, this function can be asserted easily.

Let’s take another example wherein a function that takes a higher-order function as an argument like connect(connectionListener: (isConnected: Boolean) -> Unit)

class HighOrderDemo @Inject constructor(
private val connectRemote: ConnectRemote,
) {
fun connect(connectionListener: (isConnected: Boolean) -> Unit) {
try {
connectRemote.connect{
connectionListener.invoke(it)
}
} catch (ex: Exception) {
connectionListener.invoke(false)
}
}
}

Unit test for the above function connect is:

class HighOrderDemoTest {
private lateinit var highOrderDemo: HighOrderDemo
private var connectRemote: ConnectRemote = mockk(relaxed = true)
@Before
fun before() {
highOrderDemo = HighOrderDemo(
connectRemote
)
}
@Test
fun `test successful connection`() {
every { connectRemote.connect(captureLambda<(isConnected: Boolean) -> Unit>()) } answers {
lambda<(isConnected: Boolean) -> Unit>().captured.invoke(true)
}
highOrderDemo.connect {
assertEquals(it, true)
}
}
}
every { 
connectRemote
.connect(captureLambda<(isConnected: Boolean) -> Unit>()) } 
answers {            
lambda<(isConnected: Boolean) -> Unit>().captured.invoke(true)        }

Here also, in case connectRemote.connect is called then connectionListener: (isConnected: Boolean) -> Unit lambda is captured and true is returned.

then highOrderDemo.connect can be asserted via

highOrderDemo.connect { assertEquals(it, true) }

Update:

We can write tests for the above two methods without argument captor as well in the following way:

  • Listener
every { locationTaskMock.addOnSuccessListener(any()) } answers {
    firstArg<OnSuccessListener<Location>>().onSuccess(locationTaskMock.result)
    locationTaskMock
}
  • Higher-order function
every { connectRemote.connect(any()) } answers {
    firstArg<(isConnected: Boolean) -> Unit>().invoke(true)
}

That’s it for now, I hope it helps! Enjoy and feel free to leave a comment if something is not clear or if you have questions. Thank you for reading! 🙌🙏

This article was originally published on proandroiddev.com on October 31, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu