I'm trying to create this MockController with mockk to avoid create a new class for testing.
Is possible to do that?
class MockController : IController {
override lateinit var output: (String) -> Unit
override fun start() {
output("OK")
}
}
Class to test:
class ClassToTest(
private val controller: IController,
private val output: (String) -> Unit
){
fun start() {
controller.output = { result ->
output(result)
}
controller.start()
}
}
Then I use like this TEST example:
#Test
fun checkOutputIsCalled() {
runBlocking {
var outputCalled = false
val outputClassToTest: (String) -> Unit = {
outputCalled = true
}
val classToTest = ClassToTest(MockController(), outputClassToTest)
classToTest.start()
delay(1000)
assert(outputCalled)
}
}
I'm trying to update:
#Test
fun checkOutputIsCalled() {
runBlocking {
val controller = spyk<IController>()
var outputCalled = false
val outputClassToTest: (String) -> Unit = {
outputCalled = true
}
val classToTest = ClassToTest(controller, outputClassToTest)
every { controller.start() } answers {
controller.output.invoke("OK")
} //When I execute the test, output is null because yet doesn't exist the output creted inside ClassToTest
classToTest.start()
delay(1000)
assert(outputCalled)
}
}
When I execute the test, output is null because yet doesn't exist the output creted inside ClassToTest
How this could be after the output assign?
Thanks!
You should mock your output object and your Controller. Once done, tell your mocked controller to return the mocked output when property is called. Right after the start() invocation you can verify that output lambda was invoked. Please note that all your mocks must be relaxed.
class ClassToTestTesting {
companion object {
const val INVOCATION_PARAM = "OK"
}
#Test
fun test() = runBlocking {
val paramOutput: (String) -> Unit = mockk(relaxed = true)
val controller: IController = mockk(relaxed = true) {
every { output } returns paramOutput
every { start() } answers { output.invoke(INVOCATION_PARAM) }
}
val classToTest = ClassToTest(
controller,
paramOutput
)
classToTest.start()
verify { paramOutput(INVOCATION_PARAM) }
}
}
Related
So I have a ViewModel I'm trying to unit test. It is using the stateIn operator. I found this documentation about how to test stateflows created using the stateIn operator https://developer.android.com/kotlin/flow/test but the mapLatest never triggers even though I'm collecting the flow.
class DeviceConfigurationViewModel(
val systemDetails: SystemDetails,
val step: AddDeviceStep.ConfigureDeviceStep,
val service: DeviceRemoteService
) : ViewModel(), DeviceConfigurationModel {
#OptIn(ExperimentalCoroutinesApi::class)
private val _state: StateFlow<DeviceConfigurationModel.State> =
service.state
.mapLatest { state ->
when (state) {
DeviceRemoteService.State.Connecting -> {
DeviceConfigurationModel.State.Connecting
}
is DeviceRemoteService.State.ConnectedState.Connected -> {
state.sendCommand(step.toCommand(systemDetails))
DeviceConfigurationModel.State.Connected
}
is DeviceRemoteService.State.ConnectedState.CommandSent -> {
DeviceConfigurationModel.State.Configuring
}
is DeviceRemoteService.State.ConnectedState.MessageReceived -> {
transformMessage(state)
}
is DeviceRemoteService.State.Disconnected -> {
transformDisconnected(state)
}
}
}
.distinctUntilChanged()
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000), // Keep it alive for a bit if the app is backgrounded
DeviceConfigurationModel.State.Disconnected
)
override val state: StateFlow<DeviceConfigurationModel.State>
get() = _state
private fun transformDisconnected(
state: DeviceRemoteService.State.Disconnected
): DeviceConfigurationModel.State {
return if (state.hasCause) {
DeviceConfigurationModel.State.UnableToConnect(state)
} else {
state.connect()
DeviceConfigurationModel.State.Connecting
}
}
private fun transformMessage(state: DeviceRemoteService.State.ConnectedState.MessageReceived): DeviceConfigurationModel.State {
return when (val message = state.message) {
is Message.AddedToProject -> DeviceConfigurationModel.State.Configured
is Message.ConfigWifiMessage -> {
if (!message.values.success) {
DeviceConfigurationModel.State.Error(
message.values.errorCode,
state,
step.toCommand(systemDetails)
)
} else {
DeviceConfigurationModel.State.Configuring
}
}
}
}
}
And here's my unit test. The mapLatest never seems to get triggered even though I'm collecting the flow. I'm using the suggestions here https://developer.android.com/kotlin/flow/test
#OptIn(ExperimentalCoroutinesApi::class)
class DeviceConfigurationViewModelTest {
private val disconnectedService = mock<DisconnectedService>()
private val deviceServiceState: MutableStateFlow<DeviceRemoteService.State> =
MutableStateFlow(DeviceRemoteService.State.Disconnected(disconnectedService, Exception()))
private val deviceService = mock<DeviceRemoteService> {
on { state } doReturn deviceServiceState
}
private val systemDetails = mock<SystemDetails> {
on { controllerAddress } doReturn "192.168.1.112"
on { controllerName } doReturn "000FFF962FE7"
}
private val step = AddDeviceDeviceStep.ConfigureDeviceStep(
44,
"Thou Shalt Not Covet Thy Neighbor’s Wifi",
"testing616"
)
private lateinit var viewModel: DeviceConfigurationViewModel
#Before
fun setup() {
viewModel = DeviceConfigurationViewModel(systemDetails, step, deviceService)
}
#Test
fun testDeviceServiceDisconnectWithCauseMapsToUnableToConnect() =
runTest {
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.state.collect() }
deviceServiceState.emit(
DeviceRemoteService.State.Disconnected(Exception("Something bad happened"))
)
assertThat(viewModel.state.value).isInstanceOf(DeviceConfigurationModel.State.UnableToConnect::class.java)
collectJob.cancel()
}
}
I believe this is happening because the viewModelScope uses a hardcoded Main dispatcher under the hood.
You can follow the instructions here in the Android documentation to see how you can to set the Main dispatcher for tests.
I have a function that return flow by emitAll
fun handle(actions: MoviesActions): Flow<MoviesStates> = flow {
when (actions) {
is MoviesActions.LoadMovies -> {
emit(MoviesStates.Loading)
emitAll(moviesUseCase.execute())
}
}
}
And this the use case function
suspend fun execute(): Flow<MoviesStates> = flow {
combine(f1, f2) { state1: MoviesStates, state2: MoviesStates ->
// some code
}.collect {
emit(it)
}
}
No problem in testing the first emission MoviesStates.Loading, the problem is when I try to test the flow which return from usecase by emitAll emitAll(moviesUseCase.execute()), the test fails and I got this result
java.util.NoSuchElementException: Expected at least one element
this is my unit test
#Test
fun testLoadMovies() = runBlocking {
whenever(useCase.execute()).thenReturn(flow {
MoviesStates.EmptyList
})
val actual = viewModel.handle(MoviesActions.LoadMovies).drop(1).first()
val expected = MoviesStates.EmptyList
assertEquals(actual, expected)
}
So How can I test it correctly?
Thanks to gpunto , this is the solution he suggested
#Test
fun testLoadMovies() = runTest {
whenever(useCase.execute()).thenReturn(flow {
MoviesStates.EmptyList
})
useCase.execute().collectLatest { states ->
val actual = viewModel.handle(MoviesActions.LoadMovies).drop(1).first()
val expected = states
assertEquals(expected, actual)
}
}
I'm trying to insert separators to my list using the paging 3 compose library however, insertSeparators doesn't seem to indicate when we are at the beginning or end. My expectations are that before will be null at the beginning while after will be null at the end of the list. But it's never null thus hard to know when we are at the beginning or end. Here is the code:
private val filterPreferences =
MutableStateFlow(HomePreferences.FilterPreferences())
val games: Flow<PagingData<GameModel>> = filterPreferences.flatMapLatest {
useCase.execute(it)
}.map { pagingData ->
pagingData.map { GameModel.GameItem(it) }
}.map {
it.insertSeparators {before,after->
if (after == null) {
return#insertSeparators null
}
if (before == null) {
Log.i(TAG, "before is null: ") // never reach here
return#insertSeparators GameModel.SeparatorItem("title")
}
if(condition) {
GameModel.SeparatorItem("title")
}
else null
}
}
.cachedIn(viewModelScope)
GamesUseCase
class GamesUseCase #Inject constructor(
private val executionThread: PostExecutionThread,
private val repo: GamesRepo,
) : FlowUseCase<HomePreferences, PagingData<Game>>() {
override val dispatcher: CoroutineDispatcher
get() = executionThread.io
override fun execute(params: HomePreferences?): Flow<PagingData<Game>> {
val preferences = params as HomePreferences.FilterPreferences
preferences.apply {
return repo.fetchGames(query,
parentPlatforms,
platforms,
stores,
developers,
genres,
tags)
}
}
}
FlowUseCase
abstract class FlowUseCase<in Params, out T>() {
abstract val dispatcher: CoroutineDispatcher
abstract fun execute(params: Params? = null): Flow<T>
operator fun invoke(params: Params? = null) = execute(params).flowOn(dispatcher)
}
Here is the dependency :
object Pagination {
object Version {
const val pagingCompose = "1.0.0-alpha14"
}
const val pagingCompose = "androidx.paging:paging-compose:${Version.pagingCompose}"
}
I'm assuming that filterPreferences gives you Flow of some preference and useCase.execute returns Flow<PagingData<Model>>, correct?
I believe that the problem is in usage of flatMapLatest - it mixes page events of multiple useCase.execute calls together.
You should do something like this:
val games: Flow<Flow<PagingData<GameModel>>> = filterPreferences.mapLatest {
useCase.execute(it)
}.mapLatest {
it.map { pagingData -> pagingData.map { GameModel.GameItem(it) } }
}.mapLatest {
it.map { pagingData ->
pagingData.insertSeparators { before, after -> ... }
} // .cachedIn(viewModelScope)
}
This same structure works for us very well. I'm only not sure how cachedIn will work here, we are using a different caching mechanism, but you can try.
Let's say I have a composable like this which I want to test
#Composable
fun HelloContent(sayHello: () -> Unit) {
Button(text = "Hello World", onClick = sayHello)
}
How to test if sayHello was called from compose test. I tried using mockk but that didn't seem to work
#Test
fun check_if_sayHello_was_called() {
composeTestRule.setContent {
HelloContent(sayHello = mockk())
}
composeTestRule.onNodeWithText("Hello World").performClick()
// How to test if sayHello was called
}
#Test
fun check_if_sayHello_was_called() {
var wasCalled = false
composeTestRule.setContent {
HelloContent(sayHello = { wasCalled = true })
}
composeTestRule.onNodeWithText("Hello World").assertHasClickAction()
composeTestRule.onNodeWithText("Hello World").performClick()
assert(wasCalled)
}
You can use the sayHello: () -> Unit lambda to test a boolean.
I am trying to unit test the kotlin coroutines. My project is following MVP pattern where the coroutines are used in the presenter like this:
fun authenticateWithUserAndPassword(usernameOrEmail: String, password: String) {
launchUI(strategy) {
view.showLoading()
try {
val token = retryIO("login") {
when {
settings.isLdapAuthenticationEnabled() ->
client.loginWithLdap(usernameOrEmail, password)
usernameOrEmail.isEmail() ->
client.loginWithEmail(usernameOrEmail, password)
else ->
client.login(usernameOrEmail, password)
}
}
val myself = retryIO("me()") { client.me() }
myself.username?.let { username ->
val user = User(
id = myself.id,
roles = myself.roles,
status = myself.status,
name = myself.name,
emails = myself.emails?.map { Email(it.address ?: "", it.verified) },
username = username,
utcOffset = myself.utcOffset
)
localRepository.saveCurrentUser(currentServer, user)
saveCurrentServer.save(currentServer)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, username)
saveAccount(username)
saveToken(token)
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword,
true
)
view.saveSmartLockCredentials(usernameOrEmail, password)
navigator.toChatList()
}
} catch (exception: RocketChatException) {
when (exception) {
is RocketChatTwoFactorException -> {
navigator.toTwoFA(usernameOrEmail, password)
}
else -> {
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword,
false
)
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
} finally {
view.hideLoading()
}
}
}
Here launchUI is defined as
/**
* Launches a coroutine on the UI context.
*
* #param strategy a CancelStrategy for canceling the coroutine job
*/
fun launchUI(strategy: CancelStrategy, block: suspend CoroutineScope.() -> Unit): Job =
MainScope().launch(context = strategy.jobs, block = block)
CancelStrategy class
class CancelStrategy(owner: LifecycleOwner, val jobs: Job) : LifecycleObserver {
init {
owner.lifecycle.addObserver(this)
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
jobs.cancel()
}
}
I am new to coroutine unit testing and tried writing a basic unit test
#Test
fun check_authentication_of_user() = runBlocking {
loginPresenter.authenticateWithUserAndPassword(USERNAME, PASSWORD)
verify(navigator).toChatList()
}
On running the test I am getting the following error.
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlinx.coroutines.BuildersKt__Builders_commonKt.launch, parameter context
Any help regarding how to unit test the given method will be appreciated.