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:
- https://www.raywenderlich.com/195-android-unit-testing-with-mockito
- https://proandroiddev.com/understanding-unit-tests-for-android-in-2021-71984f370240
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
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



