Teste de unidade Firebase com Kotlin

Tempo de leitura: 5 minutes

As práticas modernas de programação móvel e as práticas de programação em geral dão grande ênfase ao desenvolvimento orientado a testes. Se você está procurando desenvolver um aplicativo Android usando Kotlin, é melhor manter o TDD em mente, pois ele permitirá que seu aplicativo seja dividido em componentes testáveis ​​separáveis ​​- tornando mais fácil desenvolver testes e encontrar bugs em seu aplicativo.

Se o aplicativo tiver qualquer tipo de funcionalidade CRUD, uma boa opção de back-end é Firebase. O Firebase tem integração nativa com o Android Studio e é muito fácil de configurar se você não tiver experiência de back-end, o que o torna uma excelente escolha para programadores iniciantes e experientes.

Com o TDD em mente, você provavelmente desejará testar seus métodos que usam o Firebase para garantir que tudo esteja funcionando corretamente.

A linha Firebase em testes é a solução comum é usar um banco de dados de não produção e realizar as ações reais de leitura e gravação nesse banco de dados para garantir que tudo funcione (documentação do Firebase).

Com este método, você simplesmente usa o Firebase como faria normalmente – apenas em um banco de dados diferente para garantir que você não bagunce nada na produção.

Mas e se você não quiser usar um banco de dados de não produção? Ou você é um único desenvolvedor que não quer manter dois bancos de dados ou fica sem acesso à Internet a maior parte do tempo?

Neste blog, vou mostrar como você pode usar o Mockito para simular diferentes funções do Firebase e realizar testes de unidade local.

 

A configuração

Vou mostrar como simular o Firebase usando um cenário de exemplo. Digamos que você tenha um aplicativo CRUD com funcionalidade de login. Você tem uma classe de wrapper em torno do Firebase (prática recomendada no caso de você alterar o back end mais tarde na linha) chamada LogInModel.

LogInModel tem uma função LogIn (e-mail, senha) que queremos testar. O LogIn apenas chama a classe Firebase FirebaseAuth usando o método signInWithEmailAndPassword. Veja como o método LogIn no LogInModel seria em um nível básico:

fun logIn(email: String, password: String) {
    mAuth.signInWithEmailAndPassword(email, password)
            .addOnCompleteListener(this, OnCompleteListener { task ->
                if (task.isSuccessful) {
                    observer.logInSuccess(email, password)
                } else {
                    observer.logInFailure(task.exception, email, password)
                }
            })
}

No trecho acima, você pode notar o objeto observador. observador é do tipo LogInListener que definirei abaixo.

LogInListener é uma interface simples que outras classes podem aderir para saber se o login foi uma falha ou sucesso. LogInListener é simplesmente definido assim:

public interface LogInListener {
    void logInSuccess(String email, String password);
    void logInFailure(Exception exception, String email, String password);
}

Então, estamos tentando testar com nosso teste de unidade, certificando-se de que o logIn faz o que deve fazer – logar o usuário com o Firebase. Agora, com este exemplo, é óbvio que ele faz o que deveria, já que o código é tão curto, mas em projetos maiores, o teste se torna uma necessidade.

Os testes

Faremos um teste de unidade (no Android Studio referido como um “teste”) chamado LogInModelTest. Nossa classe de teste usará Mockito para simular o objeto firebase que estamos usando para testar a funcionalidade de nossa classe.

Dividirei LogInModelTest seção por seção para que possa entrar em detalhes sobre como a classe é construída.

class LogInModelTest : LogInListener {
    private lateinit var successTask: Task<AuthResult>
    private lateinit var failureTask: Task<AuthResult>

    @Mock
    private lateinit val mAuth: FirebaseAuth
    private lateinit var logInModel: LogInModel
    
    private var logInResult = UNDEF
    
    companion object {
        private const val SUCCESS = 1
        private const val FAILURE = -1
        private const val UNDEF = 0
    }

Este é o início do nosso arquivo de teste.

Na primeira linha, você notará nosso teste implementaLogInListener – isso torna mais fácil obter o resultado do método logIn que estamos testando. Como temos implementos no final do arquivo, você perceberá que tenho os métodos da interface LogInListener em.

Então temos o val mAuth privado @Mock: FirebaseAuth, que é o objeto Firebase que estamos simulando.

LogInModel é o objeto que faremos teste de unidade.

As duas tarefas que você vê são para substituir o resultado do login do Firebase. Ele nos permite testar um login com falha e um login bem-sucedido.

Os três const vals privados são para atualizar o resultado das chamadas LogInListener. Você poderia substituir isso por um enum, que é a prática recomendada, mas para deixar claro o que estou fazendo, eu apenas os tinha como variáveis ​​separadas. Eles são usados ​​com logInResult.

@Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        successTask = object : Task<AuthResult>() {
            override fun isComplete(): Boolean = true

            override fun isSuccessful(): Boolean = true
            // ...
            override fun addOnCompleteListener(executor: Executor,
                                onCompleteListener: OnCompleteListener<AuthResult>): Task<AuthResult> {
                onCompleteListener.onComplete(successTask)
                return successTask
            }
        }
      
