https://kotlinlang.org logo
#multiplatform
Title
# multiplatform
c

Chintan Soni

07/14/2020, 5:23 PM
Hello everyone,, Has anyone tried StateFlow in KMP? If yes, can someone guide me on how can I write Unit Test for StateFlow? I am following MVVM architecture and am trying to create a common ViewModel that can be used by any platform. In below example, I am trying to test
LoginViewModel
from its method
login
. LoginViewModel.kt:
Copy code
@ExperimentalCoroutinesApi
class LoginViewModel(private val loginViewState: LoginViewState, private val loginService: LoginService) {

    private val _apiStateFlow = MutableStateFlow(loginViewState)
    val stateFlow: StateFlow<LoginViewState> = _apiStateFlow

    private val _EMAIL_REGEX = "^[A-Za-z](.*)([@]{1})(.{1,})(\\.)(.{1,})"

    suspend fun login(email: String, password: String) {
        if (isFormValid(email, password)) {
            _apiStateFlow.value = loginViewState.copy(isLoginApiLoading = true)
            runCatching {
                loginService.login(email, password)
            }.onSuccess {
                loginViewState.copy(isLoginApiLoading = false, loginResponse = it)
            }.onFailure {
                loginViewState.copy(isLoginApiLoading = false, errorResponse = ErrorResponse(it.message.orEmpty()))
            }
        }
    }

    fun isFormValid(email: String, password: String): Boolean {
        val isEmailValid = email.isEmailValid()
        val isPasswordValid = password.isPasswordValid()
        _apiStateFlow.value = loginViewState.copy(isValidEmail = isEmailValid, isValidPassword = isPasswordValid)
        return isEmailValid && isPasswordValid
    }

    private fun String.isEmailValid() = _EMAIL_REGEX.toRegex().matches(this)
    private fun String.isPasswordValid() = length in 6..14
}

data class LoginViewState(
    val email: String = "",
    val password: String = "",
    val isValidEmail: Boolean = false,
    val isValidPassword: Boolean = false,
    val isLoginApiLoading: Boolean = false,
    val loginResponse: LoginResponse? = null,
    val errorResponse: ErrorResponse? = null
)
Service.kt:
Copy code
expect val platformEngine: HttpClientEngineFactory<HttpClientEngineConfig>

val myHttpClient: HttpClient
    get() = HttpClient(platformEngine) {
        install(JsonFeature) {
            serializer = KotlinxSerializer()
        }
        install(Logging) {
            level = LogLevel.ALL
        }
        expectSuccess = false
        HttpResponseValidator {
            validateResponse { response: HttpResponse ->
                println("Response: $response")
                val statusCode = response.status.value
                when (statusCode) {
                    in 300..399 -> throw RedirectResponseException(response)
                    in 400..499 -> throw ClientRequestException(response)
                    in 500..599 -> throw ServerResponseException(response)
                }

                if (statusCode >= 600) {
                    throw ResponseException(response)
                }
            }

            handleResponseException { cause: Throwable ->
                println("Exception: $cause")
            }
        }
        defaultRequest {
            url {
                host = "127.0.0.1:8080/"
                protocol = URLProtocol.HTTP
            }
            timeout {
                connectTimeoutMillis = 10000
                requestTimeoutMillis = 10000
                socketTimeoutMillis = 10000
            }
        }
    }
LoginService.kt:
Copy code
class LoginService(private val httpClient: HttpClient) {
    suspend fun login(email : String, password : String): LoginResponse = <http://httpClient.post|httpClient.post> {
        body = LoginRequest(email, password)
    }
}
LoginReqRes.kt:
Copy code
data class LoginRequest(val email: String = "", val password: String = "")
data class LoginResponse(val name: String = "", val age: Int = 0, val email: String = "")
data class ErrorResponse(val message: String)
Any suggestions?
1
k

Kurt Renzo Acosta

07/14/2020, 5:55 PM
You can consume it as you would
Copy code
@Test
fun stateFlowTest() = runBlocking {
    // Given
    ...

    // When 
    viewModel.login(...)

    // Then
    assertEquals(value, viewModel.stateFlow.value)
    // OR
    assertEquals(value, viewModel.stateFlow.first())     
}
☝️ 1
c

Chintan Soni

07/14/2020, 6:05 PM
@Kurt Renzo Acosta Agreed. But is there any way I can verify the updates getting in StateFlow. If you observe, I am throwing multiple states. How can verify each one of them?
21 Views