Primeiras aulas do curso iOS: testando comportamentos com Mocks

iOS: testando comportamentos com Mocks

Criando classes falsas - Introdução

Sejam bem vindos à segunda parte do nosso curso de testes para iOS. Nesse curso, vamos avançar nossos conhecimentos em teste. Ou seja, vamos aprender a testar classes com nível de complexidade maior, por isso recomendo que você já tenha feito a primeira parte deste curso, que é onde estudamos sobre testes de unidade e a prática do TDD. É importante você estar confortável com todo esse conteúdo, porque vamos continuar utilizando tudo que vimos na primeira parte.

Vamos começar entendendo como simular o comportamento de algumas classes. Por exemplo, vamos continuar trabalhando em cima do projeto de leilão, e vamos precisar, por exemplo, escrever um teste que encerre um leilão, que verifique o maior lance. Nesse projeto, vamos trabalhar com persistência de dados locais no nosso device, ou seja, vamos ter uma camada down.

Para um leilão ser encerrado, ele vai precisar ser salvo no banco de dados e ser atualizado como encerrado. Será que faz sentido mexer no banco de dados de produção do nosso app para conseguir realizar esse tipo de teste? Não faz sentido. Então vamos começar a pensar em outra forma quando simularmos essa classe down para conseguir fazer o teste.

A primeira abordagem que vamos ver vai ser criar classes falsas. Vou criar uma classe falsa na camada down do app que devolva objetos fixos para conseguirmos testar. Só que depois que fizermos isso vamos perceber que dá um pouco de trabalho criar essas classes falsas na mão. Por isso vamos instalar um framework que vai nos auxiliar, que se chama Cuckoo, que vai nos ajudar a criar essas classes falsas.

Com o Cuckoo, conseguimos criar mocks rapidamente. Depois, vamos aprender que para trabalhar com mocks precisamos ensinar esse mock a responder da forma que precisamos. Por exemplo, se precisarmos verificar os leiloes encerrados do banco de dados, não vou precisar de uma classe concreta. Vou criar um mock, que vou ensinar a devolver determinados objetos para que eu consiga testar o comportamento de alguma classe. É dessa forma que vamos trabalhar.

Depois, vamos aprender a verificar se métodos foram chamados. Por exemplo, para atualizar um leilão preciso chamar um método na camada down que atualize essas informações. Só que não vou utilizar a classe completa da camada down. Só vou verificar se o método que atualiza foi chamado, e não de fato vou invocar toda a lógica. Dessa forma, conseguimos realizar esse tipo de teste.

Depois vamos partir para mocks que lançam exceções. Ou seja, caso ocorra algum problema com as consultas, preciso criar um teste para verificar se meu aplicativo sabe tratar essas exceções. Por isso vamos criar mocks que lançam exceções, para não precisarmos mexer e forçar algum erro no banco de dados.

Por último, vamos aprender a testar o view controller e levantar a questão de quando devemos criar mocks e quando não devemos. Esse é o conteúdo que vamos ver nessa segunda parte. Espero você.

Criando classes falsas - Testando o encerrador de leilão

Essa é a segunda parte do nosso curso de testes para iOS. Vamos avançar os nossos conhecimentos em teste e aprender a criar coisas mais complexas. Para isso, vamos continuar utilizando o projeto do curso anterior, que é o leilão. Só que para ganhar tempo adicionei algumas classes e vou pedir para que vocês façam o download novamente.

Logo de cara, vou abrir a pasta models e vou começar a analisar a classe EncerradorDeLeilao. Como o próprio nome sugere, a função principal dela é encerrar leilões.

class EncerradorDeLeilao {
    private var total = 0
    func encerra() {
        let dao = LeilaoDao()
        let todosLeiloesCorrentes = dao.correntes()
        for leilao in todosLeiloesCorrentes {
            if comecouSemanaPassada(leilao) {
                leilao.encerra()
                total+=1
                dao.atualiza(leilao leilao)
            }
        }
    }
}

O método cria uma constante chamada dao, instância desse leilaodao, ou seja, a partir de agora estamos mexendo com persistência local no nosso device. Em baixo ele cria uma constante chamada todosLeiloesCorrentes, chama o dao.correntes e faz um for para percorrer toda a lista. Ele verifica se o leilão começou semana passada, ou seja, se é um leilão antigo. Se for, ele vai encerrar esse leilão, vai somar 1 e atualizar no banco de dados.

Agora que entendemos o que esse método faz, precisamos testar para ver se realmente ele está encerrando leilões que começaram uma semana antes. Para começar a testar, vou criar uma nova classe de teste, na opção "unit test case class". Com ela selecionada, vou dar o "Next", e em classe precisamos de um nome para ela. Já discutimos no curso anterior que é uma boa prática manter o mesmo nome da classe em produção, que no caso é encerrador de leilão, mais o sufixo "test" "EncerradorDeLeilaoTests".

