Thread
#multiplatform
    Chintan Soni

    Chintan Soni

    2 years ago
    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:
    @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:
    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:
    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:
    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?
    Kurt Renzo Acosta

    Kurt Renzo Acosta

    2 years ago
    You can consume it as you would
    @Test
    fun stateFlowTest() = runBlocking {
        // Given
        ...
    
        // When 
        viewModel.login(...)
    
        // Then
        assertEquals(value, viewModel.stateFlow.value)
        // OR
        assertEquals(value, viewModel.stateFlow.first())     
    }
    Chintan Soni

    Chintan Soni

    2 years ago
    @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?