6  Testes Automatizados

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:

  1. Verificar se o código produz os resultados esperados para diferentes entradas
  2. Identificar bugs e erros antes que o programa seja utilizado
  3. Garantir que modificações no código não quebrem funcionalidades existentes
  4. 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:

function maneiras_subir_escada(n)
    # Casos base
    if n == 0 || n == 1
        return 1
    else
        # Caso recursivo: soma das maneiras de chegar a partir de n-1 e n-2
        return maneiras_subir_escada(n - 1) + maneiras_subir_escada(n - 2)
    end
end
maneiras_subir_escada (generic function with 1 method)

Agora, vamos criar uma função de teste para verificar se nossa implementação está correta:

function testa_maneiras_subir_escada()
    # Geralmente, verificamos alguns casos conhecidos ou que sabemos a resposta
    if maneiras_subir_escada(1) != 1
        println("Erro para n = 1")
        return false
    end
    
    if maneiras_subir_escada(2) != 2
        println("Erro para n = 2")
        return false
    end
    
    if maneiras_subir_escada(3) != 3
        println("Erro para n = 3")
        return false
    end
    
    if maneiras_subir_escada(4) != 5
        println("Erro para n = 4")
        return false
    end
    
    println("Todos os testes para a função maneiras_subir_escada passaram!")
    return true
end

# Executamos os testes
testa_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:

function coeficiente_binomial(n, k)
    if k == 0 || k == n
        return 1
    else
        return coeficiente_binomial(n - 1, k - 1) + coeficiente_binomial(n - 1, k)
    end
end

function testa_coeficiente_binomial()
    if coeficiente_binomial(5, 2) != 10
        println("Erro para (5, 2)")
        return false
    end
    
    if coeficiente_binomial(10, 4) != 210
        println("Erro para (10, 4)")
        return false
    end
    
    if coeficiente_binomial(7, 3) != 35
        println("Erro para (7, 3)")
        return false
    end
    
    println("Todos os testes para a função coeficiente_binomial passaram!")
    return true
end

# Executamos os testes
testa_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:

using Test

@testset "Testes para maneiras_subir_escada" begin
    @test maneiras_subir_escada(1) == 1
    @test maneiras_subir_escada(2) == 2
    @test maneiras_subir_escada(3) == 3
    @test maneiras_subir_escada(4) == 5
end

@testset "Testes para coeficiente_binomial" begin
    @test coeficiente_binomial(5, 2) == 10
    @test coeficiente_binomial(10, 4) == 210
    @test coeficiente_binomial(7, 3) == 35
end
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
Test.DefaultTestSet("Testes para coeficiente_binomial", Any[], 3, false, false, true, 1.74526253061582e9, 1.745262530615851e9, false, "/Users/lucas/Desktop/livro-intro-comp-julia/chapters/06-testes-automatizados.qmd")

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:

function soma_digitos(n)
    if n <= 0
        return 0
    else
        # 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úmero
        return ultimo_digito + soma_digitos(resto_numero)
    end
end
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
    @test soma_digitos(0) == 0
    @test soma_digitos(1) == 1
    @test soma_digitos(100) == 1
    @test soma_digitos(123) == 6
    @test soma_digitos(99) == 18
end
Test Summary:            | Pass  Total  Time
Testes para soma_digitos |    5      5  0.0s
Test.DefaultTestSet("Testes para soma_digitos", Any[], 5, false, false, true, 1.745262530772914e9, 1.745262530775204e9, false, "/Users/lucas/Desktop/livro-intro-comp-julia/chapters/06-testes-automatizados.qmd")

6.4.2 Verificação de Números Primos

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:

function verifica_divisor(n, divisor)
    # Se encontramos um divisor, o número não é primo
    if n % divisor == 0
        return false
    # Se já testamos até a raiz quadrada, o número é primo
    elseif divisor * divisor > n
        return true
    else
        # Continua verificando com o próximo divisor
        return verifica_divisor(n, divisor + 1)
    end
end

function e_primo(n)
    if n <= 1 # Por definição
        return false
    elseif n == 2 # Primeiro primo
        return true
    else
        # Verifica se n tem algum divisor começando com 2
        return verifica_divisor(n, 2)
    end
end
e_primo (generic function with 1 method)

Os testes podem ser escritos como:

@testset "Testes para e_primo" begin
    @test e_primo(2) == true
    @test e_primo(3) == true
    @test e_primo(4) == false
    @test e_primo(17) == true
    @test e_primo(25) == false
end
Test Summary:       | Pass  Total  Time
Testes para e_primo |    5      5  0.0s
Test.DefaultTestSet("Testes para e_primo", Any[], 5, false, false, true, 1.745262530786754e9, 1.745262530794611e9, false, "/Users/lucas/Desktop/livro-intro-comp-julia/chapters/06-testes-automatizados.qmd")

6.5 Verifique seu Aprendizado

  1. Qual é a diferença entre usar estruturas condicionais e o módulo Test para testes automatizados?
  2. Por que os testes automatizados são importantes no desenvolvimento de software?

6.6 Explore por Conta Própria

  1. Explore outras macros disponíveis no módulo Test de Julia e experimente usá-las em seus próprios testes.