Como Realizar Testes de Unidade em Flask

Introdução

A avaliação é fundamental no processo de desenvolvimento de software, garantindo que o código se comporte conforme o esperado e esteja livre de defeitos. Em Python, o pytest é um framework de avaliação popular que oferece vantagens sobre o módulo padrão unit test, que é um framework de avaliação embutido em Python e faz parte da biblioteca padrão. O pytest inclui uma sintaxe simples, melhor saída, ferramentas poderosas e um rico ecossistema de extensões. Este tutorial irá guiar você pelo processo de configuração de uma aplicação Flask, pela integração de ferramentas do pytest e pela escrita de avaliações unitárias usando o pytest.

Pré-requisitos

Antes de começar, você precisará do seguinte:

  • Um servidor rodando Ubuntu e um usuário não-root com privilégios de sudo e uma firewall ativa. Para orientação sobre como configurar isso, por favor escolha sua distribuição de Ubuntu da lista e siga com o guia de configuração inicial do servidor. Certifique-se de trabalhar com uma versão suportada do Ubuntu.

  • Familiaridade com a linha de comando do Linux. Você pode visitar esta guia sobre primeiro Linux command line.

  • Um entendimento básico do processamento de linguagem Python e do framework de teste pytest em Python. Você pode referir-se à nossa tutorial sobre Framework de Teste Python pytest para saber mais sobre pytest.

  • Python 3.7 ou superior instalado no seu sistema Ubuntu. Para aprender a executar um script de Python no Ubuntu, você pode referir-se à nossa tutorial sobre Como executar um script de Python no Ubuntu.

Porque pytest é uma Alternativa Melhorado ao unittest

pytest oferece várias vantagens sobre o framework de teste interno unittest:

  • Pytest permite que você escreva testes com menos código de modelo, usando simples declarações assert em vez das métodos mais verbosos necessários por unittest.

  • Ele fornece saídas mais detalhadas e legíveis, tornando mais fácil identificar onde e por que um teste falhou.

  • Fixtures de Pytest permitem configurações de teste mais flexíveis e reutilizáveis do que os métodos setUp e tearDown de unittest.

  • Ele facilita a execução da mesma função de teste com vários conjuntos de entradas, o que não é tão direto em unittest.

  • Pytest possui uma rica coleção de plugins que extendem suas funcionalidades, desde ferramentas de cobertura de código até execução de testes em paralelo.

  • Ele automaticamente descobre arquivos de teste e funções que correspondem às suas convenções de nomenclatura, economizando tempo e esforço na gerencia de suites de teste.

Dada essas vantagens, pytest é frequentemente a escolha preferida para testes em Python moderno. Vamos configurar uma aplicação Flask e escrever testes unitários usando pytest.

Passo 1 – Configurando o Ambiente

O Ubuntu 24.04 vem com Python 3 por padrão. Abra o terminal e execute o comando a seguir para verificar a instalação do Python 3:

root@ubuntu:~# python3 --version
Python 3.12.3

Se o Python 3 já estiver instalado no seu computador, o comando acima retornará a versão atual da instalação do Python 3. Caso contrário, você pode executar o seguinte comando para obter a instalação do Python 3:

root@ubuntu:~# sudo apt install python3

A próxima etapa é instalar o gerenciador de pacotes pip no seu sistema:

root@ubuntu:~# sudo apt install python3-pip

Uma vez que o pip estiver instalado, vamos instalar o Flask.

Passo 2 – Criar uma Aplicação Flask

Vamos começar criando uma aplicação Flask simples. Crie um novo diretório para seu projeto e navegue até ele:

root@ubuntu:~# mkdir flask_testing_app
root@ubuntu:~# cd flask_testing_app

Agora, vamos criar e ativar um ambiente virtual para gerenciar dependências:

root@ubuntu:~# python3 -m venv venv
root@ubuntu:~# source venv/bin/activate

Instale o Flask usando o pip:

root@ubuntu:~# pip install Flask

Agora, vamos criar uma simples aplicação Flask. Crie um novo arquivo chamado app.py e adicione o código seguinte:

app.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify(message="Hello, Flask!")

