Matteo Manferdini

Muitos desenvolvedores acham os testes unitários confusos. Isto é agravado pelas técnicas avançadas que você precisa para testar classes ou código assíncrono, como o de pedidos de rede.

Na realidade, na base dos testes unitários, existem conceitos fundamentais simples. Uma vez que você os entende, os testes unitários de repente se tornam mais acessíveis.

Conteúdo

  • Teste unitário explicado em simples, termos compreensíveis
  • Testes unitários permitem verificar caixas de bordas de difícil acesso e evitar bugs
  • Testes unitários de escrita em Xcode
  • Uma boa arquitetura torna o seu código mais fácil de testar
  • Tratar tipos de valores como funções puras para simplificar os testes unitários
  • Verificando seu código usando as funções de asserções da estrutura XCTest
  • Medindo a cobertura de código de um teste em Xcode
  • Por que apontar para a cobertura de código específico não faz sentido e o que você deve fazer em seu lugar

Teste de unidade explicado em simples, termos compreensíveis

Quando você escreve um aplicativo, você passa por um ciclo de escrita de código, executando-o, e verificando se ele funciona como pretendido. Mas, à medida que seu código cresce, torna-se mais difícil testar toda a sua aplicação manualmente.

Testes automatizados podem executar esses testes para você. Assim, praticamente falando, teste unitário é código para ter certeza que o código do seu aplicativo está correto.

A outra razão pela qual testes unitários podem ser difíceis de entender é que muitos conceitos o cercam: teste em código X, arquitetura do aplicativo, duplicação de testes, injeção de dependência, e assim por diante.

Mas, no seu cerne, a idéia de teste unitário é bastante simples. Você pode escrever testes unitários simples em Swift simples. Você não precisa de nenhum recurso especial de código X ou técnica sofisticada, e você pode até mesmo executá-los dentro de um Swift Playground.

Vejamos um exemplo. A função abaixo calcula o fatorial de um inteiro, que é o produto de todos os inteiros positivos menor ou igual ao input.

1
2
3
4
5
6
7

>

func factorial(de número: Int) -> Int {
var resultado = 1
para fator em 1…número {
resultado = resultado * fator
}
resultado do retorno
}

Para testar esta função manualmente, você a alimentaria com alguns números e verificaria se o resultado é o que você espera. Então, por exemplo, você tentaria em quatro e verificaria se o resultado é 24.

Você pode automatizar isso, escrevendo uma função Swift que faz o teste para você. Por exemplo:

1
2
3
4
5

