CSE 111: Programação com Funções

S03 - Atividade de Aprendizagem (1 de 2): Testando Funções

Durante esta aula, você aprenderá como usar uma abordagem mais sistemática para desenvolver códigos. Especificamente, você aprenderá a escrever funções de teste que verificam automaticamente se as funções do programa estão corretas. Você aprenderá como usar um módulo do Python, chamado pytest para executar suas funções de teste e aprenderá como ler a saída do pytest, para ajudá-lo a encontrar e corrigir erros em seu código.

Conceitos

Aqui estão os conceitos e assuntos de programação em Python que você deve aprender durante esta aula:

Testes Ineficientes

Durantes suas aulas anteriores, você testou programas executando-os, digitando a entrada do usuário, lendo a saída do programa e verificando que a saída estava correta. Esta é uma forma válida de testar o programa. No entanto, é demorado, tedioso e propenso a erros. Uma maneira muito melhor de testar um programa é testar suas funções individualmente e escrever funções de teste separadas que verifiquem automaticamente se as funções do programa estão corretas.

Neste curso, você escreverá funções de teste em um arquivo Python separado do seu programa Python. Em outras palavras, você manterá o código normal do programa e o código de teste em arquivos separados.

Instruções Assert

Em um programa de computador, uma asserção é uma instrução que faz com que o computador verifique se uma comparação é verdadeira. Se for, ele continuará a executar o código no programa. Caso contrário, gerará um AssertionError, o que provavelmente fará com que o programa seja encerrado. (Na Semana 05, você aprenderá a escrever um código para lidar com erros, de modo que um programa não seja encerrado quando o computador indicar um erro.)

Um programador escreve asserções em um programa para informar o computador sobre comparações que devem ser verdadeiras, para que o programa seja executado com sucesso. A palavra-chave Python para escrever uma asserção é assert. Imagine um programa usado por um banco para rastrear saldos de contas, depósitos e saques. Um programador pode escrever as primeiras linhas da função deposito dessa forma:

def deposito(valor):
  # Para que este programa funcione corretamente e
  # para que os registros bancários estejam corretos, não devemos
  # permitir que alguém deposite um valor zero ou negativo.
  assert valor > 0 
  ⋮
  

A instrução assert na linha  5 do exemplo anterior fará com que o computador verifique se o valor é maior que zero (0). Se o valor for maior que zero, o computador continuará executando o programa. Entretanto, se o valor for zero ou menor que zero (ou seja, negativo), o computador gerará um AssertionError, o que provavelmente fará com que o programa seja encerrado.

Um programador pode escrever qualquer comparação válida do Python em uma instrução assert. Aqui estão alguns exemplos de vários programas não relacionados:

assert temperatura < 0
assert len(primeiro_nome) > 0
assert saldo == 0
assert ano_letivo != "3º ano do ensino médio"

Módulo pytest

pytest é um módulo Python de terceiros que facilita a escrita e execução de funções de teste. Embora existam outros módulos de teste em Python, o pytest é considerado um dos mais fáceis de usar. Por ser um módulo de terceiros, pytest não é incluído na instalação padrão do Python. Isso significa que, ao instalar o Python no seu computador, o pytest não vem junto, e você precisará instalá-lo separadamente. Nesta aula, você usará um módulo padrão do Python chamado pip para instalar o pytest.

O pytest permite que um programador escreva funções de teste simples. O nome de cada função de teste deve começar com " test_", e cada função de teste deve usar a instrução assert do Python para verificar se uma função de programa retorna um resultado correto. Por exemplo, se quisermos verificar se a função min interna funciona corretamente, poderíamos escrever uma função de teste desta forma:

def test_min():
  assert min(7, -3, 0, 2) == -3

Na função de teste anterior, a instrução assert fará com que o computador primeiro chame a função min e passe 7, −3, 0 e 2 como argumentos para a função min. A função min encontrará o valor mínimo dos seus parâmetros e retornará esse valor mínimo. Então a instrução assert comparará o valor mínimo retornado com −3. Se o valor retornado não for −3, a instrução assert gerará uma exceção que fará com que o pytest imprima uma mensagem de erro.

Caso queira verificar a documentação original do pytest, acesse este link pytest (conteúdo em inglês)

Comparando Números de Ponto Flutuante