@app.route('/about')
def about():
    return jsonify(message="This is the About page")

@app.route('/multiply/<int:x>/<int:y>')
def multiply(x, y):
    result = x * y
    return jsonify(result=result)

if __name__ == '__main__':
    app.run(debug=True)

Esta aplicação tem três rotas:

  • /: Retorna uma mensagem simples “Hello, Flask!”.
  • /about: Retorna uma mensagem simples “This is the About page”.
  • /multiply/<int:x>/<int:y>: Multiplica dois inteiros e retorna o resultado.

Para executar a aplicação, execute o comando a seguir:

root@ubuntu:~# flask run
output
* Serving Flask app "app" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Ao examinar a saída acima, você pode notar que o servidor está executando em http://127.0.0.1 e está escutando na porta 5000. Abra outra consola Ubuntu e execute os comandos curl abaixo um por um:

  • GET: curl http://127.0.0.1:5000/
  • GET: curl http://127.0.0.1:5000/about
  • GET: curl http://127.0.0.1:5000/multiply/10/20

Vamos entender o que as solicitações GET fazem:

  1. curl http://127.0.0.1:5000/:
    Isto envia uma solicitação GET para a rota raiz (‘/’) de nossa aplicação Flask. O servidor responde com um objeto JSON que contém a mensagem “Hello, Flask!”, demonstrando a funcionalidade básica de nossa rota inicial.

  2. curl http://127.0.0.1:5000/about:
    Isto envia uma solicitação GET para a rota /about. O servidor responde com um objeto JSON que contém a mensagem “This is the About page”. Isto mostra que nossa rota está funcionando corretamente.

  3. curl http://127.0.0.1:5000/multiply/10/20:
    Isto envia uma solicitação GET para a rota /multiply com dois parâmetros: 10 e 20. O servidor multiplica estes números e responde com um objeto JSON que contém o resultado (200). Isto demonstra que nossa rota de multiplicação pode processar corretamente parâmetros da URL e realizar cálculos.

Estas GET requests permitem que interajamos com as API endpoints de nossa aplicação Flask, recuperando informação ou disparando ações no servidor sem modificar quaisquer dados. Elas são úteis para buscar dados, testar a funcionalidade de endpoint e verificar que nossas rotas estão respondendo como esperado.

Vamos ver cada uma destas GET requests em ação:

root@ubuntu:~# curl http://127.0.0.1:5000/
Output
{"message":"Hello, Flask!"}
root@ubuntu:~# curl http://127.0.0.1:5000/about
Output
{"message":"This is the About page"}
root@ubuntu:~# curl http://127.0.0.1:5000/multiply/10/20
Output
{"result":200}

Passo 3 – Instalando pytest e Escrevendo seu Primeiro Teste

Agora que você tem uma aplicação Flask básica, vamos instalar pytest e escrever alguns testes unitários.

Instale pytest usando pip:

root@ubuntu:~# pip install pytest

Crie um diretório chamado tests para armazenar seus arquivos de teste:

root@ubuntu:~# mkdir tests

Agora, vamos criar um novo arquivo chamado test_app.py e adicionar o seguinte código:

test_app.py
# Importar módulo sys para modificar o ambiente de runtime do Python
import sys
# Importar módulo os para interagir com o sistema operacional
import os

# Adicionar diretório pai a sys.path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# Importar instância do aplicativo Flask do arquivo principal
from app import app 
# Importar pytest para escrever e executar testes
import pytest

@pytest.fixture
def client():
    """A test client for the app."""
    with app.test_client() as client:
        yield client

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

