Como executar testes unitários em Flask

Introdução

Testes são essenciais no processo de desenvolvimento de software, garantindo que o código se comporta como esperado e está livre de defeitos. Em Python, o pytest é um framework de testes popular que oferece várias vantagens sobre o módulo padrão unit test, que é um framework de teste embutido do Python e faz parte da biblioteca padrão. O pytest inclui uma sintaxe simples, melhor saída, ferramentas de fixtures poderosas e um rico ecossistema de plugins. Este tutorial irá guiar você através do processo de configuração de uma aplicação Flask, da integração de ferramentas de fixtures do pytest e da escrita de testes unitários usando o pytest.

Pré-requisitos

Antes de começar, você precisará dos seguintes itens:

  • Um servidor rodando Ubuntu e um usuário não-root com privilégios de sudo e uma firewall ativa. Para orientações sobre como configurar isso, por favor escolha sua distribuição do sistema operacional da lista desta 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 essa guia sobre primeiro contato com a linha de comando do Linux.

  • Um entendimento básico de programação em 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 Python em Ubuntu, você pode referir-se à nossa tutorial sobre Como executar um script Python em Ubuntu.

Porque pytest é uma Alternativa Melhorado ao unittest

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

  • Pytest permite que você escreva testes com menos código de configuração, usando simples declarações assert em vez das métodos mais verbosos necessários pelo 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 suítes de teste.

Com estes benefícios, o pytest frequentemente é a escolha preferida para testes em Python moderno. Vamos configurar uma aplicação Flask e escrever testes unitários usando o pytest.

Passo 1 – Configurando o Ambiente

O Ubuntu 24.04 vem com Python 3 por padrão. Abra o terminal e execute o seguinte comando 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 e obter a instalação do Python 3:

root@ubuntu:~# sudo apt install python3

A seguir, você precisará 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 seguinte código:

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 seguinte comando:

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ê notará que o servidor está rodando em http://127.0.0.1 e está ouvindo na porta 5000. Abra outra consola do 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 requisições GET fazem:

  1. curl http://127.0.0.1:5000/:
    Essa é uma requisição GET enviada 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 da nossa rota inicial.

  2. curl http://127.0.0.1:5000/about:
    Essa é uma requisição GET enviada 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:
    Essa é uma requisição GET enviada para a rota /multiply com dois parâmetros: 10 e 20. O servidor multiplica esses 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 de URL e executar cálculos.

Estas requisições GET permitem que interajamos com as API dos pontos finais do nosso aplicativo Flask, obtendo informações ou disparando ações no servidor sem modificar dados. Elas são úteis para buscar dados, testar a funcionalidade de pontos finais e verificar se nossas rotas estão respondendo como esperado.

Vamos ver cada uma destas requisições GET 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 um aplicativo Flask básico, 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 o 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 fábrica de pytest que cria um cliente de teste para nossa aplicação 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, e então verifica se o código de status da resposta é 200 (OK) e se a resposta JSON corresponde à mensagem esperada.

  3. def test_about(client):
    Semelhante a test_home, esta função testa a rota about (/about). Ela verifica o código de status 200 e valida o conteúdo da resposta JSON.

  4. def test_multiply(client):
    Esta função testa a rota de multiplicação com entrada válida (/multiply/3/4). Ela verifica se o código de status é 200 e se 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 multiplicar 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 combinar as entradas de string com os 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 nosso aplicativo trata corretamente solicitudes a rotas não definidas.

Estes testes cobrem a funcionalidade básica do nosso aplicativo Flask, garantindo que cada rota responda corretamente a entradas válidas e que a rota de multiplicar trata entradas inválidas apropriadamente. Utilizando pytest, podemos executar esses testes facilmente para verificar se nosso aplicativo está funcionando conforme esperado.

Passo 4 – Executando 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 o nome “test_” ou terminam com “_test”. Os testes localizados nestes arquivos serão entã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. Eles podem ser usados 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.

Aqui está como melhorar a fixture existente. Atualize a fixture do 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 é onde o teste acontece
    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

Esta configuração adiciona instruções de impressão para demonstrar as fases de setup e teardown no resultado do teste. Estas podem ser substituídas por código de gerenciamento de recursos reais se necessário.

Vamos tentar executar os testes novamente:

root@ubuntu:~# pytest -vs

A flag -v aumenta a verbosidade, e a flag -s permite que as instruções de impressão sejam exibidas no resultado do console.

Você deve ver o seguinte resultado:

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 analisar 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 está corretamente lidando com a multiplicação por zero. Esperamos que o resultado seja 0 quando multiplicamos qualquer número por zero. Esse é um caso de borda importante para testar, 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 consegue lidar com números grandes sem sobrecarregar ou ter problemas de precisão. Estamos multiplicando dois valores de um milhão por um milhão, esperando um resultado de um trilhã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 intencionalmente 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 do teste. 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 estes casos de borda e uma falha intencional, você está testando não apenas a funcionalidade básica da sua rota de multiplicação, mas também o seu comportamento sob condições extremas e suas capacidades de relatório de erros. 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 lidado corretamente) response = client.get('/multiply/1000000/1000000') assert response.status_code == 200 assert response.json == {"result": 1000000000000} # Teste de falha intencional: 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 nesta função de teste causou a falha.

Essa falha intencional é útil para mostrar como são relatadas falhas de teste e quais informações são fornecidas na mensagem de falha. Ela mostra exatamente na linha onde ocorreu a falha, 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 para fins educacionais.

Conclusão

Neste tutorial, abordamos como configurar testes unitários para uma aplicação Flask usando o pytest, integrar fixtures do pytest e mostrarão o que uma falha de teste se parece. Ao seguir estas etapas, você pode garantir que suas aplicações Flask são confiáveis e manutenveis, minimizando erros 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