-
Notifications
You must be signed in to change notification settings - Fork 0
Async Testing β Testing normal functions that launch a coroutine
Devrath edited this page Oct 26, 2023
·
8 revisions
- Most of the time we come across scenarios where the system under test(
SUT
) is a normal function. - But this normal function launches a new co-routine and performs some action in an
async
way. - If the function was a
suspend
function, We could have used arun-blocking
way to test it on the test case so the test case does not finish until the suspend function finishes the execution. - This is more common where a
view
calls a function in aview-model
and theview-model
launches a co-routine. - Since the launched co-routine is independent we will not be able to use a run-blocking function.
ERROR OBSERVED
:->
Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
ERROR REASON
:-> The coroutines are to be run on main-dispatcher
, But the point to note here is that the main-dispatcher
is associated with main-thread-looper
, The main thread is associated with an Android device and the this is not available when unit tests are run, So we face this error,
ERROR SOLUTION
:-> We can use a test dispatcher
that replaces main dispatcher
- This block provides all the co-routines to be controlled from the
test-dispatcher
. - This block also helps to skip
delays()
that co-routine uses Since it's not needed in the test case
- This helps to ensure that the
test dispatcher
finishes before jumping to the next statement's possible assertions.
ProfileViewModel.kt
class ProfileViewModel(
private val repository: UserRepository,
savedStateHandle: SavedStateHandle
): ViewModel() {
private val userId = savedStateHandle.get<String>("userId")
private val _state = MutableStateFlow(ProfileState())
val state = _state.asStateFlow()
fun loadProfile() {
viewModelScope.launch {
userId?.let { id ->
_state.update { it.copy(isLoading = true) }
val result = repository.getProfile(id)
_state.update { it.copy(
profile = result.getOrNull(),
errorMessage = result.exceptionOrNull()?.message,
isLoading = false
) }
}
}
}
}
UserRepositoryFake.kt
class UserRepositoryFake : UserRepository {
var profile = profile()
var errorToReturn : Exception? = null
override suspend fun getProfile(userId: String): Result<Profile> {
return if(errorToReturn != null) {
Result.failure(errorToReturn!!)
} else {
Result.success(profile)
}
}
}
MainCoroutineExtension.kt
@OptIn(ExperimentalCoroutinesApi::class)
class MainCoroutineExtension(
val testDispatcher: TestDispatcher = StandardTestDispatcher()
): BeforeEachCallback, AfterEachCallback {
override fun beforeEach(context: ExtensionContext?) {
Dispatchers.setMain(testDispatcher)
}
override fun afterEach(context: ExtensionContext?) {
Dispatchers.resetMain()
}
}
ProfileViewModelTest.kt
@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(MainCoroutineExtension::class)
class ProfileViewModelTest {
// SUT: Current system under test
private lateinit var viewModel : ProfileViewModel
private lateinit var repository : UserRepositoryFake
@BeforeEach
fun setUp(){
repository = UserRepositoryFake()
viewModel = ProfileViewModel(
repository = repository,
savedStateHandle = SavedStateHandle(
initialState = mapOf(
"userId" to repository.profile.user.id
)
)
)
}
@Test
fun `Test loading profile success`() = runTest {
viewModel.loadProfile()
advanceUntilIdle()
assertThat(viewModel.state.value.profile).isEqualTo(repository.profile)
assertThat(viewModel.state.value.isLoading).isFalse()
}
@Test
fun `Test loading profile error`() = runTest {
repository.errorToReturn = Exception("Test exception")
viewModel.loadProfile()
advanceUntilIdle()
assertThat(viewModel.state.value.profile).isNull()
assertThat(viewModel.state.value.errorMessage).isEqualTo("Test exception")
assertThat(viewModel.state.value.isLoading).isFalse()
}
}