O que não estiver testado, está quebrado
A origem desta citação é desconhecida e enquanto que não é inteiramente verdadeiro, também não está longe da verdade. Aplicações que não são testadas fazem com que seja difícil de melhorar o código existente e desenvolvedores de aplicações que não usam testes tendem a se tornar muito paranoicos. Se uma aplicação faz uso de testes automatizados, você pode seguramente fazer mudanças e instantaneamente saber se alguma coisa quebrou.
Flask fornece uma maneira de testar sua aplicação através da exposição do cliente de teste do Werkzeug e manipula os contexto locais por você. Você pode então usa-lo com a sua solução de teste favorita.
Nesta documentação usaremos o pacote pytest como framework base para os nossos testes. Você pode instala-lo com o comando pip
, como em:
$ pip install pytest
Primeiro, precisaremos de uma aplicação para testar; usaremos a aplicação construida na secção do Tutorial. Se você ainda não tiver a aplicação, pegue o código-fonte dos exemplos.
Só então é que podemos importar o módulo flaskr
corretamente, precisamos executar pip install -e .
dentro da pasta tutorial.
Nós começaremos por adicionar uma pasta de testes com o nome tests
dentro da raiz da aplicação. Depois criar um ficheiro em Python com o nome test_flaskr.py
para guardar os nossos testes. Quando formatamos o nome do ficheiro como test_*.py
, ele será automaticamente alcançado pelo pytest.
A seguir, criamos um pytest fixture com nome de client
que configura a aplicação para os testes e inicializa um novo banco de dados:
import os
import tempfile
import pytest
from flaskr import create_app
@pytest.fixture
def client():
db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
flaskr.app.config['TESTING'] = True
with flaskr.app.test_client() as client:
with flaskr.app.app_context():
flaskr.init_db()
yield client
os.close(db_fd)
os.unlink(flaskr.app.config['DATABASE'])
Esta fixture do cliente será chamada em cada teste individual. Isto dá-nos uma interface simples para aplicação, onde poderemos realizar requisições de teste para aplicação. O cliente também rasteará os cookies por nós.
Durante a configuração, a bandeira (flag) TESTING
é ativada. O que isto faz é desativar a captura de erros durante a realização de uma requisição, assim você pode receber relatório de erros melhores quando estiver realizando requisições de teste contra a aplicação.
Por SQLite3 ser baseado no sistema de ficheiros, podemos facilmente usar o módulo tempfile
para criar um banco de dados temporário e inicializa-lo. A função mkstemp()
faz duas coisas por nós: ela retorna um manipulador de ficheiro de baixo nível e um nome de ficheiro aleatório, que por fim usamos como nome de banco de dados. Apenas temos que deixar o db_fd por perto, assim podemos usar a função os.close()
para fechar o ficheiro.
Para deletar o banco de dados depois do teste, o fixture fecha o ficheiro e remove-o do sistema de ficheiro.
Se agora executarmos a sequência de teste (test suite), veremos uma saída semelhante a que está abaixo:
$ pytest
================ test session starts ================
rootdir: ./flask/examples/flaskr, inifile: setup.cfg
collected 0 items
=========== no tests ran in 0.07 seconds ============
Apesar de não ter executado nenhum teste atual, já sabemos que nossa aplicação flaskr
é sintaticamente valida, ou do contrário a importação teria morrido com uma exceção.
Agora é o momento de começar a testar a funcionalidade da aplicação. Vamos verificar se a aplicação exibe o texto "No entries here so far" quando acessamos a raiz da aplicação (/). Para fazer isso, adicionamos uma nova função de teste como esta ao test_flaskr.py
:
def test_empty_db(client):
"""Start with a blank database."""
rv = client.get('/')
assert b'No entries here so far' in rv.data
Repare que nossas funções de testes começam com a palavra test; isto permite que o pytest identifique automaticamente a função como um teste a ser executado.
Ao usar o client.get
podemos enviar uma requisição HTTP do tipo GET
para aplicação no caminho dado. O valor retornado será um objeto response_class
. Podemos agora usar o atributo data
para inspecionar o valor retornado (como string) da aplicação. Neste caso, nós garantimos que 'No entries here so far'
seja parte da saída.
Execute-o novamente e verás um teste passando:
$ pytest -v
================ test session starts ================
rootdir: ./flask/examples/flaskr, inifile: setup.cfg
collected 1 items
tests/test_flaskr.py::test_empty_db PASSED
============= 1 passed in 0.10 seconds ==============
A maioria das funcionalidades de nossa aplicação está somente disponível para o usuário administrador, sendo assim precisamos de uma maneira de permitir com que o nosso cliente de teste inicie e termine sessão em nossa aplicação. Para fazer isso, disparamos algumas requisições para as páginas login e logout com os dados obrigatórios do formulário (username e password). E pelas páginas de login e logout redirecionarem, dizemos ao cliente para prosseguir o redirecionamento configurando o atributo follow_redirects com valor verdadeiro.
Adiciona as seguintes funções dentro do seu ficheiro test_flaskr.py
:
def login(client, username, password):
return client.post('/login', data=dict(
username=username,
password=password
), follow_redirects=True)
def logout(client):
return client.get('/logout', follow_redirects=True)
Agora podemos testar facilmente que o inicio e termino de sessão funcionam e que falham com credenciais inválidas. Adiciona esta nova função de teste:
def test_login_logout(client):
"""Make sure login and logout works."""
username = flaskr.app.config["USERNAME"]
password = flaskr.app.config["PASSWORD"]
rv = login(client, username, password)
assert b'You were logged in' in rv.data
rv = logout(client)
assert b'You were logged out' in rv.data
rv = login(client, f"{username}x", password)
assert b'Invalid username' in rv.data
rv = login(client, username, f'{password}x')
assert b'Invalid password' in rv.data
Devemos também testar que a adição de mensagem funciona. Adiciona uma função de teste como está:
def test_messages(client):
"""Test that messages work."""
login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'])
rv = client.post('/add', data=dict(
title='<Hello>',
text='<strong>HTML</strong> allowed here'
), follow_redirects=True)
assert b'No entries here so far' not in rv.data
assert b'<Hello>' in rv.data
assert b'<strong>HTML</strong> allowed here' in rv.data
Aqui verificamos que o código HTML é permitido no texto, mas não no título, o que é o comportamento esperado.
Agora ao executar deve devolver-nos três testes passando:
$ pytest -v
================ test session starts ================
rootdir: ./flask/examples/flaskr, inifile: setup.cfg
collected 3 items
tests/test_flaskr.py::test_empty_db PASSED
tests/test_flaskr.py::test_login_logout PASSED
tests/test_flaskr.py::test_messages PASSED
============= 3 passed in 0.23 seconds ==============
Além de usar o cliente de teste como é exibido acima, há também o método test_request_context()
que pode ser usado combinado com o declaração with
para ativar uma contexto de requisição temporariamente. Com isto você pode acessar os objetos request
, g
e session
como nas funções de apresentação (views). Aqui está um exemplo completo que demonstra o uso desta técnica:
import flask
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
assert flask.request.path == '/'
assert flask.request.args['name'] == 'Peter'
Todos os outros objetos que estão relacionados ao contexto podem ser usados da mesma maneira.
Se você quiser testar sua aplicação usando configurações diferentes e parecer não haver uma boa maneira de faze-lo, considere a possibilidade de mudar a fábricas de aplicação (veja Fábricas de Aplicação).
Perceba, todavia se você estiver usando um contexto de requisição de teste, as funções before_request()
e after_request()
não são chamados automaticamente. Contudo as funções teardown_request()
são de fato executadas quando o contexto de requisição de teste deixa o bloco with
. Se você quiser que as funções before_request()
sejam executadas também, você precisa executar você mesmo o preprocess_request()
:
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
app.preprocess_request()
...
Pode ser necessário para isto, abrir conexões com banco de dados ou algo similar dependendo de como a sua aplicação foi desenhada.
Se você quiser executar as funções after_request()
você precisa chama-las dentro do process_response()
que todavia requer que você passe-o um objeto de resposta:
app = flask.Flask(__name__)
with app.test_request_context('/?name=Peter'):
resp = Response('...')
resp = app.process_response(resp)
...
Em geral isto é menos útil porque até aquele momento você pode de maneira direta começar a usar o cliente de teste.
- Relatório de Mudança
- Novo a partir da versão 0.10
Um padrão muito comum é guardar informações de autorização do usuário e conexões com o banco de dados no contexto da aplicação ou no objeto flask.g
. O principal padrão para se alcançar isso é pôr lá o objeto em primeiro momento de uso e depois remove-lo numa teardown. Imagine por algo momento este código para pegar o usuário atual:
def get_user():
user = getattr(g, 'user', None)
if user is None:
user = fetch_current_user_from_database()
g.user = user
return user
Para um teste seria bom sobrescrever este usuário a partir de fora sem ter que mudar código algum. Isto pode ser alcançado encaixando o sinal flask.appcontext_pushed
:
from contextlib import contextmanager
from flask import appcontext_pushed, g
@contextmanager
def user_set(app, user):
def handler(sender, **kwargs):
g.user = user
with appcontext_pushed.connected_to(handler, app):
yield
E depois usa-lo:
from flask import json, jsonify
@app.route('/users/me')
def users_me():
return jsonify(username=g.user.username)
with user_set(app, my_user):
with app.test_client() as c:
resp = c.get('/users/me')
data = json.loads(resp.data)
assert data['username'] == my_user.username
- Relatório de Mudança
- Novo a partir da versão 0.4
Algumas vezes é útil acionar uma requisição comum porém todavia manter o contexto por perto por mais tempo para que uma introspeção adicional possa acontecer. Com o Flask 0.4 isso é possível através do método test_client()
com um bloco with
:
app = flask.Flask(__name__)
with app.test_client() as c:
rv = c.get('/?tequila=42')
assert request.args['tequila'] == '42'
Se você estivesse usando apenas o test_client
sem o bloco with
, o assert
falharia com um erro porque request não se encontra mais disponível (porque você está tentando usa-lo fora da requisição atual).
- Relatório de Mudança
- Novo a partir da versão 0.8
Algumas vezes pode ser muito útil acessar ou modificar as sessões a partir do cliente de teste. Geralmente existem duas maneiras de fazer isso. Se você quiser apenas garantir que a sessão tem certas chaves para certos valores, você pode apenas manter o contexto por perto e acessar flask.session
:
with app.test_client() as c:
rv = c.get('/')
assert flask.session['foo'] == 42
Isto contudo não torna-o possível modificar ou acessar a sessão antes que uma requisição seja acionada. Desde a versão 0.8 do Flask passamos a fornecer o que é chamado de "transação de sessão" que simula as chamadas apropriadas para abertura de uma sessão dentro do contexto do cliente de teste e modifica-lo. Ao final da transação a sessão está guardada e pronta para ser usada pelo cliente de teste. Isto funciona independentemente da sessão usada no backend:
with app.test_client() as c:
with c.session_transaction() as sess:
sess['a_key'] = 'a value'
# uma vez que isto estiver alcançado a sessão foi guardada e está pronta para ser usada pelo cliente
c.get(...)
Repare que neste caso, você tem de usar o objeto sess
ao invés do proxy flask.session
. O objeto contudo ele mesmo fornecerá a mesma interface.
- Relatório de Mudança
- Novo a partir da versão 1.0
O Flask tem grande suporte para JSON, que é uma escolha popular para construção de APIS em JSON. Fazer requisições em JSON e examinar o JSON retornado pela resposta é algo muito conveniente:
from flask import request, jsonify
@app.route('/api/auth')
def auth():
json_data = request.get_json()
email = json_data['email']
password = json_data['password']
return jsonify(token=generate_token(email, password))
with app.test_client() as c:
rv = c.post('/api/auth', json={
'email': '[email protected]', 'password': 'secret'
})
json_data = rv.get_json()
assert verify_token(email, json_data['token'])
Passando o argumento json
dentro do método do cliente de teste configura o dado requisitado para objeto JSON serializado e configura o tipo de conteúdo para application/json
. Você recebe JSON a partir da requisição ou resposta através do método get_json
.
O Click vem com utilitários para testar seus comandos da CLI. Um CliRunner
executa os comandos de forma insolada e captura suas saídas dentro de um objeto Result
.
O Flask fornece o test_cli_runner()
para criar um FlaskCliRunner
que passa a aplicação Flask para a CLI automaticamente. Use seu método invoke()
para chamar comandos da mesma maneira que seriam chamados a partir da linha de comando.
import click
@app.cli.command('hello')
@click.option('--name', default='World')
def hello_command(name):
click.echo(f'Hello, {name}!')
def test_hello():
runner = app.test_cli_runner()
# invoca o comando diretamente
result = runner.invoke(hello_command, ['--name', 'Flask'])
assert 'Hello, Flask' in result.output
# ou pelo seu nome
result = runner.invoke(args=['hello'])
assert 'World' in result.output
No exemplo acima, é útil invocar o comando pelo nome porque ele verifica que o comando foi corretamente registado com a aplicação.
Se você quiser testar como o seu comando parsea os parâmetros, sem executar o comando, use seu método make_context()
. Isto é útil para testes de validações complexas e tipos personalizados.
def upper(ctx, param, value):
if value is not None:
return value.upper()
@app.cli.command('hello')
@click.option('--name', default='World', callback=upper)
def hello_command(name):
click.echo(f'Hello, {name}!')
def test_hello_params():
context = hello_command.make_context('hello', ['--name', 'flask'])
assert context.params['name'] == 'FLASK'