func testFactorial() {
if factorial(of: 4) != 24 {
print(“O factorial de 3 está errado”)
}
>

Este é um simples código Swift que todos podem entender. Tudo o que ele faz é verificar a saída da função e imprimir uma mensagem se ela não estiver correta.

É um teste unitário em poucas palavras. Tudo o resto são sinos e apitos adicionados pelo Xcode para tornar o teste mais simples.

Or pelo menos, esse seria o caso se o código em nossos aplicativos fosse tão simples quanto a função factorial que acabamos de escrever. É por isso que você precisa do suporte do Xcode e técnicas avançadas como test doubles.

Não obstante, esta idéia é útil tanto para testes simples quanto complexos.

Testes unitários permitem que você verifique casos difíceis de alcançar e previna bugs

Você pode obter vários benefícios dos testes unitários.

O mais óbvio é que você não tem que testar seu código continuamente manualmente. Pode ser tedioso executar seu aplicativo e chegar a um local específico com a funcionalidade que você quer testar.

Mas isso não é tudo. Os testes unitários também permitem que você teste casos de borda que podem ser difíceis de criar em uma aplicação em execução. E os casos de borda são onde a maioria dos bugs vivem.

Por exemplo, o que acontece se alimentarmos zero ou um número negativo da nossa função factorial(de:) de cima?

1
2
3
4
5

func testFactorialOfZero() {
if factorial(of: 0) != 1 {
print(“O factorial de 0 está errado”)
}
>

As nossas quebras de código:

Numa aplicação real, este código causaria uma quebra. Zero está fora do intervalo no for loop.

Mas nosso código não falha por causa de uma limitação dos intervalos Swift. Ele falha porque nós esquecemos de considerar inteiros não-positivos em nosso código. Por definição, o fatorial de um número inteiro negativo não existe, enquanto o fatorial de 0 é 1.

1
2
3
4
5
6
7
8
9
10
11
12
13

func factorial(de número: Int) -> Int? {
if (número: < 0) {
return nil
}
if (número == 0) {
return 1
}
var resultado = 1
para fator em 1…número {
resultado = resultado * fator
}
resultado de retorno
}

Agora podemos executar nossos testes novamente e até mesmo adicionar um cheque para números negativos.

1
2
3
4
5

teste funcFactorialOfNegativoInteger() {
if factorial(of: -1) != nulo {
print(“O factorial de -1 está errado”)
}
>

Aqui você vê o último e, na minha opinião, o benefício mais importante dos testes unitários. Os testes que escrevemos anteriormente garantem que, à medida que atualizamos nossa(s) função(s) factorial(ais), não quebramos seu código existente.

Isso é crucial em aplicativos complexos, onde você tem que adicionar, atualizar e excluir código continuamente. Os testes unitários dão a você a confiança de que suas alterações não quebraram o código que estava funcionando bem.

Escrevendo testes unitários no Xcode

Com um entendimento dos testes unitários, agora podemos olhar para as características de teste do Xcode.

Quando você cria um novo projeto Xcode, você tem a chance de adicionar testes unitários imediatamente. Eu vou usar, como exemplo, um aplicativo para rastrear calorias. Você pode encontrar o projeto Xcode completo aqui.

Quando você marcar essa opção, seu projeto modelo incluirá um alvo de teste já configurado. Você sempre pode adicionar um mais tarde, mas eu normalmente marquei a opção por padrão. Mesmo que eu não vá escrever testes imediatamente, tudo será configurado quando eu decidir.

O alvo de teste já começa com algum código de template para os nossos testes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

importação XCTest
@testable import Calories
classe CaloriesTests: XCTestCase {
override func setUpWithError() lança {
// Coloque o código de configuração aqui. Este método é chamado antes da invocação de cada método de teste na classe.
}
anular func tearDownWithError() lança {
// Põe o código teardown aqui. Este método é chamado após a invocação de cada método de teste na classe.
}
func testExample() lança {
// Este é um exemplo de um caso de teste funcional.
// Use o XCTAssert e funções relacionadas para verificar se os seus testes produzem os resultados corretos.
}
func testPerformanceExample() lança {
// Este é um exemplo de um caso de teste de desempenho.
self.measure {
// Coloque o código que você quer medir o tempo aqui.
}
}
>

  • A linha @testable import Calories dá-lhe acesso a todos os tipos no alvo principal do seu projecto. O código de um módulo Swift não é acessível do exterior a menos que todos os tipos e métodos sejam explicitamente declarados como públicos. A palavra-chave @testable permite que você contorne esta limitação e acesse até o código privado.
  • A classe CaloriesTests representa um caso de teste, ou seja, um agrupamento de testes relacionados. Uma classe de caso de teste precisa descer do XCTestCase.
  • O método setUpWithError() permite que você configure um ambiente padrão para todos os testes em um caso de teste. Se você precisar fazer alguma limpeza após seus testes, você usa tearDownWithError(). Estes métodos funcionam antes e depois de cada teste em um caso de teste. Nós não vamos precisar deles, então você pode apagá-los.
  • O método testExample() é um teste como os que escrevemos para o nosso exemplo factorial. Você pode nomear os métodos de teste como quiser, mas eles precisam começar com o prefixo de teste para que o Xcode os reconheça.
  • E finalmente, o testPerformanceExample() mostra como medir o desempenho do seu código. Os testes de desempenho são menos padrão que os testes unitários, e você os usa apenas para código crucial do que o necessário para ser rápido. Nós também não usaremos este método.

Você pode encontrar todos os seus casos de teste e testes relativos listados no navegador de testes do Xcode.

Você pode adicionar novos casos de teste ao seu projeto, simplesmente criando novos .swift com código como o acima, mas é mais fácil usar o menu de adição na parte inferior do navegador de testes.

Uma boa arquitetura torna o seu código mais fácil de testar

Num projeto real, você normalmente não tem funções soltas como no nosso exemplo factorial. Ao invés disso, você tem estruturas, enumerações e classes representando as várias partes do seu app.

É crucial organizar seu código de acordo com um padrão de design como MVC ou MVVM. Ter código bem arquitetado não só torna o seu código bem organizado e fácil de manter. Ele também facilita os testes unitários.

Isso também nos ajuda a responder uma pergunta comum: qual código você deve testar primeiro?

A resposta é: comece pelos seus tipos de modelo.

Existem várias razões para isso:

  • Se você seguir o padrão MVC ou uma de suas derivadas, seus tipos de modelo conterão a lógica de negócios do domínio. Todo o seu aplicativo depende da exatidão desse código, portanto, mesmo alguns testes têm um impacto significativo.
  • Os tipos de modelos são muitas vezes estruturas, que são tipos de valores. Estes são muito mais fáceis de testar do que os tipos de referência (veremos porque em um momento).
  • Tipos de referência, ou seja, classes, têm dependências e causam efeitos colaterais. Para escrever testes unitários para estes, você precisa de testes duplos (dummies, stubs, fakes, spies e mocks) e usar técnicas sofisticadas.
  • Controllers (em termos MVC) ou modelos de visualização (em termos MVVM) são frequentemente conectados a recursos como armazenamento em disco, uma base de dados, a rede, e sensores de dispositivos. Eles também podem executar código paralelo. Estes tornam os testes unitários mais difíceis.
  • Ver código é o que muda mais frequentemente, produzindo testes quebradiços que quebram facilmente. E ninguém gosta de corrigir testes quebrados.

Tratando tipos de valores como funções puras para simplificar os testes unitários

Então, vamos adicionar alguns tipos de modelos à nossa aplicação.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

>

Structure FoodItem {
let name: String
let caloriesFor100Grams: Int
let gramas: Int
struct Meal {
private(set) var items: =
var calorias: Int {
var calorias = 0
para item em itens {
calorias += (item.caloriasPara100Gramas / 100) * item.gramas
}
calorias de retorno
}
func add(_ item: FoodItem) {
items.append(item)
}
>

A estrutura de refeições contém alguma lógica simples para adicionar itens alimentares e calcular o total de calorias dos alimentos que contém.

Os testes unitários têm esse nome porque você deve testar o seu código em unidades separadas, removendo todas as dependências (testar as dependências em conjunto é, em vez disso, chamado de teste de integração). Cada unidade é então tratada como uma caixa preta. Você alimenta algumas entradas e testa sua saída para verificar se é o que você espera.

Isso foi fácil no caso do nosso exemplo factorial porque era uma função pura, ou seja, uma função onde a saída depende apenas da entrada, e que não cria nenhum efeito colateral.

Mas esse não parece ser o caso do nosso tipo Meal, que contém estado.

O valor retornado pela propriedade calorias computadas não tem entrada. Depende apenas do conteúdo do array de itens. O método add(_:), ao invés disso, não retorna nenhum valor e apenas altera o estado interno de uma refeição.

Mas estruturas são tipos de valores, e podemos considerá-los como puras funções. Como eles não têm dependências, você pode considerar o estado inicial de uma estrutura como o input de um método e seu estado após chamar o método como o output.

(Esta é uma das razões pelas quais você deve se abster de colocar tipos de referência dentro de tipos de valores).

Verificando seu código usando as funções de asserções do framework XCTest

Temos agora algum código para testar, e um lugar no nosso projeto Xcode onde escrever os testes.

Falta-nos um último ingrediente: uma forma de expressar se nosso código está correto ou não.

No exemplo no início deste artigo, eu usei declarações de impressão simples. Isso não é prático para testes unitários reais. Você não quer perder tempo examinando os registros para identificar quais testes falharam. Precisamos de uma forma que aponte directamente para os testes que falharam.

Na estrutura XCTest, você encontra uma série de funções de afirmação que fazem o Xcode apontar para testes que não passam.

Com esses, podemos escrever o nosso primeiro teste.

Como mencionei acima, os testes unitários são úteis para verificar os casos de limite, por isso vamos começar a garantir que uma refeição vazia não tem calorias.

1
2
3
4
5
6

classe CaloriasTestes: XCTestCase {
func testEmptyMeal() lança {
let meal = Meal()
XCTAssertEqual(refeição.calorias, 0, “Uma refeição vazia deve ter 0 calorias”)
}
>

O XCTest oferece uma função genérica XCTAssert que lhe permite afirmar qualquer condição. Você poderia usar isso para qualquer teste que você escreva, mas é melhor usar funções de afirmação mais especializadas quando possível, como XCTAssertEqual (acima), XCTAssertNil, e outras. Estes produzem melhores erros no código X do que XCTAssert.

Você pode executar um único teste clicando no diamante ao lado do seu nome na calha do editor de código.

Quando um teste passa, ele se torna verde, enquanto os testes reprovados são marcados em vermelho. Você pode mudar o 0 no teste para qualquer outro número para ver como um teste falhado fica.

Você pode executar todos os testes em um caso de teste clicando no diamante ao lado da declaração de classe. Você também pode usar o navegador de testes que vimos acima para executar testes individuais ou casos de teste.

Você muitas vezes quer executar todos os testes no seu projeto de uma só vez, o que você pode fazer rapidamente pressionando cmd+U no seu teclado ou selecionando a opção Teste no menu Produto do Xcode.

Medindo a cobertura de código de um teste no Xcode

A próxima pergunta mais comum sobre testes unitários é: quanto do seu código você deve testar?

Esta é normalmente medida pela cobertura de código, ou seja a percentagem de código coberta pelos seus testes. Então, antes de respondermos essa pergunta, vamos ver como você pode medir a cobertura do código dos seus testes no Xcode.

Primeiro de tudo, você precisa deixar o Xcode reunir a cobertura para o seu alvo de teste. Clique no botão com o nome do projeto no controle segmentado na barra de ferramentas do Xcode (ao lado do botão de parada). Depois, selecione Edit scheme… no menu pop-up.

There, selecione Test in the outline, depois vá para a aba Options. E, finalmente, selecione Gather coverage for. Você pode deixar sua opção para todos os alvos.

Pode agora reexecutar seus testes, o que permitirá que o Xcode reúna os dados de cobertura. Você encontrará o resultado no navegador do Relatório, selecionando o item Código de cobertura no esquema.

Aqui, você pode verificar as porcentagens de código cobertas pelos seus testes para todo o projeto e cada arquivo.

Estes números não são tão significativos para o nosso exemplo, já que quase não escrevemos nenhum código. Ainda assim, podemos ver que o método add(_:) do tipo Meal tem uma cobertura de 0%, o que significa que ainda não o testámos.

A propriedade calorias computadas tem, em vez disso, uma cobertura de 85,7%, o que significa que existem alguns caminhos de execução no nosso código que o nosso teste não desencadeou.

No nosso exemplo simples, é fácil de perceber que caminho é esse. Nós apenas testamos as calorias de uma refeição vazia, então o código no for loop não foi executado.

Em métodos mais sofisticados, porém, pode não ser tão simples.

Para isso, você pode trazer a faixa de cobertura de código no editor Xcode. Você pode encontrar isso no menu Adjust Editor Options no canto superior direito.

Esta irá revelar uma tira que lhe mostra a cobertura para cada linha de código.

Os números dizem-lhe quantas vezes cada linha foi executada durante os testes. Em vermelho, você pode ver quais linhas não foram executadas (completas) ou executadas apenas parcialmente (listradas).

Por que apontar para uma cobertura específica de código não faz sentido e o que você deve fazer em vez disso

Então, que porcentagem é uma boa porcentagem de cobertura de código?

Ospiniões são vastamente diferentes. Alguns desenvolvedores pensam que 20% é o suficiente. Outros vão para números mais altos, como 70% ou 80%. Há até desenvolvedores que acreditam que apenas 100% é aceitável.

Na minha opinião, a cobertura de código é uma métrica sem sentido. Você pode usá-la para informar suas decisões sobre os testes, mas você não deve tratá-la como um objetivo que você tem que atingir.

Para ver porque vamos escrever outro teste para cobrir o código que ainda não testamos.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

classe CaloriesTests: XCTestCase {
let banana = FoodItem(nome: “Banana”, caloriasPara100Gramas: 89, gramas: 118)
let steak = FoodItem(nome: “Bife”, caloriasPara100Gramas: 271, gramas: 225)
let GoatCheese = FoodItem(name: “Goat Cheese”, caloriesFor100Gramas: 364, gramas: 28)
func testEmptyMeal() throws {
let meal = Meal()
XCTAssertEqual(refeição).calorias, 0, “Uma refeição vazia deve ter 0 calorias”)
}
func testCalories() {
var meal = Meal()
meal.add(banana)
meal.add(bife)
meal.add(bife)
meal.add(bode.queijo)
XCTAssertEqual(meal.items.count, 3)
XCTAssertEqual(meal.calories, 534)
}
>

Se você reexecutar todos os testes, você terá uma cobertura de 100% para o arquivo Model.swift. Parece bom, certo?

Agora, vá e remova o método testEmptyMeal(). Se você executar o teste restante sozinho, você verá que a cobertura para os nossos tipos de refeição ainda é 100%.

Isso já mostra que o número de 100% pode lhe dar uma falsa sensação de segurança. Você sabe que agora nós não estamos testando todos os casos de limite para a propriedade calorias computadas. E ainda assim, nossa cobertura de teste não reflete que.

Eu adicionaria que 100% de cobertura não só é enganosa como até prejudicial. Algum código em seu projeto é propenso a mudanças constantes, especialmente o código mais novo e especialmente nas visualizações.

Cobrir 100% dele significa que você só terá que produzir testes quebradiços que quebram continuamente, e você tem que consertar. Estes testes não detectam bugs. Eles quebram porque a funcionalidade do seu código muda.

E aqui chegamos ao ponto que ninguém fala sobre.

Testes de escrita não é divertido, apesar do que alguns desenvolvedores afirmam. Corrigir testes é ainda menos divertido. Faça isso em demasia, e você vai odiar os testes unitários e colocá-los de lado por completo.

Psychology é tão essencial para o desenvolvimento de software quanto as melhores práticas. Nós não somos máquinas.

Então, qual o número que você deve apontar? Isso requer uma discussão mais extensa, mas aqui vou dar um resumo da minha opinião.

Escrever testes úteis, começando com o código mais antigo do seu projeto que não vai mudar. Use a cobertura de código apenas para identificar qual código ainda não foi testado, mas não o use como um objetivo ou para decidir quando escrever mais testes. 20% de cobertura é melhor que 80% se seus testes forem significativos, e você está testando o código certo.

Como mencionei acima, os tipos de modelo contêm o código mais crítico em seu projeto, que também tende a mudar o menos. Então, comece por aí.

Eu normalmente não me preocupo em testar cada um dos caminhos de execução. Por exemplo, em um projeto real, eu não escreveria o método testEmptyMeal() que eu mostrei acima. Sim, é crucial para testar casos de borda, mas nem todos são importantes.

Eu normalmente escreveria apenas o teste testCalories(). Isso me diz que meu código funciona, e me avisará se, mais tarde, eu cometer um erro ao mudar este método.

Certo, ele não cobre todos os caminhos, mas este código é tão simples que isso é apenas uma perda de tempo. Gastar seu tempo escrevendo código real para recursos que ajudam seus usuários é mais importante do que testar cada caminho de execução.

Eu sei que vou obter algum flack de alguns desenvolvedores para esta opinião, mas eu não me importo. A cobertura de testes é um daqueles debates que nunca terminará.

O código que você escreve normalmente está na maioria das vezes certo. Não há necessidade de ser paranóico. Ter uma infinidade de testes que quebram toda vez que você muda alguma coisa é um passivo, não um ativo.

Seu tempo e força de vontade são limitados. Gaste-os em tarefas mais importantes.

Arquitectar aplicações SwiftUI com MVC e MVVM

É fácil fazer uma aplicação atirando algum código junto. Mas sem as melhores práticas e arquitetura robusta, você logo termina com um código de spaghetti incontrolável. Neste guia vou mostrar-lhe como estruturar correctamente as aplicações SwiftUI.

GET THE FREE BOOK NOW

Deixe uma resposta

O seu endereço de email não será publicado.