Desenvolvemos diversos algoritmos recursivos e, para verificar se eles funcionavam corretamente, realizamos testes manuais executando as funções com diferentes entradas e verificando seus resultados. No entanto, à medida que nossos programas se tornam mais complexos, essa abordagem manual se torna ineficiente e propensa a erros. Neste capítulo, introduziremos o conceito de testes automatizados, que nos permitirá verificar de forma sistemática e confiável se nossas funções estão operando como esperado.
6.1 Por que Testar?
Quando escrevemos código, queremos ter certeza de que ele está funcionando corretamente. Os testes automatizados nos ajudam a:
Verificar se o código produz os resultados esperados para diferentes entradas
Identificar bugs e erros antes que o programa seja utilizado
Garantir que modificações no código não quebrem funcionalidades existentes
Documentar o comportamento esperado de nossas funções
Em desenvolvimento de software, uma prática comum é o TDD (Test-Driven Development), onde primeiro escrevemos os testes e depois implementamos o código que satisfaz esses testes. Esta abordagem incentiva um design mais claro e uma melhor compreensão dos requisitos antes mesmo de começar a programar.
6.2 Escrevendo Testes Com Condicionais
Vamos começar com uma abordagem mais simples para criar testes automatizados usando estruturas condicionais. Consideraremos o “Problema das Escadas” que vimos no capítulo anterior.
Relembrando, o problema consistia em determinar de quantas maneiras diferentes podemos subir uma escada com \(n\) degraus, se podemos dar passos de 1 ou 2 degraus por vez. Nossa solução foi:
functionmaneiras_subir_escada(n)# Casos baseif n ==0|| n ==1return1else# Caso recursivo: soma das maneiras de chegar a partir de n-1 e n-2returnmaneiras_subir_escada(n -1) +maneiras_subir_escada(n -2)endend
maneiras_subir_escada (generic function with 1 method)
Agora, vamos criar uma função de teste para verificar se nossa implementação está correta:
functiontesta_maneiras_subir_escada()# Geralmente, verificamos alguns casos conhecidos ou que sabemos a respostaifmaneiras_subir_escada(1) !=1println("Erro para n = 1")returnfalseendifmaneiras_subir_escada(2) !=2println("Erro para n = 2")returnfalseendifmaneiras_subir_escada(3) !=3println("Erro para n = 3")returnfalseendifmaneiras_subir_escada(4) !=5println("Erro para n = 4")returnfalseendprintln("Todos os testes para a função maneiras_subir_escada passaram!")returntrueend# Executamos os testestesta_maneiras_subir_escada()
Todos os testes para a função maneiras_subir_escada passaram!
true
Nesta função de teste, verificamos se a nossa implementação retorna os valores corretos para diferentes entradas. Se algum teste falhar, exibimos uma mensagem indicando qual caso falhou. Se todos os testes passarem, exibimos uma mensagem de sucesso.
Este é um princípio importante para testes automatizados: se o teste passar, ele deve indicar apenas que deu certo! Isso significa que, idealmente, os testes não devem imprimir muitas mensagens quando tudo estiver funcionando corretamente, apenas quando algo der errado.
Vamos fazer o mesmo para o cálculo do “Coeficiente Binomial”, que também vimos no capítulo anterior:
functioncoeficiente_binomial(n, k)if k ==0|| k == nreturn1elsereturncoeficiente_binomial(n -1, k -1) +coeficiente_binomial(n -1, k)endendfunctiontesta_coeficiente_binomial()ifcoeficiente_binomial(5, 2) !=10println("Erro para (5, 2)")returnfalseendifcoeficiente_binomial(10, 4) !=210println("Erro para (10, 4)")returnfalseendifcoeficiente_binomial(7, 3) !=35println("Erro para (7, 3)")returnfalseendprintln("Todos os testes para a função coeficiente_binomial passaram!")returntrueend# Executamos os testestesta_coeficiente_binomial()
Todos os testes para a função coeficiente_binomial passaram!
true
6.3 Testes com o Módulo Test
Até agora, criamos funções de teste manualmente usando estruturas condicionais. No entanto, Julia fornece um módulo de testes integrado chamado Test, que oferece funcionalidades mais avançadas para testes automatizados.
Vamos reescrever nossos testes usando o módulo Test:
usingTest@testset"Testes para maneiras_subir_escada"begin@testmaneiras_subir_escada(1) ==1@testmaneiras_subir_escada(2) ==2@testmaneiras_subir_escada(3) ==3@testmaneiras_subir_escada(4) ==5end@testset"Testes para coeficiente_binomial"begin@testcoeficiente_binomial(5, 2) ==10@testcoeficiente_binomial(10, 4) ==210@testcoeficiente_binomial(7, 3) ==35end
Test Summary: | Pass Total Time
Testes para maneiras_subir_escada | 4 4 0.1s
Test Summary: | Pass Total Time
Testes para coeficiente_binomial | 3 3 0.0s
Com o módulo Test, utilizamos a macro @testset para agrupar testes relacionados e a macro @test para verificar condições específicas. Se um teste falha, o módulo exibe automaticamente informações úteis sobre a falha, como a expressão que falhou e os valores esperados versus os valores obtidos.
Além disso, o módulo Test oferece outras macros úteis:
@test_throws: verifica se uma expressão lança uma exceção específica
@test_approx_eq: verifica se dois valores de ponto flutuante são aproximadamente iguais (considerando erros de arredondamento)
@test_broken: marca um teste que é esperado falhar (útil para documentar bugs conhecidos)
6.4 Mais Exemplos
Vamos implementar duas novas funções e seus respectivos testes: uma função para calcular a soma dos dígitos de um número e outra para verificar se um número é primo.
6.4.1 Soma dos Dígitos
Primeiramente, vamos criar uma função que calcula a soma dos dígitos de um número inteiro. Por exemplo, para o número 123, a soma dos dígitos seria 1 + 2 + 3 = 6.
Antes de implementar a função, vamos pensar nos casos de teste:
Se a função recebe um inteiro de um único dígito ela deve retornar esse dígito
Se a função recebe 100, ela deve retornar 1 + 0 + 0 = 1
Se a função recebe 123, ela deve retornar 1 + 2 + 3 = 6
Se a função recebe 99, ela deve retornar 9 + 9 = 18
Podemos implementar a função usando recursão. A ideia é “descascar” o número, extraindo um dígito de cada vez:
functionsoma_digitos(n)if n <=0return0else# Obtemos o último dígito com o resto da divisão por 10 ultimo_digito = n %10# Removemos o último dígito com a divisão inteira por 10 resto_numero = n ÷10# Somamos o último dígito com a soma dos dígitos do resto do númeroreturn ultimo_digito +soma_digitos(resto_numero)endend
soma_digitos (generic function with 1 method)
Os casos de teste discutidos acima podem ser implementados utilizando o módulo Test:
@testset"Testes para soma_digitos"begin@testsoma_digitos(0) ==0@testsoma_digitos(1) ==1@testsoma_digitos(100) ==1@testsoma_digitos(123) ==6@testsoma_digitos(99) ==18end
Test Summary: | Pass Total Time
Testes para soma_digitos | 5 5 0.0s
Vamos criar uma função para verificar se um número é primo. Um número primo é aquele que é divisível apenas por 1 e por ele mesmo. Antes de escrever a função vamos pensar nos testes:
Por definição, qualquer número menor ou igual a 1 não é primo
O número 2 é primo (fácil de verificar)
O número 3 é primo (também fácil de verificar)
O número 4 não é primo, pois 2 também divide 4
O número 17 é primo
O número 25 não é primo, pois 5 também divide 25
Podemos implementar a função usando uma abordagem recursiva que tenta dividir o número por cada inteiro de 2 até a raiz quadrada do número:
functionverifica_divisor(n, divisor)# Se encontramos um divisor, o número não é primoif n % divisor ==0returnfalse# Se já testamos até a raiz quadrada, o número é primoelseif divisor * divisor > nreturntrueelse# Continua verificando com o próximo divisorreturnverifica_divisor(n, divisor +1)endendfunctione_primo(n)if n <=1# Por definiçãoreturnfalseelseif n ==2# Primeiro primoreturntrueelse# Verifica se n tem algum divisor começando com 2returnverifica_divisor(n, 2)endend
e_primo (generic function with 1 method)
Os testes podem ser escritos como:
@testset"Testes para e_primo"begin@teste_primo(2) ==true@teste_primo(3) ==true@teste_primo(4) ==false@teste_primo(17) ==true@teste_primo(25) ==falseend
Test Summary: | Pass Total Time
Testes para e_primo | 5 5 0.0s