Ele já traz vários métodos. Vamos apagar os dois últimos, que não vamos utilizar, e também vamos aproveitar para apagar os comentários.

class EncerradorDeLeilaoTests: XCTestCase {
    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
    }
}

O primeiro método de teste que vamos criar será testDeveEncerrarLeiloesQueComecaramUmaSemanaAntes(). Lembrando que todo método de teste tem que começar com test. Repare que a nomenclatura ficou longa, mas também já comentamos no curso anterior que não tem problema criar métodos com nomes longos nas classes de teste, porque serve para documentar e entender o que a classe de produção está fazendo.

func testDeveEncerrarLeiloesQueComecaramUmaSemanaAntes() {

}

Para conseguir testar se um leilão foi encerrado ou não, primeiro precisamos criar uma data antiga, porque nosso encerrador de leilão verifica se o leilão começou na semana passada. Para conseguir criar essa data, vamos começar com um date formatter, que é um formatador de datas. Depois, vou passar um formato para essa data. Ele vai ser ano, mês e dia: yyyy/MM/dd.

func testDeveEncerrarLeiloesQueComecaramUmaSemanaAntes() {
    let formatador = DateFormatter()
    formatador.dateFormat = "yyyy/MM/dd"
}

Agora sim podemos criar a data. dataAntiga é igual ao formatador.date. Estamos criando com o guard let porque o date é opcional. Ele vai tentar criar essa data. Para não trabalhar com variáveis do tipo optional, estou criando um guard let.

Temos que seguir o mesmo formato que criamos lá em cima. Primeiro o ano, que pode ser 2018, depois o mês e o dia. A data vai ser 09/05/2018. Por fim, precisamos colocar a condição de else com return.

func testDeveEncerrarLeiloesQueComecaramUmaSemanaAntes() {
    let formatador = DateFormatter()
    formatador.dateFormat = "yyyy/MM/dd"
    guard let dataAntiga = formatador.date(from: "2018/05/09") else { return }
}

Agora, precisamos criar os leilões. Vou chamar, por exemplo, de tvLed e ele será igual a CriadorDeLeilao(). Reparem que não temos acesso porque precisamos importar as classes do projeto. Lá em cima, digito @testable import e o nome do projeto, que é Leilao.

import XCTest
@testable import Leilao

Agora, sim, podemos chamar o CriadorDeLeilao().para() o nome do produto, descricao, que no caso é TV Led e depois naData(), que é dataAntiga. Por fim, .controi().

let tvLed = CriadorDeLeilao().para(descricao: "TV Led").naData(data: dataAntiga).constroi()

Vamos aproveitar e criar outro leilão também, por exemplo, de uma geladeira. Chamamos novamente o CriadorDeLeilao().para() decebendo descricao: "Geladeira" e naData() recebendo data dataAntiga. Temos então dois leilões. O próximo passo é chamar o encerrador com .encerra()

let tvLed = CriadorDeLeilao().para(descricao: "TV Led").naData(data: dataAntiga).constroi()
let geladeira = CriadordeLeilao().para(descricao: "Geladeira").naData(data: dataAntiga).constroi()

let encerradorDeLeilao = EncerradorDeLeilao()
encerradordeLeilao.encerra()

Agora precisamos fazer a verificação e ver se o tvLed.encerrado, que é uma variável booleana que ele retorna, é verdadeira. Só que repare que ele também é opcional. Vamos extrair o valor para conseguir fazer o assert, então guard let statusTvLed = tvLed.isEncerrado(). Se ele não conseguir, damos um return. Embaixo a mesma coisa com a geladeira.

guard let statusTvLed = tvLed.isEncerrado() else { return } 
guard let status.Geladeira = geladeira.isEncerrado() else { return } 

Agora, precisamos fazer a asserção XCTAssertTrue(), passando o status do produto. Tem que estar true se nosso encerrador de leilão funcionar.

XCTAssertTrue(statusTvLed)
XCTAssertTrue(statusGeladeira)

Rodando o teste, tem algumas falhas. Vamos entender o que aconteceu?

func testDeveEncerrarLeiloesQueComecaramUmaSemanaAntes() {
    let formatador = DateFormatter()
    formatador.dateFormat = "yyyy/MM/dd"
    guard let dataAntiga = formatador.date(from: "2018/05/09") else { return }
}

    let tvLed = CriadorDeLeilao().para(descricao: "TV Led").naData(data: dataAntiga).constroi()
    let geladeira = CriadordeLeilao().para(descricao: "Geladeira").naData(data: dataAntiga).constroi()

    let encerradorDeLeilao = EncerradorDeLeilao()
encerradordeLeilao.encerra()

    guard let statusTvLed = tvLed.isEncerrado() else { return } 
    guard let status.Geladeira = geladeira.isEncerrado() else { return } 

    XCTAssertTrue(statusTvLed)
    XCTAssertTrue(statusGeladeira)
}