Na memória de um computador, tudo (todos os números, textos, sons, imagens, filmes, etc.) é armazenado usando o sistema numérico binário. Ao executar um programa Python, um computador armazena números inteiros em binário, de uma forma que representa exatamente os números inteiros. Por exemplo, um computador armazena o número inteiro 23 como 00010111 em binário, que é uma representação exata do decimal 23. Entretanto, um computador aproxima números de ponto flutuante (números com dígitos após a casa decimal). Por exemplo, ao executar um programa Python, um computador armazena o número de ponto flutuante 23.7 como o binário 0100000000110111101100110011001100110011001100110011001100110011. Este número binário é na verdade 23.69999999999999928945726424 em decimal, que é uma aproximação de 23.7

Como os computadores aproximam números de ponto flutuante, devemos compará-los cuidadosamente em nossas funções de teste. É uma prática ruim verificar se números de ponto flutuante são iguais usando apenas o operador de igualdade (==). Uma maneira melhor de comparar dois números de ponto flutuante é subtraí-los e verificar se a diferença é pequena, como mostrado na linha  6 do exemplo  4.

# Exemplo 4
# A variável "e" e a variável "f" podem ser quaisquer números
# de pontos flutuante de um cálculo matemático.
e = 7.135
f = 7.128
if abs(e - f) < 0.01:
    print(f"{e} e {f} são próximos o suficiente.")
    print("Vamos considerá-los iguais.")
else:
    print(f"{e} e {f} não são próximos o suficiente,")
    print("portanto não são iguais.")

No exemplo  4 na linha 6, se a diferença entre e e f for menor que 0.01, o computador considerará os dois números como sendo iguais. O número 0.01 na comparação na linha  6 é chamado de tolerância. A tolerância é a maior diferença entre dois números de ponto flutuante que o programador aceita para ainda considerar que os números são iguais.

Função approx

A comparação no exemplo  4 na linha  6 é um pouco tediosa de escrever e ler. Além disso, às vezes é difícil escolher a tolerância. O módulo pytest contém uma função chamada approx, para nos ajudar a comparar números de ponto flutuante mais facilmente. A função approx * (conteúdo em inglês) compara dois números de ponto flutuante e retorna True se eles forem iguais dentro de uma tolerância apropriada.

A função approx tem o seguinte cabeçalho:

def approx(valor_esperado, rel=None, abs=None, nan_ok=False)

Observe que os três últimos parâmetros da função approx têm valores padrão: rel=None, abs=None, nan_ok=False. Como eles têm valores padrão, quando chamamos approx, não precisamos passar argumentos para os três últimos parâmetros. Em outras palavras, em uma função de teste, podemos chamar approx dessa forma:

def test_funcao():
  assert valor_real == approx(valor_esperado)

Se chamarmos a função approx com apenas um argumento, ela comparará o valor real com o valor esperado e retornará True se a diferença entre os dois valores for menor que valor_esperado / 1000000, que é a tolerância padrão.

No entanto, essa tolerância pode não ser adequada em todos os casos. A função approx possui dois parâmetros opcionais. A função approx possui dois parâmetros opcionais, rel e abs, que permitem definir uma tolerância mais apropriada para a comparação.

Por exemplo, para testar a função math.sqrt, podemos escrever:

# Exemplo 5
def test_sqrt():
  assert math.sqrt(5) == approx(2.24, rel=0.01)

Observe o argumento nomeado rel na linha  3 do exemplo anterior. Esse argumento faz com que a função approx calcule a tolerância com base no valor esperado. Assim, a instrução assert verifica se o valor real retornado de math.sqrt(5) está dentro de 1% (0.01) de 2.24.

Quando usamos o parâmetro rel, a função approx executa um código semelhante ao exemplo  6 para determinar se os valores reais e esperados são iguais

# Exemplo 6
# Calcula a tolerância.
tolerancia = valor_esperado * rel
# Usa a tolerância para determinar se os valores
# reais e esperados são próximos o suficiente para serem
# considerados iguais.
if abs(valor_real - valor_esperado) < tolerancia:
    return True
else:
    return False

Aprendemos a partir das linhas  3 e  7 do exemplo  6, que approx retornará True se a diferença entre o valor real retornado de math.sqrt(5) e o valor esperado for menor que 0.0224 (2.24 * 0.01).

Também podemos usar o argumento abs para dar uma tolerância a approx. Podemos escrever um teste para a função math.sqrt dessa forma:

# Exemplo 7
def test_sqrt():
    assert math.sqrt(5) == approx(2.24, abs=0.01)

Observe o argumento nomeado abs na linha  3 do exemplo anterior. O argumento nomeado abs faz com que a função approx retorne True se a diferença entre os valores reais e esperados for menor que o número em abs (0.01 no exemplo anterior). Isso é diferente do argumento nomeado rel, que faz com que approx retorne True, se a diferença for menor que rel * valor_esperado. O argumento chamado abs é mais simples e fácil de entender do que o argumento chamado rel.