        failureTask = object : Task<AuthResult>() {
            override fun isComplete(): Boolean = true

            override fun isSuccessful(): Boolean = false
            // ...
            override fun addOnCompleteListener(executor: Executor,
                                onCompleteListener: OnCompleteListener<AuthResult>): Task<AuthResult> {
                onCompleteListener.onComplete(failureTask)
                return failureTask
            }
        }
        logInModel = LogInModel(mAuth, this)
    }

Este é o nosso método de configuração @Before.

MockitoAnnotations.initMocks (this) simplesmente configura nossos objetos fictícios.

Aqui estou definindo o successTask e o failureTask. Agora, Task é uma interface, por isso estou definindo tantas funções (o “…” contém mais funções). Estou apenas destacando os importantes aqui.

No final do método, simplesmente inicializamos o resto das variáveis.

Agora vamos aos métodos de teste!

@Test
    fun logInSuccess_test() {
        val email = "cool@cool.com"
        val password = "123456"
        Mockito.`when`(mAuth!!.signInWithEmailAndPassword(email, password))
                .thenReturn(successTask)
        logInModel!!.logIn(email, password)
        assert(logInResult == SUCCESS)
    }

Aqui, estamos testando um login bem-sucedido.

Configuramos nossa combinação de e-mail / senha primeiro.

Em seguida, usamos o Mockito.when para garantir que, quando chamarmos nosso objeto FirebaseAuth, retornaremos successTask (uma tarefa que indica que o login foi bem-sucedido) e não obteremos uma exceção de ponteiro nulo ou outro comportamento inesperado.

Chamamos logIn na classe em teste e, em seguida, afirmamos que nossa tentativa de login teve sucesso. logInResult é definido pela interface que mencionei antes.

Se você quiser testar a falha, basta verificar se a chamada resultou em falha e usar o failTesk com a chamada Mockito.when. Aqui está um exemplo de como fazer isso:

@Test
    fun logInFailure_test() {
        val email = "cool@cool.com"
        val password = "123_456"
        Mockito.`when`(mAuth!!.signInWithEmailAndPassword(email, password))
                .thenReturn(failureTask)
        accountModel!!.logIn(email, password)
        assert(logInResult == FAILURE)
    }

Finalmente, passamos para o último código restante em nossa classe – os métodos de interface!

 override fun logInSuccess(email: String, password: String) {
        logInResult = SUCCESS
    }

    override fun logInFailure(exception: Exception, email: String, password: String) {
        logInResult = FAILURE
    }
}

Aqui, simplesmente definimos logInResult = SUCCESS se a tentativa de login foi bem-sucedida e, caso contrário, definimos como FAILURE. Isso permite que a declaração assert acima que você viu funcione.

E é isso! Embora seja definitivamente um pouco mais de código do que usar um banco de dados de não produção separado, é definitivamente possível fazer o teste de unidade do Firebase sem a necessidade do referido banco de dados extra. Se você quebrar o código em pedaços menores, verá que nenhum deles é especialmente complicado. Boa sorte no teste!

Para fins de referência, aqui está toda a classe em um snippet:

class LogInModelTest : LogInListener {
    private lateinit var successTask: Task<AuthResult>
    private lateinit var failureTask: Task<AuthResult>

    @Mock
    private lateinit val mAuth: FirebaseAuth
    private lateinit var logInModel: LogInModel
    
    private var logInResult = UNDEF
    
    companion object {
        private const val SUCCESS = 1
        private const val FAILURE = -1
        private const val UNDEF = 0
    }
    
    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        successTask = object : Task<AuthResult>() {
            override fun isComplete(): Boolean = true

            override fun isSuccessful(): Boolean = true
            // ...
            override fun addOnCompleteListener(executor: Executor,
                                onCompleteListener: OnCompleteListener<AuthResult>): Task<AuthResult> {
                onCompleteListener.onComplete(successTask)
                return successTask
            }
        }
      
        failureTask = object : Task<AuthResult>() {
            override fun isComplete(): Boolean = true

            override fun isSuccessful(): Boolean = false
            // ...
            override fun addOnCompleteListener(executor: Executor,
                                onCompleteListener: OnCompleteListener<AuthResult>): Task<AuthResult> {
                onCompleteListener.onComplete(failureTask)
                return failureTask
            }
        }
        logInModel = LogInModel(mAuth, this)
    }
  
    @Test
    fun logInSuccess_test() {
        val email = "cool@cool.com"
        val password = "123456"
        Mockito.`when`(mAuth!!.signInWithEmailAndPassword(email, password))
                .thenReturn(successTask)
        logInModel!!.logIn(email, password)
        assert(logInResult == SUCCESS)
    }
    
    @Test
    fun logInFailure_test() {
        val email = "cool@cool.com"
        val password = "123_456"
        Mockito.`when`(mAuth!!.signInWithEmailAndPassword(email, password))
                .thenReturn(failureTask)
        accountModel!!.logIn(email, password)
        assert(logInResult == FAILURE)
    }
  
    override fun logInSuccess(email: String, password: String) {
        logInResult = SUCCESS
    }

    override fun logInFailure(exception: Exception, email: String, password: String) {
        logInResult = FAILURE
    }
}

E se você chegou até aqui, aqui está um 🎁 como um símbolo do meu agradecimento!