iOS Teste Unitário e Teste de UI — Tutorial
Aprenda como criar testes unitários e testes de UI nos seus aplicativos iOS, e como checar a cobertura do seu código.
Este tutorial é uma tradução não oficial do post original de Audrey Tam autora do site do Ray Wenderlich.
Escrever testes não é a tarefa mais glamurosa, mas é importante, pois os testes fazem com que seu aplicativo não se torne uma bagunça com um monte de bugs escondidos.
Talvez você já tenha um aplicativo "funcionando" sem testes, e quer escrever testes para qualquer mudança que você fizer nele. Talvez você tenha alguns testes escritos, mas não tem certeza se eles são os testes certos. Ou talvez você esteja desenvolvendo um aplicativo agora mesmo e quer testá-lo.
Este tutorial mostra como usar os testes no Xcode Test Navigator para testar a model do aplicativo e métodos assíncronos, como fazer simulações de interações com bibliotecas ou objetos usando mocks, como testar a UI e a performance, e como usar a ferramenta de cobertura de código. Ao longo do caminho você vai pegar o vocabulário usado pelos testadores e, ao final deste tutorial, você estará injetando dependências no seu System Under Test(SUT) com desenvoltura.
Testar, Testar….
O que testar?
Antes de escrever qualquer teste, é importante começar com o básico: o que você precisa testar? Se o seu objetivo é aumentar um aplicativo que já existe, você deveria escrever para qualquer componente que você deseja criar ou modificar.
Geralmente, os testes devem cobrir:
- O core do app: Classes Model e métodos, e suas interações com a controller.
- Os fluxos mais comuns de UI
- Condições principais
- Correção de erros
Primeiras coisas FIRST(primeiro): Melhores práticas para testes
A palavra FIRST é acrônimo que descreve importantes critérios para um teste unitário efetivo. Estes critérios são:
- Fast(Rápido): os testes devem rodar rapidamente, caso contrários as pessoas não vão executá-los.
- Independent/Isolates(Independentes/Isolados): testes não devem fazer setup ou teardown para outro.
- Repeatable(Repetível): você deve obter os mesmos resultados toda vez que os testes forem executados. Problemas com API e concorrências podem causar falhas intermitentes.
- Self-validating(auto-validáveis): Testes devem ser completamente automatizados; o resultado de cada teste deve ser "passou" ou "falhou", para que não haja necessidade do programador interpretar algum log.
- Timely(Preventivos): Idealmente, os testes devem ser escritos antes do código que será testado (TDD — Desenvolvimento guiado a teste, primeiro o teste depois o código).
Depois desta introdução teórica, vamos à prática.
Começando
Baixe, descompacte, abra e dê uma olhada nos projetos iniciais BullsEye e HalfTunes.
BullsEye é baseado no exemplo desenvolvido no livro iOS Apprentice; Eu peguei a lógica do jogo da classe BullsEyeGame e adicionei um novo design.
No canto direito debaixo tem um segmented-control para o usuário selecionar a forma de jogar. Uma o usuário move o slider para o valor fornecido, e a outra ele fornece o valor no qual ele acredita que o slider está. A action do segmented-control também armazena a forma de jogar como padrão.
HalfTunes é um exemplo do nosso tutorial de URLSession, atualizado para Swift 3. Usuários podem baixar e executar trechos de músicas vindos da API do iTunes para músicas.
Vamos começar os testes!
Teste unitário no Xcode
Com o Xcode Test Navigator fica fácil trabalhar com testes; você vai usar isto para criar e rodar os testes no seu aplicativo.
Abra o projeto BullsEye e pressione Command-6 para abrir o test navigator.
Clique no botão + no canto esquerdo inferior da tela, então selecione a opção New Unit Test Target… do menu.
Aceite o nome padrão BullsEyeTests. Quando o test bundle aparecer no test navigator, clique nele para abrir o editor. Se não aparecer automaticamente, fechando e abrindo o teste navigator.
O template importa XCTest
e define BullsEyeTests
como uma subclasse de XCTestCase,
com setup(), tearDown()
e métodos de exemplo.
Existem três formas de executar uma classe de teste:
- Product\Test ouCommand-U. Estes dois comandos vão executar todos os testes.
- Clique no botão play no test navigator
- Clique no botão com formato de diamante no editor.
Teste as diferentes formas para ver como é.
Quando todos os testes forem bem sucedidos, os diamantes ficarão verdes.
Clique no diamante cinza no final do método testPerformanceExample()
para abrir os resultados do teste de performance.
Para este projeto você não precisa do método testPerformanceExample()
, então pode deletá-lo.
Usando o XCTAssert para testar Model.
Primeiro, você usará XCTAssert para testar a função principal da model do BullsEye: O objeto de BullsEyeGame está calculando a pontuação de um round?
No arquivo BullsEyeTests.swift, adicione esta linha abaixo do último import.
Esta linha está dando acesso do teste unitário à classe e métodos do BullsEye.
No topo do arquivo BullsEyeTests,
adicione esta propriedade:
Crie e start um novo objeto de BullsEyeGame
no método setup()
depois da chamada do super:
Isto vai criar um objeto de SUT(System Under Test) a nível de classe, então todos desta classe podem acessar as propriedades e métodos do objeto de SUT.
Aqui, você também chamará o método startNewGame
.
Muitos dos seus testes utilizarão o targetValue
para testar se o cálculo da pontuação está correto.
Não esqueça de liberar seu objeto SUT no tearDown()
antes de chamar o super.
Dica: é uma boa prática criar o objeto SUT no setup()
e liberá-lo no tearDown()
para assegurar que todos começarão com o SUT limpo. Para saber mais leia este post, em inglês, Jon Reid’s sobre esse assunto.
Agora você está pronto para escrever seu primeiro teste!
Substitua o código do método testExample()
pelo seguinte:
Um método de teste deve sempre começar com a palavra test
seguido da descrição do que este método está testando.
Uma boa prática para formatar métodos de teste é dividí-lo em três seções: given, when e then.
- Na seção given, configure qualquer valor necessário para o teste: neste exemplo, você criou
guess
especificando o quanto isso difere dotargetValue.
- Na seção when, execute o código que será testado: chame o método
gameUnderTest.check(_:)
- Na seção then, imponha o valor que você espera(no caso de exemplo
gameUnderTest.scoreRound
é 100 — 5) com uma mensagem que será impressa caso o teste falhe.
Rode o teste clicando no ícone de diamante no test navigator. O app será executado e o ícone do diamante ficará verde com uma marcação de check.
Dê uma olhada na lista completa de métodos XCTestAssertions, segure o command e clique no método XCTAssertEqual
para abrir o arquivo XCTestAssertions.h, ou vá para a documentção no site da Apple.
A ideia de dividir o método nas seções Given-When-Then surgiu com Behavior Driven Develpment(BDD), que é o desenvolvimento guiado a comportamento, como um termo amigável para o cliente, como um apelido. O G-W-T é chamado também de Arrange-Act-Assert e Assemble-Activate-Assert.
Debugando um Teste
Existe um bug no BullsEyeGame
, agora você vai praticar como encontrá-lo.
Para ver o bug em ação, renomeie o método testScoreIsComputed
para testScoreIsComputedWhenGuessGTTarget
, então duplique ele criando o método chamado testScoreIsComputedWhenGuessLTTarget
.
Neste método ao invés de somar 5, subtraia 5 do targetValue
na seção given. Deixe o resto do mesmo jeito.
A diferença entre guess
e targetValue
continua 95, então o score continuará sendo 95.
No breakpoint navigator, adicione um Test Failure Breakpoint; isto irá parar o teste quando o método que está sendo testado informar uma falha.
Rode o seu teste: ele deve parar na linha XCTAssertEqual
que é onde o teste falhou.
Inspecione gameUnderTest
e guess
no console de debug.
guess
é targetValue - 5
, mas o scoreRound
é 105 e não 95!
Para investigar mais, use o processo normal de debug: adicione um breakpoint na seção when e também um no método check(_:)
do arquivo BullsEyeGame.swift, onde é criada a diferença. Execute o teste novamente e vá passo-a-passo para ver o valor da constante difference.
O problema é que a difference
é negativa, então quando você faz a subtração de 100-(-5), isso vira uma soma resultando em 105. Para resolver isso, use a função global abs()
para pegar o valor absoluto. Descomente a linha 49 e comente a linha 50, remova os breakpoints e execute o teste novamente.
Para qualquer valor numérico `x`, `x.magnitude` é o valor absoluto de `x`.
Você pode usar a propriedade `magnitude` em operações que são simples de implementar em termos de valores não assinalados, como imprimir o valor de um inteiro, que é apenas a impressão do caractere ‘-’ na frente do valor absoluto.
Por exemplo:
let x = -200
x.magnitude == 200
Existe uma função global chamada `abs(_:)`, ela fornece uma sintaxe mais amigável quando você precisa encontrar um valor absoluto. Como a função `abs(_:)` sempre retorna um valor do mesmo tipo, mesmo que no contexto de Generic, é recomendado utilizá-la no lugar da propriedade `magnitude`.
Usando XCTestExpectation para testar operações assíncronas
Agora que você aprendeu como testar models e debugar falhas, vamos dar um passo a frente para usar XCTestExpectation
para testar operações de rede, requisições a APIs e etc.
Abra o projeto HalfTunes. Este projeto usa URLSession
para baixar exemplos de musicas do iTunes. Suponhamos que você queira adicionar o app para usar o Alamofire para fazer requisições de rede. Para ver se vai dar certo, você deve escrever testes de rede e executá-los antes e depois de você mudar o código para o Alamofire.
Os métodos de URLSession
são assíncronos: eles tem um retorno imediato, mas eles terminam sua execução um tempo depois. Para testar métodos assíncronos você utiliza XCTestExpectation
que fará com que o seu método de teste aguarde a resposta completa de uma chamada assíncrona.
Testes assíncronos, normalmente, são mais demorados, então mantenha-os separados dos testes rápidos. Crie um novo arquivo de testes apenas para os testes assíncronos.
Então, vamos adicionar os testes ao HalfTunes.
Selecione o test navigator, clique no + e então clique no botão New Unit Test Target… e dê o nome HalfTunesSlowTests.
Import o HalfTunes para baixo do import do arquivo.
Todos os testes nesta classe vão usar a sessão padrão para enviar as requisições, então declare um objeto sessionUnderTest
, instancie ele no setup()
e limpe-o no tearDown()
. Não esqueça de no setup()
colocar os códigos sempre abaixo da chamada do super.setup()
, e no tearDown()
colocar a chamada do super.tearDown()
sempre depois dos seus códigos.
Substitua o método testExample()
pelo método abaixo:
Este teste verifica se a requisição que você enviou retornou o código http 200.
Uma boa parte do código deste método você escreveu anteriormente, exceto as seguintes linhas:
expectation(_:)
retorna um objeto deXCTestExpectation
que está armazenado empromise
. Um outro nome muito comum urilizado para este objeto éexpectation
oufuture
. O parêmetrodescription
descreve o que você espera que aconteça.- Para corresponder o
description
você chamapromise.fulfill()
na condição de sucesso da closure do método assíncrono. waitForExpectations(_:handler:)
mantém o teste rodando até que todas expectations sejam preenchidas, ou o intervalotimeout
acabe, o que acontecer primeiro.
Execute o teste. Se você estiver conectado a internet, o teste dará certo no primeiro segundo depois que o app for carregado no simulador.
Falhou Rapidamente
Falhas ferem, mas isso não é para sempre. Aqui você vai entender como descobrir rapidamente se os seus testes falharam, economizando tempo para gastar com outras coisa.
Altere seu teste para falhar. Remova a letra S da url.
Rode o teste. Ele falhou, mas foi por conta de timeout, os 5 segundos passaram sem resposta do servidor. Quando a requisição falha, o teste só termina o acaba o tempo determinado no timeout.
Você pode fazer este teste falhar mais rápido alterando a expectation: ao invés de esperar pela requisição bem sucedida, espere apenas até a chamada do completion handler do método assíncrono. Isso acontece mais rápido que a resposta do servidor, ambos OK ou error, que irão preencher o expectation. Então seu teste pode verificar se o a requisição foi bem sucedida.
Para ver como isso funciona, crie um novo test. Mas antes corrija a url, coloque o S na palavra itunes. Adicione o seguinte teste:
A grande diferença aqui é fazer com que o expectation seja preenchido no completion handler, o que demora 1 segundo para acontecer. Se a requisição falhar, então o then
assertion falha.
Rode o teste. Agora falha bem rápido, e falha porque a requisição falhou e não porque deu timeout.
Corrija a palavra itunes e teste novamente.
Simulando Objetos e Interações
Testes assíncronos confirmam que o código que você fez para a se conectar à API está certo. Agora talvez você queira testar se o seu código está funcionando corretamente quando recebe uma entrada de URLSession
, ou quando atualiza o UserDafaults
ou o CloudKit
.
A maioria dos aplicativos interagem com objetos de sistemas ou bibliotecas de objetos — você não controla objetos — e testes com estes objetos pode ser lentos e não repetíveis, violando dois dos 5 princípios FIRST. Ao invés de fazer testes com interações reais, você pode simular interações e usar objetos “mockados”.
Utilize a simulação e objetos mockados quando seu código uma dependencia com objetos de sistema ou com uma biblioteca de objetos — crie um objeto fake e utilize-o no seu código.
Leia este artigo sobre Injeção de Dependência do Jon Reid para entender melhor.
Entrada de dados Fake
Neste teste, você vai conferir se o método updateSearchResults(_:)
está fazendo o parse corretamente dos dados baixados conferindo se searchResults.count
está correto.
The SUT é a view controller, e você vai simular uma sessão com alguns dados pré-baixados.
Selecione o test navigator, clique no + e então clique no botão New Unit Test Target… e dê o nome HalfTunesFakeTests. Importe este código abaixo do primeiro import do arquivo.
Declare o SUT, inicialize ele no setup()
e libere ele no tearDown()
.
Não esqueça de no setup()
colocar os códigos sempre abaixo da chamada do super.setup()
, e no tearDown()
colocar a chamada do super.tearDown()
sempre depois dos seus códigos.
O objeto SUT é uma view controller porque o HalfTunes tem um problema de Massive View Controller. Todo o trabalho é feito na SearchViewController.swift. Mova os código de conexão de rede para módulos separados para reduzir o problema e melhorar a testabilidade.
Agora você vai precisar de um objeto JSON que sua sessão fake fornecerá par o seu teste. Apenas alguns itens já servem então limitei o download dos itens do iTunes adicionando o parâmetro &limit=3
ao final da url da API.
https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3
Copie esta url e cole-a no seu navegador. Com ela você vai baixar um arquivo chamado 1.txt
ou algo assim. Confirme que ele é um JSON, e então renomeie-o para abbaData.json e adicione ao grupo de testes HalfTunesFakeTests no XCode.
O projeto HalfTunes tem um arquivo chamado DHURLSessionMock.swift. Neles está definido um protocolo chamado DHURLSession
com a declaracao de métodos para criar um requisiçao com uma URL
ou com uma URLRequest.
No arquivo tambem tem URLSessionMock
que implementa o protocolo DHURLSession
e que cria uma URLSession
mockada com data, response e error.
Vamos definir os dados fake para data e response, e criar o objeto de sessão fake, no setup()
depois do código que cria o SUT.
No final do setup()
atribua a sessão fake ao atributo de sessão do SUT.
Agora você está pronto para escrever o teste que vai verificar se a chamada do método updateSearchResults(_:)
está fazendo o parse dos dados fake. Substitua o método de exemplo testExample()
pelo seguinte:
O teste realizado no 7, é para verificar e confirmar que o searchResults
está vazio antes da execução da task — isso deve ser verdade pois você criou o SUT no setup()
.
Os dados fake contém o JSON para três objetos Track
, então no linha 25, o arraysearchResults
deve ter 3 itens.
Execute o teste, ele irá rodar rapidamente pois como não é um teste real, não tem conexão de rede.
Fake Atualização de Objeto
O teste anterior você utilizou um arquivo .JSON para simular o parse. Agora você usar um objeto "mockado" para testar se o seu código está atualizando corretamente o UserDefaults
.
Abra o projeto BullsEye. Você deve se lembrar de que o app tem duas formas de se jogar, uma é a que você move o slider para a posição na qual você acredita que é a referente a um número dado, e a outra é a que você insere o valor que você acredita que o slider está. Estas duas opções são selecionadas por segmented-control, e a cada vez que você seleciona uma opção o gameStyle
é armazenado no UserDefaults
.
Então o teste de agora vai verificar se essa gravação está acontecendo corretamente.
No test navigator, adicione um novo teste unitário clicando no botão New Unit Test Target… e nomeie como BullsEyeMockTests.
Adicione o seguinte código depois do import
:
A classe MockUserDefaults
está sobrescrevendo o método set(_:forKey:)
da classe UserDefaults
para incrementar a variável gameStyleChanged
. Uma forma mais lógica de fazer esse controle é definindo true
ou false
para a variável, mas incrementando uma variável do tipo Int
temos mais possibilidades de testes, como por exemplo testar se o método foi chamado somente uma vez.
Crie a variável SUT e o objeto de mock na classe BullsEyeMockTests
:
No setup()
instancie o SUT e o objeto de mock e atribua o objeto à propriedade defaults
do SUT.
Libere o SUT e o objeto mockado no tearDown()
:
Substitua o método testExample()
por testGameStyleCanBeChanged()
:
O teste na linha 7 verifica se a propriedade gameStyleChanged
do objeto mockado é igual a 0, o que de fato deve ser pois o segmented-control ainda não foi acionado.
O teste na lina 13 também deve ser verdadeiro pois as linhas 8 e 9, fazem a chamada da ação do segmented-control, fazendo com que a propriedade gameStyleChanged
seja incrementada.
Rode o teste e veja o resultado.
Teste de UI no Xcode
Os testes de UI chegaram no Xcode 7. Você pode gravar interações com a interface do seu app. Os testes de UI funcionam encontrando, queries e eventos dos objetos de interface e enviando isso para eles.
Vamos a ver como funciona no código.
No projeto BullsEye, vá ao test navigator e adicione um UI Test Target. Certifique-se de que o target a ser testado é BullsEye, e então aceite o nome sugerido BullsEyeUITests.
Adicione esta variável no início da classe BullsEyeUITests
:
No método setup()
, substitua XCUIApplication().launch()
por:
Altere o nome do método testExample()
para testGameStyleSwitch()
.
Abra uma nova linha no método testGameStyleSwitch()
e clique no botão gravar na parte debaixo da janela do editor de código.
Quando o app aparecer no simulador, toque no botão Slide do segmented-control e na label com o texto Get as close as you can to: e depois clique no botão gravar novamente para terminar a gravação.
Você vai ver um código como este no seu método:
Se tiver mais algum código, pode deletar.
A linha 1 está duplicando a propriedade que você instanciou no setup()
e você não precisa de tap()
ainda. Então pode deletar a linha 1 e os .tap()
ao final das linhas 2 e 3.
Altere o método para ficar assim:
Agora vamos criar a seção Given.
Agora que você tem nomes para dois botões e duas possíveis labels. Adicione o seguinte.
Este método verifica se a label correta existe quando cada botão é selecionado ou tocado. Execute o teste.
Teste de Performance
A documentação da Apple diz: Um teste de performance pega um bloco de código que você quer avaliar e executa ele 10 vezes coletando o tempo médio de execução e o desvio padrão para as execuções. A média de cada medida forma um valor para a execução do teste que pode ser comparado com o valor baseline para avaliar o sucesso ou a falha do teste.
Escrever um teste de performance é muito simples: você só precisa colocar o código que você quer testar dentro da closure do método mesure()
.
Para ver como isso funciona, vá o projeto HalfTunes, e no HalfTunesFakeTests, substitua o testPerformanceExample()
por este:
Rode o teste. Para ver o resultado do teste clique no ícone, diamante cinza, que aparece perto do final da closure measure()
.
Clique em Set Baseline e execute o teste novamente. Ele irá informar que foi pior ou melhor que o parâmetro de baseline. No botão Edit, você pode resetar os valores de baseline.
Baselines são armazenados com base na configuração do device, então você pode rodar o teste em diferentes tipos de devices e cada teste tem um baseline diferente.
Toda vez que você fizer uma alteração no app que impactar na performance, é indicado que você execute o teste novamente.
Cobertura de Código
A ferramenta de cobertura de código informa quais partes do seu app estão sendo testadas, e com isso você sabe quais partes ainda não foram testadas.
Você deve executar testes de performance com a cobertura de código ativada? A documentação da Apple diz o seguinte: A captação de dados da cobertura de código implica na performance… afetando a performance da execução do código de forma linear. Se você julgar necessário ativar a cobertura de código não tem problema.
Para ativar a cobertura de código abra o esquema de Test, marque o checkbox da cobertura de código.
Rode todos os testes com o Command+U, então abra o reports navigator com o Command+8. Selecione By Time, e então clique em Coverage.
Clique na seta para mostrar a lista de funções do arquivo SearchViewController.swift:
Passe o mouse sobre a barra azul perto do método updateSearchResults(_:)
, repare que a cobertura não é 100%.
Clique duas vezes na linha do método para abrir o código fonte.
O número ao lado do método indica quantas vezes este teste testado. Seções que não foram chamadas ficam vermelhas.
100% de cobertura?
Como deve ser difícil cobrir 100% do código?! Google “100% unit test coverage”, e você encontrar vários argumentos a favor e contra isso. Argumentos contra dizem que os últimos 10–15% não valem o esforço. Argumentos a favor dizem que os últimos 10–15% são os mais importantes, e por isso é tão difícil implementá-los. O Google diz que é difícil testar coisa mal feita. Para saber mais sobre isso leia este artigo que fala os problemas que um código não testável pode trazer. Você vai chegar a conclusão que TDD — Test Driven Development é o caminho a ser seguido.
Para onde ir daqui?
Agora você boas ferramentas e conhecimento para escrever testes nos seus aplicativos. Eu espero que este tutorial tenha te dado confiança para testas todas coisas.
Você pode encontrar os projetos finais aqui
Seguem algumas recomendações de estudo:
- Agora que você sabe escrever testes para seus projetos, o próximo passo é a automação: Integração e Entrega Contínua. Comece com o processo de automação de testes da Apple com Xcode Server e o
xcodebuild
, e o artigo de entrega contínua da Wikipedia que foi escrito por especialistas da ThoughtWorks. - TDD en Swift Playgrounds usa
XCTestObservationCenter
para rodar casos de testes nos Playgrounds. Você pode desenvolver o código do seu projeto e os testes nos Playgrounds e depois transferir eles para o seu aplicativo. - Watch Apps: Como nós testamos eles? A CMD+U Conference mostra como usar o PivotalCoreKit para testar aplicativos de watchOS.
- Se você tem um aplicativo e ainda não escreveu os testes para ele, é recomendado que você leia o livro Trabalhando Efetivamente com Código Legado do Michael Feathers — Working Effectively with Legacy Code by Michael Feathers — pois código sem testes é código legado!
- Os arquivos de exemplo do Jon Reid no Quality Coding são um ótimo lugar para aprender mais sobre Desenvolvimento Guiado a Teste — Test Driven Development.
Se você tiver algum comentário ou sugestão, deixe aqui no artigo.