Como Testar uma Função

Para testar uma função, você deve fazer o seguinte:

  1. Escreva uma função que faça parte do seu programa Python normal.
  2. Pense em diferentes valores de parâmetros que farão com que o computador execute todo o código em sua função e possivelmente farão com que sua função falhe ou retorne um resultado incorreto.
  3. Em um arquivo Python separado, escreva uma função de teste que chame a sua função de programa e use uma instrução assert para verificar automaticamente se o valor retornado da função do seu programa está correto.
  4. Usa pytest para executar a função de teste.
  5. Leia a saída do pytest e use-a para ajudar a encontrar e corrigir erros tanto na função de programa quanto na função de teste.
Exemplo

Abaixo está uma função simples chamada fahr_para_celsius, que converte uma temperatura de Fahrenheit para Celsius e retorna a temperatura em Celsius. A função fahr_para_celsius faz parte de um programa Python maior em um arquivo chamado clima.py.

# clima.py
def fahr_para_celsius(fahr):
  """Converter uma temperatura em Fahrenheit para
  Celsius e retornar a temperatura em Celsius.
  """
  celsius = (fahr - 32) * 5 / 9
  return celsius

Queremos testar a função fahr_para_celsius. No cabeçalho de função na linha  2 em clima.py, vemos que fahr_para_celsius recebe um parâmetro chamado fahr. Para testar adequadamente esta função, devemos chamá-la pelo menos três vezes com os seguintes argumentos.

Em um arquivo separado chamado test_clima.py, escrevemos uma função de teste chamada test_fahr_para_celsius da seguinte forma:

# test_clima.py
from clima import fahr_para_celsius
from pytest import approx
import pytest
def test_fahr_para_celsius():
    """Testa a função fahr_para_celsius chamando-a e
    comparando os valores retornados com os valores esperados.
    Observe que esta função de teste usa pytest.approx para comparar
    números de ponto flutuante.
    """
    assert fahr_para_celsius(-25) == approx(-31.66667)
    assert fahr_para_celsius(0) == approx(-17.77778)
    assert fahr_para_celsius(32) == approx(0)
    assert fahr_para_celsius(70) == approx(21.1111)
# Chama a função main que faz parte do pytest para que o
# o computador execute as funções de teste neste arquivo.
pytest.main(["-v", "--tb=line", "-rN", __file__])

Observe em test_clima.py nas linhas  11–14 que a função de teste test_fahr_para_celsius chama a função de programa fahr_para_celsius quatro vezes: uma vez com um número negativo, uma vez com zero e duas vezes com números positivos. Observe também que a função de teste usa assert e approx.

Após escrever a função de teste, usamos pytest para executar a função de teste. Na linha  17 em vez de escrever uma chamada para a função main, como fazemos em arquivos de programa, escrevemos uma chamada para a função pytest.main. No CSE 111, na parte inferior de todos os arquivos de teste, escreveremos uma chamada para pytest.main, exatamente como mostrado na linha  17. Esta chamada para pytest.main fará com que o módulo pytest execute todas as funções de teste no arquivo test_clima.py Quando o pytest executa as funções de teste, ele produz um resultado que nos informa se os testes foram aprovados ou reprovados, dessa forma:

> python test_clima.py
=================================== test session starts ===================================
plataforma win32--Python 3.13.4, pytest-8.4.1, pluggy-1.6.0
cachedir: .pytest_cache
rootdir: C:\Users\CSE111\week3
collected 1 item

 test_clima.py:: test_fahr_para_celsius PASSED               [100%]
 
==================================== 1 passed in 0.10s ====================================

Conforme mostrado acima, o pytest executa a função test_fahr_para_celsius, que chama a função fahr_para_celsius quatro vezes e verifica se fahr_para_celsius retorna o valor correto a cada vez. Podemos ver da saída do pytest, "PASSED [100%]" e "1  passed", que a função fahr_para_celsius retornou o resultado esperado (correto) quatro vezes.

Separando o Código do Programa e o Código de Teste

Em CSE 111, escreveremos as funções de teste em um programa separado das funções do programa. É uma boa ideia separar as funções de teste e as funções do programa porque essa separação facilita o lançamento de um programa para os usuários sem liberar as funções de teste para eles. Em geral, os usuários de um programa não querem as funções de teste. Uma consequência de escrever funções do programa e funções de teste em arquivos separados é que devemos adicionar uma instrução de importação no topo do arquivo de teste que importa todas as funções do programa que serão testadas.