Nós criamos dois produtos, e embaixo chamamos o encerrador. Só que a classe do encerrador de leilão, com esse método encerra(), chama o dao.correntes(). Ele pega do banco de dados todos os leilões correntes para depois verificar se o leilão é antigo ou não. Ou seja, o teste não está falhando porque os produtos não estão salvos no banco de dados. Apenas criamos eles, e na hora que ele chama esse método ele não encontra nenhum produto.

Vamos voltar ao teste e salvar os produtos no banco de dados, porque precisamos fazer o teste passar. Antes dele encerrar, é preciso salvar os produtos então let dao = LeilaoDao(). Em sequência, vamos pegar o dao.salva e vou passar a tvLed e a geladeira.

let dao = LeilaoDao()
dao.salva(tvLed)
dao.salva(geladeira)

Depois que salvamos no banco de dados, ele vai encerrar o leilão. Nesse momento, já é para aparecer a tvLed e a geladeira. Agora, para conseguir testar, vamos precisar recuperar novamente do banco de dados.

let dao = LeilaoDao()
dao.salva(tvLed)
dao.salva(geladeira)

let encerradorDeLeilao = EncerradorDeLeilao()
encerradorDeLeilao.encerra()

let leiloesEncerrados = dao.encerrados()

Tiraremos esses dois guard let que criamos e começamos a verificar de outra forma. Como temos uma lista, primeiro vamos verificar o valor dela, o número de elementos que tem dentro dela. XTCAssertEqual() tem que ter 2 produtos, a geladeira e o TV Led. Depois passamos leiloesEncerrado.count. Dentro desse assert true, vamos pegar leiloesEncerrados, o elemento da primeira posição desse array, ou seja, 0, e vou pegar um isEncerrado. O mesmo será feito com geladeira, mas nesse caso pegando a segunda posição, ou seja, posição 1.

XCTAssertEqual(2, leiloesEncerrados.count)
XCTAssertTrue(leiloesEncerrados[0].isEncerrado()!)
XCTAssertTrue(statusGeladeira)[1].isEncerrado()!)'

Agora nosso teste passou! Realmente estamos conseguindo encerrar o leilão. Rodando novamente, sem mexer em nada, ele falha. Nosso framework de teste diz que esperava dois objetos e recebeu quatro. O problema é que na primeira vez funcionou porque criamos a tvLed e a geladeira, e como não havia nada no banco de dados, ele salvou esses dois produtos. Quando ele foi fazer a verificação, tinha que ter geladeira e tvLed, mas quando rodamos o teste pela segunda vez tvLed e geladeira já estavam lá, mais os dois produtos que criamos. Toda vez que rodarmos o teste ele vai adicionar dois produtos e mudar o cenário.

Será que essa é a melhor forma de testar?

Criando classes falsas - Criando classe falsa

Nós acabamos de escrever um teste para verificar se o método encerra() da classe EncerradorDeLeilao realmente está funcionando. Porém, nos deparamos com um problema: nós estamos executando o teste e salvando os objetosutilizados para testar de verdade no banco de dados do nosso aplicativo. Cada vez que executamos os testes, são incluídos dois objetos no banco de dados e isso muda o cenário do teste.

Faz sentido salvar esses objetos de teste no banco de dados de verdade do nosso aplicativo? Não. Mas então como testar o comportamento da classe EncerradorDeLeilao sem salvar os objetos de verdade no banco de dados? Poderíamos por exemplo criar uma classe falsa para simular esse comportamento.

Na pasta "Dao", vamos criar uma nova classe. Ele traz um arquivo em branco, a ideia é ter os mesmos métodos da classe LeilaoDao sem salvar no banco de dados. Então, vamos chamá-lo de "LeilaoDaoFalso". Ele traz um arquivo em branco e nele vamos colocar class LeilaoDaoFalso.

class LeilaoDaoFalso { 

}

Agora, vamos dar uma olhada no que precisamos. Se entrarmos na classe LeilaoDao, temos por exemplo o método salva(), que utilizamos para testar. Então vamos chamar LeilaoDaoFalso e em seguida o método salva(). Neste método, vamos receber o _ leilao: Leilao. E acima vamos criar uma variável chamada leiloes:[] que será um array de Leilao que inicializaremos vazia.

class LeilaoDaoFalso { 

    private var leiloes:[Leilao] = []
    func salva(_ leilao:Leilao) {
    }
}

No método salva(), não vamos incluir isso no banco de dados. Vamos pegar a lista de leiloes, que acabamos de criar e adicionar o elemento com append(newElement: ). E adicionamos o objeto que recebemos por parâmetro.

func salva(_ leilao:Leilao) {
    leiloes.append(leilao)
}

Além do método salva() vamos precisar do método encerrados() que vamos utilizar para ter a lista de leilões encerrados. Vamos criar esse método. Ele vai devolver uma lista de Leilão. A ideia é retornar a lista de leilões, mas queremos só os encerrados. Então vamos fazer um filtro com .filter() e vamos filtrar apenas os leilões que tem a variavel encerrado igual a true.

func encerrados() -> [Leilao] {
    return leiloes.filter({ $.encerrao == true })
}

Além desse método, precisamos do método correntes(), porque vamos utilizar no encerrador de leilão. Vamos criar esse método. Vou devolver também uma lista de Leilao. Aqui vou filtrar os leilões que tem a variável encerrado igual a false.

func correntes() -> [Leilao] {
    return leiloes.filter({ $0.encerrado == false })
}

Para finalizar, também tem o método atualiza(), que vai receber um leilao, nele não vamos fazer nada, vamos apenas criar, porque vamos utilizar na classe encerrador de leilão.

func atualiza(leilao:Leilao) {}

Temos uma classe falsa, que vai simular a classe concreta do banco de dados, e precisamos utilizar a classe. Onde estou criando a constante dao, vou trocar o let dao igual a LeilaoDao() por LeilaoDaoFalso().

Nosso teste falhou. Vamos entender. Ele disse que espera dois elementos dentro da lista e vieram dez. Eu rodei os testes e ele continuou acumulando elementos no banco de dados. Se já instanciamos o LeilaoDaoFalso(), deveria estar funcionando.

Embaixo, estamos instanciando a classe encerrador de leilão e chamando o método encerra. Ele logo de cara instância a classe concreta. Aqui está o problema. Estamos instanciando, porém aquele LeilaoDaoFalso() não está sendo utilizado, porque ele está instanciado oLeilaoDao(). Será que faz sentido? Na verdade, ela só precisa utilizar o LeilaoDao(). Não faz sentido instanciar. Aí entra o conceito de injeção de dependências. Vamos tirar a instanciação da classe LeilaoDao() desse método encerra() e vamos receber essa dependência no método construtor da classe. Vou colocar o tipo LeilaoDaoFalso(). Não tem problema, porque estamos apenas testando se faz sentido trabalhar com essas classes falsas, depois mudamos.

init(_ leilaoDao:LeilaoDaoFalso)

No método construtor da classe EncerradorDeLeilao, estou recebendo o leilaoDao, e agora vou guardar um atributo da classe, que é do tipo LeilaoDaoFalso. Depois, coloco que self.dao é igual ao leilaoDao que estou recebendo por parâmetro. Dessa forma, não preciso mais instanciar esse objeto nessa classe EncerradorDeLeilao

class EncerradorDeLeilao {

    private var total = 0 
    private var dao:LeilaoDaoFalso

    init(_ leilaoDao:LeilaoDaoFalso) {
        self.dao = leilaoDao
    }
    func encerra() {
        let todosLeiloesCorrentes = dao.correntes()
        for leilao in todosLeiloesCorrentes {
            if comecouSemanaPassada(leilao) {
                leilao.encerra()
                total+=1
                dao.atualiza(leilao: leilao)
            }
        }
    }
}

Agora que tenho o método construtor onde estou esperando o leilaoDao, preciso ir no teste e injetar o LeilaoDaoFalso(), que acabamos de criar.

let encerradorDeLeiao = EncerradorDeLeilao(dao)
encerradorDeLeilao.encerra()

Agora nosso teste passou. Dessa forma, estamos injetando o LeilaoDaoFalso na classe EncerradorDeLeilao e continuamos utilizando o dao que recebemos por parâmetro. Faz sentido realmente trabalhar com essas classes falsas. A seguir, continuamos.

Sobre o curso iOS: testando comportamentos com Mocks

O curso iOS: testando comportamentos com Mocks possui 166 minutos de vídeos, em um total de 37 atividades. Gostou? Conheça nossos outros cursos de iOS em Mobile, ou leia nossos artigos de Mobile.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda iOS acessando integralmente esse e outros cursos, comece hoje!

Plus

  • Acesso a TODOS os cursos da plataforma

    Mais de 1200 cursos completamente atualizados, com novos lançamentos todas as semanas, em Programação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

  • Alura Língua (incluindo curso Inglês para Devs)

    Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.

12X
R$85
à vista R$1.020
Matricule-se

Pro

  • Acesso a TODOS os cursos da plataforma

    Mais de 1200 cursos completamente atualizados, com novos lançamentos todas as semanas, em Programação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

  • Alura Língua (incluindo curso Inglês para Devs)

    Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.

12X
R$120
à vista R$1.440
Matricule-se
Conheça os Planos para Empresas

Acesso completo
durante 1 ano

Estude 24h/dia
onde e quando quiser

Novos cursos
todas as semanas