Vamos analisar as funções neste arquivo de teste:

  1. @pytest.fixture def client():
    Esta é uma fixture de pytest que cria um cliente de teste para nosso aplicativo Flask. Ela usa o método app.test_client() para criar um cliente que pode enviar solicitações para nossa aplicação sem executar o servidor real. A instrução yield permite que o cliente seja usado em testes e, em seguida, fechado corretamente após cada teste.

  2. def test_home(client):

    Esta função testa a rota home (/) do nosso aplicativo. Ela envia uma solicitação GET para a rota usando o cliente de teste, então afirma que o código de status da resposta é 200 (OK) e que a resposta JSON corresponde à mensagem esperada.

  3. def test_about(client):

    Parecido com test_home, esta função testa a rota sobre (/about). Ela verifica por um código de status 200 e confirma o conteúdo da resposta JSON.

  4. def test_multiply(client):

    Esta função testa a rota de multiplicar com entrada válida (/multiply/3/4). Ela verifica que o código de status é 200 e que a resposta JSON contém o resultado correto da multiplicação.

  5. def test_multiply_invalid_input(client):
    Esta função testa a rota de multiplicação com entrada inválida (multiply/three/four). Ela verifica se o código de status é 404 (Não Encontrado), o que é o comportamento esperado quando a rota não consegue corresponder as entradas de string aos parâmetros inteiros necessários.

  6. def test_non_existent_route(client):
    Esta função testa o comportamento do aplicativo quando uma rota não existente é acessada. Ela envia uma solicitação GET para /non-existent, que não está definida em nosso aplicativo Flask. O teste afirma que o código de status da resposta é 404 (Não Encontrado), garantindo que o nosso aplicativo trata corretamente solicitudes a rotas não definidas.

Estes testes abrangem a funcionalidade básica do nosso aplicativo Flask, garantindo que cada rota responda corretamente a entradas válidas e que a rota de multiplicação trata entradas inválidas apropriadamente. Utilizando pytest, podemos executar facilmente estes testes para verificar que o nosso aplicativo está funcionando conforme esperado.

Passo 4 – Executar as Testes

Para executar as testes, execute o seguinte comando:

root@ubuntu:~# pytest

Por padrão, o processo de descoberta de pytest irá varrer recursivamente a pasta atual e suas subpastas procurando arquivos que começam com nomes que contenham “test_” ou terminem com “_test”. Os testes localizados nestes arquivos então serão executados. Você deveria ver saídas semelhantes a:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app collected 5 items tests/test_app.py .... [100%] ======================================================= 5 passed in 0.19s ========================================================

Isso indica que todos os testes passaram com sucesso.

Passo 5: Usando Fixtures no pytest

Fixtures são funções que são usadas para fornecer dados ou recursos aos testes. Elas podem ser usadas para configurar e desconfigurar ambientes de teste, carregar dados ou realizar outras tarefas de configuração. No pytest, as fixtures são definidas usando o decorador @pytest.fixture.

Veja como melhorar a fixture existente. Atualize a fixture de cliente para usar lógica de configuração e desconfiguração:

test_app.py
@pytest.fixture
def client():
    """Set up a test client for the app with setup and teardown logic."""
    print("\nSetting up the test client")
    with app.test_client() as client:
        yield client  # É aqui que ocorre o teste
    print("Tearing down the test client")

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

Este setup adiciona declarações de impressão para demonstrar as fases de setup e teardown no output dos testes. Essas podem ser substituídas por código de gerenciamento de recursos até necessário.

Vamos tentar executar os testes novamente:

root@ubuntu:~# pytest -vs

A flag -v aumenta a verbosidade, e a flag -s permite que declarações de impressão sejam exibidas na saída do console.

Você deve ver a seguinte saída:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 5 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client ============================================ 5 passed in 0.35s =============================================

Step 6: Adding a Failure Test Case

Vamos adicionar um caso de teste de falha ao arquivo de teste existente. Modifique o arquivo test_app.py e adicione a função abaixo no final para um caso de teste de falha com um resultado incorreto:

test_app.py
def test_multiply_edge_cases(client):
    """Test the multiply route with edge cases to demonstrate failing tests."""
    # Test with zero
    response = client.get('/multiply/0/5')
    assert response.status_code == 200
    assert response.json == {"result": 0}

    # Test with large numbers (this might fail if not handled properly)
    response = client.get('/multiply/1000000/1000000')
    assert response.status_code == 200
    assert response.json == {"result": 1000000000000}

    # Intentional failing test: incorrect result
    response = client.get('/multiply/2/3')
    assert response.status_code == 200
    assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case"

Vamos desconstruir a função test_multiply_edge_cases e explicar o que cada parte faz:

  1. Test with zero:
    Este teste verifica se a função de multiplicação corretamente handle a multiplicação por zero. Esperamos que o resultado seja 0 quando multiplicando qualquer número por zero. Esta é uma importante condição de borda a ser testada, porque algumas implementações podem ter problemas com a multiplicação por zero.

  2. Teste com números grandes:
    Este teste verifica se a função de multiplicação pode lidar com números grandes sem estourar ou com problemas de precisão. Estamos multiplicando dois valores de um milhão, esperando um resultado de um bilião. Este teste é crucial porque verifica os limites superiores das capacidades da função. Note que isso poderia falhar se a implementação do servidor não lidar corretamente com números grandes, o que poderia indicar a necessidade de bibliotecas de números grandes ou um tipo de dado diferente.

  3. Teste propositalmente defeituoso:
    Este teste está deliberadamente configurado para falhar. Ele verifica se 2 * 3 é igual a 7, o que é incorreto. Este teste visa demonstrar o aspecto de um teste defeituoso no resultado de testes. Isto ajuda a entender como identificar e debugar testes defeituosos, que é uma habilidade essencial em desenvolvimento de testes dirigidos e processos de depuração.

Ao incluir esses casos de borda e um defeito intencional, você está testando não só a funcionalidade básica da sua rota de multiplicação, mas também seu comportamento em condições extremas e suas capacidades de relatório de erro. Esta abordagem de teste ajuda a garantir a robustez e a confiabilidade da nossa aplicação.

Vamos tentar executar os testes novamente:

root@ubuntu:~# pytest -vs

Você deveria ver o seguinte output:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 6 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_edge_cases Setting up the test client FAILED Tearing down the test client ================================================================= FAILURES ================================================================== _________________________________________________________ test_multiply_edge_cases __________________________________________________________ client = <FlaskClient <Flask 'app'>> def test_multiply_edge_cases(client): """Test the multiply route with edge cases to demonstrate failing tests.""" # Teste com zero response = client.get('/multiply/0/5') assert response.status_code == 200 assert response.json == {"result": 0} # Teste com números grandes (isto pode falhar se não for tratado corretamente) response = client.get('/multiply/1000000/1000000') assert response.status_code == 200 assert response.json == {"result": 1000000000000} # Teste propositalmente defeituoso: resultado incorreto response = client.get('/multiply/2/3') assert response.status_code == 200 > assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case" E AssertionError: This test should fail to demonstrate a failing case E assert {'result': 6} == {'result': 7} E E Differing items: E {'result': 6} != {'result': 7} E E Full diff: E { E - 'result': 7,... E E ...Full output truncated (4 lines hidden), use '-vv' to show tests/test_app.py:61: AssertionError ========================================================== short test summary info ========================================================== FAILED tests/test_app.py::test_multiply_edge_cases - AssertionError: This test should fail to demonstrate a failing case ======================================================== 1 failed, 5 passed in 0.32s ========================================================

A mensagem de falha acima indica que o teste test_multiply_edge_cases no arquivo tests/test_app.py falhou. especificamente, a última afirmação neste método de teste causou o falha.

Este defeito intencional é útil para demonstrar como os avisos de falha dos testes são relatados e quais informações são fornecidas na mensagem de falha. Ele mostra exatamente na linha onde ocorreu o erro, os valores esperados e reais, e a diferença entre eles.

Em um cenário real, você corrigiria o código para tornar o teste passar ou ajustaria o teste se o resultado esperado estivesse incorreto. No entanto, neste caso, a falha é intencional por finalidades educacionais.

Conclusão

Neste tutorial, nós abordamos como configurar testes unitários para uma aplicação Flask usando pytest, integrando fixtures pytest e mostrando o que uma falha de teste se parece. Ao seguir essas etapas, você pode garantir que suas aplicações Flask são confiáveis e manutenveis, minimizando bugs e melhorando a qualidade do código.

Você pode referir-se à documentação oficial de Flask e Pytest para aprender mais.

Source:
https://www.digitalocean.com/community/tutorials/unit-test-in-flask