A linha  2 de test_clima.py acima é um exemplo de uma instrução import que importa funções de um arquivo de programa. A linha  3 corresponde a este modelo:

from file_name import function_1, function_2, … function_N

Quando o computador importa funções de um arquivo, ele executa imediatamente todas as instruções que não estão escritas dentro de uma função. Isso inclui a instrução para chamar a função main:

# Inicia este programa
# chamando a função main.
main()

Isto significa que quando executamos nossas funções de teste, o computador importará nossas funções do programa e ao mesmo tempo, executará a chamada para main(), que iniciará a execução do programa. Entretanto, não queremos que o computador execute o programa enquanto executa as funções de teste, então temos um problema. Como podemos fazer com que o computador importe as funções do programa sem executar a função main? Felizmente, os desenvolvedores do Python nos deram uma solução para esse problema. Em vez de escrever o seguinte código para iniciar a execução do nosso programa:

# Inicia este programa
# chamando a função main.
main()

Escrevemos uma instrução if acima da chamada para main() dessa forma:

# Se este arquivo for executado dessa forma:
# > python programa.py
# então chama a função main. No entanto, se este arquivo for simplesmente
# importado (por exemplo, para um arquivo de teste), então pula a chamada para main.
if __name__ == "__main__":
    main()

Escrever a instrução if acima da chamada para main() é a maneira correta de escrever o código para iniciar um programa. A linguagem de programação Python garante que quando o computador importa as funções do programa (para testá-las), a comparação na instrução if será falsa, então o computador vai pular a chamada para main(). Em outro momento, quando o computador executa o programa (não as funções de teste), a comparação na instrução if será verdadeira, o que fará com que o computador chame a função main e inicie o programa.

Quais Funções de Programa Deveríamos Testar?

Como programadores responsáveis, queremos garantir que todas as funções do nosso programa funcionem corretamente. Idealmente, devemos escrever pelo menos um teste para cada função.

No entanto, isso nem sempre é prático. As funções mais fáceis de testar são aquelas que:

Já as funções mais difíceis de testar são aquelas que:

Nas próximas oito aulas do CSE 111, escreveremos testes apenas para funções que não interagem diretamente com o usuário nem imprimem no terminal — ou seja, as que são fáceis de testar.

Por esse motivo, não escreveremos testes para a função main, pois ela geralmente:

Vídeo

Assista ao vídeo a seguir, que mostra a criação de duas funções de teste e uso do pytest para executá-las.

Documentação

A documentação oficial online (conteúdo em inglês) do pytest contém muito mais informações sobre o uso do pytest.

Resumo

Durante esta aula, você aprenderá a escrever funções de teste que verificam automaticamente se as funções do programa estão funcionando corretamente. No CSE 111, você escreverá funções de teste em um arquivo Python separado do seu arquivo de programa. No topo do arquivo de teste, você importará as funções do programa. Então você escreverá uma função de teste para cada função de programa, exceto o main. Em uma função de teste, você escreverá instruções assert que comparam o valor retornado de uma função de programa com o valor esperado. Você usará um módulo Python padrão chamado pytest para executar suas funções de teste. Quando um teste falha, você usará a saída do pytest para ajudar a encontrar e corrigir os erros no seu código.

Atividade

Objetivo

Melhorar sua capacidade de verificar a exatidão das funções escrevendo uma função de teste e executando-a com pytest.

Tarefa

Escreva uma função de teste para uma função criada anteriormente e execute os testes utilizando pytest.

Documentação Útil

Etapas

Faça o seguinte:

  1. Abra um novo janela de terminal no VS Code fazendo o seguinte:
    1. Abra o VS Code.
    2. Na barra de menu do VS Code, clique em "Terminal"
    3. No menu, clique em "Novo Terminal"
    Uma captura de tela do VS Code mostrando como abrir uma janela do terminal

    Isso abrirá uma janela do terminal na parte inferior da janela do VS Code. Um terminal é uma janela ou quadro onde um usuário pode digitar e executar comandos.

    Uma captura de tela do VS Code mostrando uma janela do terminal aberto
  2. Copie e cole o seguinte comando no janela do terminal e execute-o pressionando a tecla Enter. Este comando atualizará o pip e várias outras partes dos módulos de instalação do Python para que o pip funcione corretamente.
    • Usuários do MacOS:
      python3 -m pip install --user --upgrade pip setuptools wheel
    • Usuários do Windows:
      python -m pip install --user --upgrade pip setuptools wheel
      • Se o seu computador estiver executando o sistema operacional Windows e o comando acima não funcionar, tente o comando py em vez do comando python dessa forma:
      py -m pip install --user --upgrade pip setuptools wheel
    Uma captura de tela do VS Code mostrando o comando para atualizar o pip
  3. Instale o módulo pytest copiando, colando e executando o seguinte comando na janela do terminal.
    • Usuários do MacOS:
      python3 -m pip install --user pytest
    • Usuários do Windows:
      python -m pip install --user pytest
      • Se o seu computador estiver executando o sistema operacional Windows e o comando acima não funcionar, tente o comando py em vez do comando python dessa forma:
      py -m pip install --user pytest
    Uma captura de tela do VS Code mostrando o comando para instalar o pytest
  4. Baixe estes dois arquivos Python: palavras.py e test_palavras.py e salve-os na mesma pasta.
  5. Abra o arquivo palavras.py baixado no VS Code. Observe que o arquivo palavras.py contém duas pequenas funções chamadas prefixo e sufixo. Observe também que cada função tem uma string de documentação (uma string entre aspas triplas imediatamente abaixo do cabeçalho de função) que descreve o que a função faz. Leia as strings de documentação para ambas as funções.
  6. Abra o arquivo test_palavras.py baixado no VS Code. Em test_palavras.py examine a função test_prefixo. Observe que ela não aceita parâmetros e contém nove instruções assert. Cada instrução assert chama a função prefixo e então compara o valor retornado da função prefixo com o valor esperado.
  7. Em test_palavras.py escreva uma função chamada test_sufixo que seja semelhante à função test_prefixo. A função test_sufixo não deve receber parâmetros e deve conter nove instruções assert que chamam a função ´sufixo com estes parâmetros:
    Argumentos Valor de
    Retorno
    Esperado
    s1 s2
    "" "" ""
    "" "correto" ""
    "imparcial" "" ""
    "prever" "" ""
    "cansado" "fatigado" "ado"
    "nadando" "voando" "ando"
    "trator" "redutor" "tor"
    "animal" "pedestal" "al"
    "respeitoso" "precioso" "oso"
  8. Salve seu arquivo test_palavras.py e execute-o clicando no ícone verde de execução no VS Code.

Procedimento de Teste

Verifique se seu programa de teste funciona corretamente seguindo cada etapa deste procedimento:

  1. Execute seu programa de teste e certifique-se de que a saída do programa de teste seja semelhante à saída da execução do exemplo abaixo.
    > python test_palavras.py
    =================================== test session starts ===================================
    platform win32--Python 3.13.4, pytest-8.4.1, pluggy-1.6.0 -- : C:\Users\CSE111\week03
    cachedir: .pytest_cache
    rootdir: C:\CSE111\week03
    collected 2 items
    
     test_palavras.py:: test_prefixo PASSED                     [ 50%]
     test_palavras.py:: test_sufixo PASSED                     [100%]
    
    ==================================== 2 passed in 0.23s ====================================

Exemplo de Solução

Quando terminar seu programa, consulte o exemplo de solução para comparar com o seu.

Primeiro, procure concluir o programa sem olhar o exemplo de solução. No entanto, se já tiver trabalhado nele por bastante tempo e ainda estiver com dificuldades, sinta-se à vontade para usá-lo como apoio para finalizar seu programa.

Gráfico de Chamadas

O gráfico de chamadas a seguir mostra as chamadas e retornos das funções no exemplo de solução desta tarefa. Neste gráfico, vemos que o computador começa a executar as funções de teste de exemplo chamando a função pytest.main. Ao executar a função pytest.main, o computador chama a função test_prefixo. Ao executar a função test_prefixo, o computador chama a função prefixo. Em seguida, ainda executando a função pytest.main, o computador chama a função test_sufixo. Ao executar a função test_sufixo, o computador chama a função sufixo.

Gráfico de chamadas de função para esta tarefa

Ponderar

Durante esta tarefa, você baixou um arquivo Python que contém duas funções do programa chamadas prefixo e sufixo. Você escreveu uma função de teste chamada test_sufixo, semelhante à função test_prefixo que foi fornecida a você. Você usou o pytest para executar ambas as funções de teste e examinou a saída do pytest para verificar se as funções de teste foram aprovadas. Como as funções de teste chamaram prefixo e sufixo com muitos argumentos diferentes e verificaram (usando assert) que os valores retornados de prefixo e sufixo estavam corretos, podemos assumir que as funções prefixo e sufixo funcionam corretamente. Você acha que escrever e executar funções de teste ajudará você a escrever programas melhores?

Links Úteis: