Trilha de aprendizado · Nível 9 · Tutorial 8

Testar o comportamento de funções decoradas

Ao concluir, você será capaz de verificar tanto o comportamento acrescentado por um decorador quanto as características da função original que devem continuar válidas.

  • Nível: Intermediário
  • Duração: 20 min
  • 8 passos
Testar o comportamento de funções decoradas

O que você vai percorrer

  1. Definir o que a interface decorada deve garantir Use o contrato público da função decorada para decidir o que testar: o que ela preserva da função original e o que o decorador acrescenta. 2 min
  2. Observar chamadas com funções auxiliares Crie uma função auxiliar observável para reunir evidências de chamadas sem alterar nem repetir a ação testada. 2 min
  3. Verificar argumentos e quantidade de execuções Use o registro de chamadas para testar o que realmente atravessa o wrapper: argumentos posicionais, nomeados e o número de execuções da função original. 2 min
  4. Testar retornos e propagação de exceções Verifique que a função decorada devolve todos os resultados originais e permite que falhas previstas cheguem a quem a chamou. 3 min
  5. Verificar a ordem dos eventos Teste a sequência completa produzida por wrappers empilhados e pela função original. 3 min
  6. Conferir metadados sem contornar o teste Verifique os metadados preservados por um decorador e mantenha o teste centrado na interface pública. 2 min
  7. Detectar vazamento de estado e configuração Use sequências públicas de eventos para confirmar que rótulos permanecem estáveis e que cada função decorada mantém seu próprio contador de tentativas. 3 min
  8. Consolidar uma minissuíte contra regressões Reúna verificações essenciais em uma suíte curta, execute-a localmente e use um defeito controlado para confirmar que ela protege o contrato público da função decorada. 4 min

O que você vai aprender

  • Criar funções auxiliares que registrem chamadas para verificar argumentos e quantidade de execuções.
  • Testar resultados e propagação de exceções pela interface decorada.
  • Verificar o comportamento adicional, a ordem de execução e a preservação dos metadados esperados.
  • Detectar compartilhamento indevido de estado entre funções decoradas com configurações distintas.

Antes de começar

  • Criar decoradores com functools.wraps
  • Criar decoradores parametrizados
  • Escrever e executar testes de funções com pytest
  • Selecionar casos de teste e verificar exceções

Passo 1 de 8

Definir o que a interface decorada deve garantir

Use o contrato público da função decorada para decidir o que testar: o que ela preserva da função original e o que o decorador acrescenta.

O contrato está na chamada pública

Teste a interface que será usada

Uma função decorada tem uma interface pública: é por ela que o código cliente faz chamadas. Os testes devem descrever o que essa chamada precisa garantir, sem depender de como o wrapper foi organizado internamente.

No exemplo-guia, uma função decorada recebe argumentos, executa a função original e acrescenta eventos em memória. O contrato reúne tanto o comportamento preservado quanto o comportamento novo.

Mapa da chamada decorada

A chamada pública atravessa o wrapper, que acrescenta eventos, e então alcança a função original. O resultado ou a exceção retorna pela mesma interface pública.

Diagrama mostrando uma chamada pública entrando em um wrapper, que registra eventos e chama a função original; o resultado ou a exceção segue de volta para quem chamou.

O teste observa entradas e saídas da interface decorada; ele não precisa conhecer variáveis internas do wrapper.

Contrato do exemplo-guia

Exemplo

O que a função decorada deve garantir

Considere uma função decorada com o rótulo configurável "pagamento".

Para cada chamada pública válida, ela deve:

  • encaminhar à função original os argumentos recebidos;
  • executar a função original exatamente uma vez;
  • devolver exatamente o resultado produzido pela original;
  • propagar uma exceção produzida pela original.

Além disso, o decorador deve registrar eventos em uma lista compartilhada para observação:

  • registrar entrada em toda tentativa;
  • registrar saída somente se a função original terminar sem exceção;
  • associar os eventos ao rótulo "pagamento";
  • atribuir um número de tentativa próprio daquela função decorada.

Metadados esperados, como nome e documentação, também pertencem ao contrato, mas serão verificados adiante.

Dica

Resultado igual não basta

Um teste que verifica apenas o retorno pode deixar passar uma chamada duplicada: duas execuções podem produzir o mesmo valor. Por isso, o contrato separa retorno, encaminhamento de argumentos e quantidade de execuções.

Classifique as expectativas

Preservado ou acrescentado?

Associe cada expectativa à categoria correta no contrato da função decorada.

Toque em um item e depois no par correspondente.

Passo 2 de 8

Observar chamadas com funções auxiliares

Crie uma função auxiliar observável para reunir evidências de chamadas sem alterar nem repetir a ação testada.

Evidência produzida pela própria chamada

Uma auxiliar observável

Para testar uma função decorada, defina no próprio teste uma função simples e decore-a normalmente. Essa auxiliar deve registrar o que recebeu e o que produziu em listas mantidas pelo teste.

Use registros com marcadores diferentes: por exemplo, uma tupla iniciada por "chamada" para as entradas e outra iniciada por "resultado" para a saída. Assim, os dados da função original não se confundem com eventos acrescentados pelo decorador.

O que fica observável

A chamada pública passa pelo decorador e chega à auxiliar. A auxiliar registra uma vez os argumentos e uma vez o resultado conhecido.

Diagrama mostrando uma chamada a uma função decorada passando por um wrapper até uma função auxiliar; a auxiliar grava uma entrada de chamada com argumentos e uma entrada de resultado em uma lista de registros.

A lista de registros é a evidência do teste; ela evita precisar executar a função de novo.

Dica

Não chame de novo para contar

O tamanho dos registros de chamada informa quantas vezes a função original foi executada. Fazer uma chamada extra apenas para inspecionar o comportamento alteraria a evidência que você quer verificar.

Um exemplo local completo

Prepare um arquivo de teste

No seu computador, crie um arquivo chamado test_observacao.py. O exemplo abaixo inclui um decorador simples, uma lista compartilhada intencionalmente para eventos e uma fábrica que cria uma auxiliar nova com seus próprios registros.

test_observacao.py

Copie o arquivo completo e execute-o com pytest.

python
from functools import wraps


def registrar_eventos(eventos, rotulo):
    def decorar(funcao):
        @wraps(funcao)
        def wrapper(*args, **kwargs):
            eventos.append(("entrada", rotulo))
            resultado = funcao(*args, **kwargs)
            eventos.append(("saida", rotulo))
            return resultado

        return wrapper

    return decorar


def criar_auxiliar(registros):
    def calcular(valor, aumento=1):
        resultado = valor + aumento
        registros.append(("chamada", valor, aumento))
        registros.append(("resultado", resultado))
        return resultado

    return calcular


def test_registra_chamada_e_resultado_sem_repetir_execucao():
    eventos = []
    registros = []
    calcular = criar_auxiliar(registros)
    calcular_observado = registrar_eventos(eventos, "calculo")(calcular)

    retorno = calcular_observado(10, aumento=5)

    assert retorno == 15
    assert registros == [
        ("chamada", 10, 5),
        ("resultado", 15),
    ]
    assert len([item for item in registros if item[0] == "chamada"]) == 1
    assert eventos == [("entrada", "calculo"), ("saida", "calculo")]

Pratique a observação

Exemplo

Leitura da evidência

Depois de uma única chamada, registros contém duas informações diferentes:

  • ("chamada", 10, 5) mostra os valores recebidos pela auxiliar.
  • ("resultado", 15) mostra o valor que ela produziu.

O filtro item[0] == "chamada" encontra somente as entradas de chamada. Seu tamanho é 1, portanto a auxiliar executou uma vez — sem uma segunda chamada de inspeção.

Complete e execute

No arquivo local, confirme que a auxiliar registra a chamada e o resultado. Execute python -m pytest test_observacao.py. Em seguida, explique qual registro permite concluir a quantidade de execuções da auxiliar sem fazer uma chamada extra.

Escreva pelo menos 80 caracteres (0/80).

Passo 3 de 8

Verificar argumentos e quantidade de execuções

Use o registro de chamadas para testar o que realmente atravessa o wrapper: argumentos posicionais, nomeados e o número de execuções da função original.

Observe o que atravessa o wrapper

Retorno não é evidência suficiente

Um teste da função decorada deve observar mais do que o valor devolvido. Um wrapper pode encaminhar argumentos errados ou chamar a função original duas vezes e, ainda assim, devolver um resultado aparentemente correto.

Registre na função original os pares (args, kwargs). Assim, o teste verifica exatamente os argumentos recebidos e conta as execuções sem chamar a função novamente.

Pontos observáveis da chamada

Diagrama mostrando uma chamada pública passando por um wrapper e chegando à função original, que registra argumentos posicionais, argumentos nomeados e uma execução.

Compare o registro feito pela função original com os argumentos usados na chamada pública.

Dica

Valores padrão não são encaminhamento

Se a chamada pública for calcular(10), o wrapper correto encaminha args == (10,) e kwargs == {}. O valor padrão de quantidade=1 é aplicado quando a função original recebe a chamada; ele não deve aparecer artificialmente no registro do wrapper.

Teste chamadas posicionais, nomeadas e mistas

Arquivo local: test_encaminhamento.py

Crie este arquivo e execute python -m pytest test_encaminhamento.py. Cada teste cria uma lista nova para que seus registros não se misturem.

python
from functools import wraps


def registrar_chamadas(chamadas):
    def decorador(funcao):
        @wraps(funcao)
        def wrapper(*args, **kwargs):
            return funcao(*args, **kwargs)

        return wrapper

    return decorador


def criar_calcular(chamadas):
    @registrar_chamadas(chamadas)
    def calcular(preco, quantidade=1, *, desconto=0):
        chamadas.append(("chamada", (preco, quantidade), {"desconto": desconto}))
        return preco * quantidade - desconto

    return calcular


def test_encaminha_argumento_posicional_sem_inserir_padrao():
    chamadas = []
    calcular = criar_calcular(chamadas)

    resultado = calcular(10)

    assert resultado == 10
    # A função original registra os valores após aplicar quantidade=1.
    assert chamadas == [("chamada", (10, 1), {"desconto": 0})]


def test_encaminha_argumentos_nomeados():
    chamadas = []
    calcular = criar_calcular(chamadas)

    resultado = calcular(preco=8, quantidade=3, desconto=2)

    assert resultado == 22
    assert chamadas == [("chamada", (8, 3), {"desconto": 2})]


def test_encaminha_chamada_mista():
    chamadas = []
    calcular = criar_calcular(chamadas)

    resultado = calcular(5, quantidade=4, desconto=1)

    assert resultado == 19
    assert chamadas == [("chamada", (5, 4), {"desconto": 1})]

Complete a verificação do registro

Argumentos nomeados vazios

Em uma função auxiliar que registra diretamente (args, kwargs), complete a asserção para a chamada calcular(10):

assert registro == [((10,), ____)]

Conte execuções, não apenas resultados

Uma chamada pública, uma execução registrada

Para o contrato deste exemplo, cada chamada pública à função decorada deve produzir exatamente um registro de chamada da função original. Faça essa asserção separadamente do teste de retorno.

Se o wrapper executar a função duas vezes, o resultado pode continuar sendo 19, mas a lista terá dois registros — e o contrato foi violado.

Encontre a duplicação

Um wrapper com defeito chama a função original duas vezes, mas devolve o resultado da primeira execução. Qual verificação detecta esse defeito para uma única chamada pública?

Passo 4 de 8

Testar retornos e propagação de exceções

Verifique que a função decorada devolve todos os resultados originais e permite que falhas previstas cheguem a quem a chamou.

O retorno faz parte do contrato

Chame pela interface pública

O teste deve chamar a função decorada, não a função original guardada dentro do decorador. Assim, ele verifica se o wrapper preserva o resultado produzido pela função.

Teste valores que tornem defeitos visíveis: um valor comum, None e um valor falso como 0. Um wrapper que executa a função mas esquece return sempre devolve None — e, por isso, um teste somente com None poderia passar por engano.

Dois caminhos observáveis

Nos dois caminhos, a entrada é registrada. A saída só é registrada quando a função original termina normalmente.

Diagrama com uma chamada pública atravessando um wrapper e seguindo para dois caminhos: sucesso, com eventos de entrada, execução, saída e valor de retorno; falha, com eventos de entrada, execução e exceção, sem evento de saída.

Sucesso: entrada → execução → saída → retorno. Falha: entrada → execução → exceção.

Cubra valores que o wrapper não pode alterar

Preparação nova em cada teste

Use listas novas para observar as execuções. Além do resultado público, registre a chamada da função original: isso evita concluir que um retorno None prova, sozinho, que ela foi executada.

Testes de retorno em test_registro.py

Este exemplo pressupõe que registrar_eventos recebe um rótulo e uma lista de eventos, como no exemplo-guia do tutorial.

python
from app import registrar_eventos


def test_devolve_um_valor_calculado():
    eventos = []
    chamadas = []

    @registrar_eventos("calculo", eventos)
    def triplicar(numero):
        chamadas.append(numero)
        return numero * 3

    resultado = triplicar(7)

    assert resultado == 21
    assert chamadas == [7]
    assert eventos == [
        ("entrada", "calculo", 1),
        ("saida", "calculo", 1),
    ]


def test_preserva_none_e_ainda_executa_a_original():
    eventos = []
    chamadas = []

    @registrar_eventos("aviso", eventos)
    def avisar(mensagem):
        chamadas.append(mensagem)
        # O retorno implícito é None.

    resultado = avisar("pronto")

    assert resultado is None
    assert chamadas == ["pronto"]
    assert eventos == [
        ("entrada", "aviso", 1),
        ("saida", "aviso", 1),
    ]


def test_preserva_zero_sem_substituir_o_resultado():
    eventos = []

    @registrar_eventos("estoque", eventos)
    def quantidade_disponivel():
        return 0

    assert quantidade_disponivel() == 0
    assert eventos == [
        ("entrada", "estoque", 1),
        ("saida", "estoque", 1),
    ]

Não oculte a falha

A exceção deve atravessar o wrapper

Quando a função original lança uma exceção prevista, envolva a chamada decorada com pytest.raises. Verifique o tipo e uma mensagem significativa. Depois, confira os eventos: houve entrada, mas não houve saída, pois a execução não terminou com sucesso.

Teste de propagação

Acrescente este teste ao mesmo arquivo. Ele verifica a interface decorada e não aceita que o wrapper transforme a falha em um retorno comum.

python
import pytest

from app import registrar_eventos


def test_propaga_erro_e_nao_registra_saida():
    eventos = []

    @registrar_eventos("divisao", eventos)
    def dividir(dividendo, divisor):
        if divisor == 0:
            raise ValueError("divisor não pode ser zero")
        return dividendo / divisor

    with pytest.raises(ValueError, match="divisor não pode ser zero"):
        dividir(10, 0)

    assert eventos == [("entrada", "divisao", 1)]

Complete a asserção

Para garantir que um wrapper não troque um resultado falso por outro valor, complete:

assert quantidade_disponivel() == ____

Explique a evidência necessária

Por que `None` não basta?

Por que um teste que verifica somente resultado is None não detecta necessariamente um wrapper que esqueceu de devolver o resultado da função original?

Escreva pelo menos 80 caracteres (0/80).

Resumo

Checklist deste step

  • Chame a função decorada para testar a interface que os usuários realmente usam.
  • Verifique um valor comum, None com evidência de execução e um valor falso como 0.
  • Use pytest.raises na chamada decorada para confirmar tipo e mensagem da exceção.
  • No contrato adotado, sucesso registra entrada e saída; falha registra apenas entrada.

Passo 5 de 8

Verificar a ordem dos eventos

Teste a sequência completa produzida por wrappers empilhados e pela função original.

A ordem também faz parte do contrato

Não basta encontrar os eventos

Uma lista de eventos permite observar a ordem relativa entre o wrapper e a função original. Para dois decoradores empilhados, a chamada pública entra primeiro na camada externa, depois na interna, executa a função original e sai na ordem inversa.

Verificar apenas que todos os eventos estão presentes não é suficiente: uma saída registrada antes da função original ainda é um defeito, mesmo que o retorno final esteja correto.

Fluxo de dois decoradores

As cores distinguem as camadas: azul = wrapper externo, laranja = wrapper interno e verde = função original.

Diagrama em camadas mostrando o fluxo entrar no wrapper externo, seguir para o interno, alcançar a função original e retornar primeiro pelo interno e depois pelo externo.

A sequência esperada é: entrada externa → entrada interna → execução original → saída interna → saída externa.

Dica

Resultado e sequência são verificações diferentes

Uma função pode devolver o resultado esperado mesmo com eventos fora de ordem. Faça uma asserção para o retorno e outra para a lista completa de eventos.

Registrar e comparar a sequência completa

Teste de ordem com camadas identificadas

Cada rótulo identifica a camada que produziu o evento.

python
from functools import wraps


def registrar(eventos, rotulo):
    def decorar(funcao):
        @wraps(funcao)
        def wrapper(*args, **kwargs):
            eventos.append(f"{rotulo}:entrada")
            resultado = funcao(*args, **kwargs)
            eventos.append(f"{rotulo}:saida")
            return resultado

        return wrapper

    return decorar


def test_eventos_decoradores_empilhados():
    eventos = []

    @registrar(eventos, "externo")
    @registrar(eventos, "interno")
    def dobrar(valor):
        eventos.append("original:execucao")
        return valor * 2

    resultado = dobrar(5)

    assert resultado == 10
    assert eventos == [
        "externo:entrada",
        "interno:entrada",
        "original:execucao",
        "interno:saida",
        "externo:saida",
    ]

Por que comparar a lista inteira?

A igualdade entre listas verifica conteúdo e posição. Portanto, esse teste falha se um evento faltar, aparecer a mais, tiver outro rótulo ou ocorrer em uma posição incorreta.

Crie a lista eventos dentro de cada teste: ela é compartilhada intencionalmente pelas camadas daquele cenário para observação, mas não deve transportar registros de outro teste.

Praticar a leitura da sequência

Ordene os eventos

Uma função foi decorada com uma camada externa e uma interna. Coloque os eventos na ordem produzida por uma chamada bem-sucedida.

  1. externo:saida
  2. interno:entrada
  3. interno:saida
  4. externo:entrada
  5. original:execucao

Complete a asserção de ordem

Complete o evento que deve ocupar o terceiro lugar:

assert eventos == ["externo:entrada", "interno:entrada", ___, "interno:saida", "externo:saida"]

Passo 6 de 8

Conferir metadados sem contornar o teste

Verifique os metadados preservados por um decorador e mantenha o teste centrado na interface pública.

Metadados também são parte do contrato

O que conferir

Além de testar chamadas, retornos e eventos pela função decorada, confira os metadados que o contrato promete preservar. Com functools.wraps, é esperado que __name__ e __doc__ da interface decorada correspondam aos da função original.

Essas verificações são complementares: metadados corretos não provam que o wrapper encaminha argumentos, devolve o resultado ou registra eventos corretamente.

Camadas e referências

A interface pública é a função decorada. O atributo __wrapped__ revela a camada imediatamente envolvida, mas chamá-la pula uma camada do comportamento.

Diagrama com uma chamada entrando no wrapper externo, passando por um wrapper interno até a função original; setas laterais mostram __wrapped__ apontando para a camada imediatamente interna.

Em decoradores empilhados, cada __wrapped__ aponta somente para a próxima camada interna.

Teste a ligação imediatamente envolvida

Guarde a referência antes de decorar

Ao aplicar um decorador explicitamente, mantenha a referência da função antes da transformação. Assim, você pode verificar por identidade que __wrapped__ aponta para a função que aquele wrapper envolveu.

Exemplo com aplicação explícita

Arquivo de teste com pytest

python
from functools import wraps


def registrar(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper


def calcular_total(preco, quantidade=1):
    """Calcula o total de itens."""
    return preco * quantidade


original = calcular_total
decorada = registrar(original)


def test_preserva_metadados_e_referencia_imediata():
    assert decorada.__name__ == "calcular_total"
    assert decorada.__doc__ == "Calcula o total de itens."
    assert decorada.__wrapped__ is original

Metadados não substituem o teste público

Não contorne a camada que quer validar

Em um empilhamento, o __wrapped__ da camada externa pode ser outro wrapper. Chamar apenas externa.__wrapped__() deixa de executar a camada externa; portanto, não demonstra que ela registra seus eventos, encaminha argumentos ou preserva retornos e exceções.

Use __wrapped__ para testar a ligação entre camadas. Use a função decorada para testar o comportamento público.

Avalie a conclusão

Se externa.__wrapped__() retorna o resultado esperado, então o decorador externo está validado.

Prática: complete as verificações

Nome e identidade

Complete as duas lacunas:

original = calcular_total
decorada = registrar(original)

assert decorada._____ == "calcular_total"
assert decorada.__wrapped__ ___ original

Dica

Checklist desta dimensão

Verifique __name__, __doc__ e a ligação por __wrapped__. Depois, mantenha testes separados para a chamada pública: argumentos, retorno, exceções e eventos.

Passo 7 de 8

Detectar vazamento de estado e configuração

Use sequências públicas de eventos para confirmar que rótulos permanecem estáveis e que cada função decorada mantém seu próprio contador de tentativas.

O que deve — e não deve — ser compartilhado

Observe o estado pelo contrato público

Uma lista de eventos pode ser compartilhada intencionalmente: ela reúne evidências para o teste. Já o contador de tentativas precisa pertencer a cada função decorada. Assim, chamadas intercaladas de duas funções não podem fazer o número de uma avançar a outra.

Não inspecione células da closure nem variáveis internas. Faça chamadas pela interface decorada e compare a sequência completa de eventos.

Registro comum, contadores separados

O mesmo registro permite observar as duas funções; os contadores independentes preservam o estado de cada uma.

Diagrama com uma lista de eventos compartilhada recebendo eventos de duas funções, cada função ligada ao seu próprio contador de tentativas.

Compartilhar a observação é diferente de compartilhar o estado que define as tentativas.

Dica

Preparação nova por teste

Crie uma nova lista de eventos e novas funções decoradas em cada teste. Isso evita que uma execução anterior esconda ou invente um vazamento de estado.

Cenários que expõem o vazamento

Decorador e testes de isolamento

Crie um arquivo local chamado test_estado_decorador.py com este conteúdo e execute python -m pytest -q.

python
import pytest
from functools import wraps


def registrar(eventos, rotulo):
    def decorar(funcao):
        tentativa = 0

        @wraps(funcao)
        def wrapper(*args, **kwargs):
            nonlocal tentativa
            tentativa += 1
            eventos.append((rotulo, "entrada", tentativa))
            resultado = funcao(*args, **kwargs)
            eventos.append((rotulo, "saida", tentativa))
            return resultado

        return wrapper

    return decorar


def test_falha_tambem_consume_uma_tentativa():
    eventos = []

    @registrar(eventos, "pagamento")
    def cobrar(valor):
        if valor < 0:
            raise ValueError("valor inválido")
        return valor

    assert cobrar(10) == 10
    with pytest.raises(ValueError, match="valor inválido"):
        cobrar(-1)
    assert cobrar(5) == 5

    assert eventos == [
        ("pagamento", "entrada", 1),
        ("pagamento", "saida", 1),
        ("pagamento", "entrada", 2),
        ("pagamento", "entrada", 3),
        ("pagamento", "saida", 3),
    ]


def test_funcoes_com_rotulos_distintos_nao_misturam_contadores():
    eventos = []

    @registrar(eventos, "email")
    def enviar(destinatario):
        return destinatario.upper()

    @registrar(eventos, "sms")
    def avisar(destinatario):
        return destinatario.lower()

    assert enviar("Ana") == "ANA"
    assert avisar("Bia") == "bia"
    assert enviar("Caio") == "CAIO"

    assert eventos == [
        ("email", "entrada", 1),
        ("email", "saida", 1),
        ("sms", "entrada", 1),
        ("sms", "saida", 1),
        ("email", "entrada", 2),
        ("email", "saida", 2),
    ]


def test_mesmo_decorador_configurado_da_contadores_por_funcao():
    eventos = []
    auditar = registrar(eventos, "auditoria")

    @auditar
    def criar():
        return "criada"

    @auditar
    def remover():
        return "removida"

    criar()
    remover()
    criar()

    entradas = [evento for evento in eventos if evento[1] == "entrada"]
    assert entradas == [
        ("auditoria", "entrada", 1),
        ("auditoria", "entrada", 1),
        ("auditoria", "entrada", 2),
    ]

Leia as evidências

No primeiro teste, a falha produz somente o evento de entrada da tentativa 2, mas a chamada seguinte recebe tentativa 3. Nos outros dois, a ordem intercalada revela que cada função começa em 1 e progride sem afetar a outra — inclusive quando ambas reutilizam o mesmo decorador já configurado.

Complete e interprete a sequência

Contador da função correta

Depois de enviar("Ana"), a chamada avisar("Bia") deve registrar ("sms", "entrada", ___).

Diagnóstico por eventos

Suponha que, no segundo teste, a entrada de avisar("Bia") seja ("sms", "entrada", 2). Que defeito isso revela? Por que a lista única de eventos não é, por si só, um defeito?

Escreva pelo menos 80 caracteres (0/80).

Passo 8 de 8

Consolidar uma minissuíte contra regressões

Reúna verificações essenciais em uma suíte curta, execute-a localmente e use um defeito controlado para confirmar que ela protege o contrato público da função decorada.

Uma suíte curta, com evidências distintas

O que a minissuíte protege

Uma suíte contra regressões não precisa repetir a mesma chamada muitas vezes. Cada teste deve proteger uma expectativa observável do contrato:

  • argumentos e execução: a função original recebe o que a interface pública recebeu e roda uma vez;
  • retorno e exceção: valores, inclusive valores falsos, voltam sem alteração; falhas não são escondidas;
  • eventos e ordem: entradas e saídas aparecem na sequência combinada;
  • metadados: __name__, __doc__ e __wrapped__ mantêm a ligação esperada;
  • isolamento: configurações e contadores de funções diferentes não vazam entre si.

Essas verificações são complementares: acertar o retorno não prova que a chamada ocorreu uma única vez, por exemplo.

Mapa das verificações

Observe como cada teste obtém evidência pela interface pública ou por efeitos públicos registrados.

Diagrama em camadas mostrando uma chamada pública atravessando um wrapper até a função original, com ramificações de observação para argumentos, retorno, exceção, eventos, metadados e estado isolado.

A chamada decorada é o centro da verificação; os registros em memória tornam os efeitos observáveis.

Dica

Critério prático

Prefira uma asserção que falhe por um motivo claro. Se duas asserções protegem exatamente a mesma expectativa, mantenha a que comunica melhor o contrato.

Código-base e minissuíte

Prepare dois arquivos locais

Em uma pasta vazia, crie os arquivos decoradores.py e test_decoradores.py com os conteúdos abaixo. Se ainda não tiver pytest no ambiente, instale-o com python -m pip install pytest.

decoradores.py

Implementação correta do decorador parametrizado.

python
from functools import wraps


def registrar(rotulo, eventos):
    def decorar(funcao):
        tentativa = 0

        @wraps(funcao)
        def wrapper(*args, **kwargs):
            nonlocal tentativa
            tentativa += 1
            eventos.append(("entrada", rotulo, tentativa))
            resultado = funcao(*args, **kwargs)
            eventos.append(("saida", rotulo, tentativa))
            return resultado

        return wrapper

    return decorar

test_decoradores.py

A minissuíte abaixo reúne uma evidência para cada dimensão importante do contrato.

python
import pytest

from decoradores import registrar


def test_encaminha_argumentos_e_executa_uma_vez():
    eventos = []
    chamadas = []

    @registrar("soma", eventos)
    def somar(a, b=0, *, ajuste=0):
        chamadas.append((a, b, ajuste))
        return a + b + ajuste

    assert somar(2, b=3, ajuste=4) == 9
    assert chamadas == [(2, 3, 4)]
    assert eventos == [("entrada", "soma", 1), ("saida", "soma", 1)]


def test_preserva_retorno_falso_e_propagacao_de_excecao():
    eventos = []

    @registrar("busca", eventos)
    def buscar(ativo):
        if not ativo:
            return 0
        raise ValueError("cadastro indisponivel")

    assert buscar(False) == 0
    with pytest.raises(ValueError, match="cadastro indisponivel"):
        buscar(True)
    assert eventos == [
        ("entrada", "busca", 1),
        ("saida", "busca", 1),
        ("entrada", "busca", 2),
    ]


def test_mantem_metadados_e_referencia_a_original():
    eventos = []

    def formatar(nome):
        """Devolve um nome em letras maiusculas."""
        return nome.upper()

    original = formatar
    decorada = registrar("formato", eventos)(formatar)

    assert decorada.__name__ == "formatar"
    assert decorada.__doc__ == "Devolve um nome em letras maiusculas."
    assert decorada.__wrapped__ is original
    assert decorada("ana") == "ANA"


def test_isola_contadores_e_rotulos_entre_funcoes():
    eventos = []
    auditoria = registrar("auditoria", eventos)

    @auditoria
    def abrir():
        return "abriu"

    @registrar("pagamento", eventos)
    def cobrar():
        return "cobrou"

    assert abrir() == "abriu"
    assert cobrar() == "cobrou"
    assert abrir() == "abriu"
    assert eventos == [
        ("entrada", "auditoria", 1), ("saida", "auditoria", 1),
        ("entrada", "pagamento", 1), ("saida", "pagamento", 1),
        ("entrada", "auditoria", 2), ("saida", "auditoria", 2),
    ]

Execute e injete um defeito controlado

Primeira execução

No terminal aberto nessa pasta, execute:

python -m pytest -q

Com a implementação correta, espere 4 passed. Agora altere somente a última linha do wrapper em decoradores.py, trocando return resultado por return None. Salve e rode o mesmo comando novamente.

Variante defeituosa

Esta é a única alteração necessária para simular uma regressão de retorno.

python
# Dentro de wrapper, mantenha o restante igual:
resultado = funcao(*args, **kwargs)
eventos.append(("saida", rotulo, tentativa))
return None  # defeito controlado: descarta o resultado original

Atenção

Leia a falha pelo contrato

A expectativa é que test_encaminha_argumentos_e_executa_uma_vez falhe porque somar(...) passa a devolver None, não 9. A suíte ainda pode mostrar outras falhas dependentes de retorno. Isso não é ruído: cada falha aponta uma promessa pública quebrada.

Depois da observação, restaure return resultado e execute novamente para confirmar que a correção devolveu a suíte ao estado verde.

Resumo

Checklist final de evidências

Uma suíte aprovada aumenta a confiança no contrato coberto; ela não prova a ausência de todo defeito possível.

  • Chame a função decorada para verificar a interface que os usuários realmente utilizam.
  • Registre chamadas e eventos para observar argumentos, quantidade e ordem sem executar a função novamente.
  • Teste retornos falsos e exceções, além do caminho comum de sucesso.
  • Verifique metadados separadamente: eles não substituem testes de encaminhamento, retorno ou eventos.
  • Intercale chamadas de funções decoradas distintas para revelar estado ou configuração compartilhados indevidamente.

Aplicação final

Explique a regressão detectada

Após executar a variante defeituosa, relate: qual teste detectou o problema, qual parte do contrato foi violada e por que chamar somente __wrapped__ não substituiria esse teste.

Escreva pelo menos 120 caracteres (0/120).

Minissuíte consolidada

Parabéns! Você concluiu: Testar o comportamento de funções decoradas

Muito bem! Você reuniu evidências para argumentos, execução, retornos, exceções, eventos, ordem, metadados e isolamento — e confirmou que a suíte detecta uma regressão real na interface decorada.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso