.. _testing:
Testando aplicações Flask
=========================
**Qualquer coisa sem testes está quebrada.**
A origem desta citação é desenconhecida, e embora não esteja completamente
certa, também não está longe da verdade. Apliações sem testes tornam difícil
melhorar o código existente e desenvolvedores de aplicações sem testes tendem
a ficar bastante paranóicos. Se sua aplicação tem testes automatizados, você pode fazer alterações com segurança pois saberá imediatamente se algo quebrou.
Flask fornece uma forma de testar sua aplicação dando acesso ao
:class:`~werkzeug.test.Client` da biblioteca Werkzeug, e gerenciando as
variáveis locais do contexto para você. Com isso você pode usar a sua solução
favorita para testes. Nesta documentação, usaremos o pacote :mod:`unittest`
que vem pré-instalado com o Python.
A aplicação
-----------
Primeiro, precisamos de uma aplicação para testar; usaremos a aplicação do
:ref:`tutorial`. Se você ainda não tem essa aplicação, pegue o código fonte do
`exemplo traduzido`_.
.. _exemplo traduzido:
http://github.com/ramalho/flask-br/tree/master/examples/flaskr/
O esqueleto dos testes
----------------------
Para testar a aplicação, criaremos um segundo módulo (`flaskr_tests.py`) e
nele montaremos o esqueleto dos testes unitários::
# coding: utf-8
import os
import flaskr
import unittest
import tempfile
class FlaskrTestCase(unittest.TestCase):
def setUp(self):
self.bd_arq, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
flaskr.app.config['TESTING'] = True
self.app = flaskr.app.test_client()
flaskr.criar_bd()
def tearDown(self):
os.close(self.bd_arq)
os.unlink(flaskr.app.config['DATABASE'])
if __name__ == '__main__':
unittest.main()
O código no método :meth:`~unittest.TestCase.setUp` cria um novo cliente de
testes (``test_client``) e inicializa um novo banco de dados. Este método é
invocado antes da execução de cada método de teste na classe
``FlaskrTestCase`` (ainda não escrevemos nenhum teste). Para excluir o banco
de dados após cada teste, fechamos o arquivo e o apagamos do disco em
:meth:`~unittest.TestCase.tearDown`. Além disso, no ``setUp``, a configuração
``TESTING`` é ligada. Isso desabilita a captura de erros durante o tratamento
das requisições, gerando relatórios de erros melhores quando fazemos
requisições de teste.
Este cliente de testes nos oferece uma interface fácil para a aplicação.
Podemos disparar requisições de testes na aplicação, e o cliente também
gerencia os cookies par nós.
Como o SQLite3 é baseado em sistema de arquivos, podemos facilmente usar o módulo ``tempfile`` para criar um banco de dados temporário e incializá-lo. A função :func:`~tempfile.mkstemp` faz duas coisas: devolve um descritor de arquivo aberto e o caminho para o arquivo de nome aleatório; este último usamos como nome do banco de dados. Precisamos guardar o descritor `bd_arq` para podermos depois fechar o arquivo com a função :func:`os.close`.
Se neste momento executarmos a suíte de testes, veremos o seguinte resultado::
$ python flaskr_tests.py
----------------------------------------------------------------------
Ran 0 tests in 0.000s
OK
Mesmo não tendo executado nenhum teste, já sabemos que nossa aplicação Flaskr
não tem erros de sintaxe, pois se tivesse o import inicial teria falhado.
Primeiro teste
--------------
Agora é hora de começar a testar a funcionalidade da aplicação. Vamos verificar se a aplicação exibe "nenhuma entrada" ao acessar a raiz da aplicação (``/``).
Para tanto, vamos acrescentar um método de testes em nossa classe, assim::
class FlaskrTestCase(unittest.TestCase):
def setUp(self):
self.bd_arq, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
flaskr.app.config['TESTING'] = True
self.app = flaskr.app.test_client()
flaskr.criar_bd()
def tearDown(self):
os.close(self.bd_arq)
os.unlink(flaskr.app.config['DATABASE'])
def teste_bd_vazio(self):
res = self.app.get('/')
assert 'nenhuma entrada' in res.data
Observe que os métodos de teste usam o prefixo `test` [#]_; isso permite que o
:mod:`unittest` identifique automaticamente estes métodos como testes a serem executados.
O método `self.app.get` envia para a aplicação uma requisição HTTP GET para o
caminho especificado. O valor devolvido será um objeto
:class:`~flask.Flask.response_class`. Então podemos usar o atributo
:attr:`~werkzeug.wrappers.BaseResponse.data` para inspecionar o conteúdo
devolvido pela aplicação, que é uma string. Neste caso, verificamos que a sub-
string ``'nenhuma entrada'`` está presente.
Rode o teste de novo e você verá que agora temos um teste passando::
$ python flaskr_tests.py
.
----------------------------------------------------------------------
Ran 1 test in 0.034s
OK
Logando e deslogando
--------------------
A maior parte das funcionalidades da nossa aplicação só está disponível para o
usuário administrador, por isso precisamos de uma maneira de fazer o nosso
cliente de teste logar e deslogar. Para tanto, vamos disparar requisições para
as páginas de login e logout com os dados do obrigatórios do formulário
(usuário e senha). E, como as páginas de login e logout fazem
redirecionamentos, ordenamos que o cliente ``follow_redirects`` (seguir
redirecionamento).
Coloque estes dois métodos na continuação da sua classe `FlaskrTestCase`::
def login(self, username, password):
return self.app.post('/entrar', data=dict(
username=username,
password=password
), follow_redirects=True)
def logout(self):
return self.app.get('/sair', follow_redirects=True)
Agora podemos facilmente verificar que logar e deslogar funciona, e que o
login exige as credenciais corretas. Acrescente este novo teste à classe
[#]_::
def teste_login_logout(self):
rv = self.login('admin', 'default')
assert 'Login OK' in rv.data
rv = self.logout()
assert 'Logout OK' in rv.data
rv = self.login('adminx', 'default')
assert 'Usuário inválido' in rv.data
rv = self.login('admin', 'defaultx')
assert 'Senha inválida' in rv.data
Testar inserção de entradas
---------------------------
Também devemos testar que é possível inserir entradas no blog. Crie um novo
método de teste assim::
def teste_nova_entrada(self):
self.login('admin', 'default')
rv = self.app.post('/inserir', data=dict(
titulo='
',
texto='HTML é permitido aqui'
), follow_redirects=True)
assert rv.status_code == 200
assert 'nenhuma entrada' not in rv.data
assert '<Olá>' in rv.data
assert 'HTML é permitido aqui' in rv.data
Aqui verificamos que é permitido usar HTML no texto mas não no título, que é o
comportamento desejado.
Ao executar os testes agora devemos ter três passando::
$ python flaskr_tests.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.332s
OK
Para ver testes mais complexos verificando cabeçalhos e códigos de statos,
veja o exemplo `MiniTwit`_ no repositório do Flask, que possui uma suite de
testes mais extensa.
.. _MiniTwit:
http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/
Outros truques para testes
--------------------------
Além de usar o cliente de testes apresentado acima, há também o método
:meth:`~flask.Flask.test_request_context` que pode ser usado em combinação
com a instrução ``with`` para ativar um contexto de requisição temporariamente. Com isso você pode acessar os objetos :class:`~flask.request`,
:class:`~flask.g` e :class:`~flask.session` como se estivesse em uma função de view. Eis um exemplo que ilustra esta técnica::
app = flask.Flask(__name__)
with app.test_request_context('/?nome=Pedro'):
assert flask.request.path == '/'
assert flask.request.args['nome'] == 'Pedro'
Todos os demais objetos que são vinculados ao contexto pode ser usados da
mesma maneira.
Se você quer testar sua aplicação com diferentes configurações e procuar uma
boa forma de fazer isso, considere o uso de uma fábrica de aplicações (veja
:ref:`app-factories`).
Note no entanto que se você está usando um contexto de requisição de teste, as
funções :meth:`~flask.Flask.before_request` e
:meth:`~flask.Flask.after_request` não são automaticamente invocadas. No
entanto, as funções :meth:`~flask.Flask.teardown_request` são invocadas quando
o bloco ``with`` é encerrado. Se você precisa que as funções funções
:meth:`~flask.Flask.before_request` sejam invocadas, precisa chamar
:meth:`~flask.Flask.preprocess_request` em seu teste::
app = flask.Flask(__name__)
with app.test_request_context('/?nome=Pedro'):
app.preprocess_request()
...
Isto pode ser necessário para abrir a conexão com o banco de dados ou realizar
alguma outra operação do gênero, dependendo de como sua aplicação foi
projetada.
Se quiser acionar as funções :meth:`~flask.Flask.after_request` terá que
invocar :meth:`~flask.Flask.process_response`, que precisa receber um objeto
`response`::
app = flask.Flask(__name__)
with app.test_request_context('/?nome=Pedro'):
resp = Response('...')
resp = app.process_response(resp)
...
Porém isso em geral não é tão util, porque neste ponto vale mais a pena usar o
cliente de testes.
Mantendo o contexto por mais tempo
----------------------------------
.. versionadded:: 0.4
Às vezes pode ser útil disparar uma requisição normalmente mas ainda assim
manter o contexto vivo por mais um tempo para permitir alguma introspecção
adicional. A partir do Flask 0.4 isso é possível usando o
:meth:`~flask.Flask.test_client` em 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ê usasse apenas o :meth:`~flask.Flask.test_client` sem o bloco ``with``
o ``assert`` geraria um erro porque o ``request`` não está mais disponível
(você estaria tentando acessá-lo fora do contexto de uma requisição).
Acessar e modificar sessões
---------------------------
.. versionadded:: 0.8
Às vezes pode ser muito útil acessar ou modificar uma sessão a partir do
cliente de testes. Geralmente há duas formas de fazer isto. Se você apenas
quer verificar que a sessão tem determinadas chaves com certos valores, pode
simplesmente manter o contexto ativo e acessar :data:`flask.session`::
with app.test_client() as c:
rv = c.get('/')
assert flask.session['foo'] == 42
Entretanto, desta maneira não é possível modificar a sessão ou acessar a
sessão antes da requisição ser disparada. A partir do Flask 0.8 fornecemos uma
"transação de sessão" que simula as chamadas apropriadas para se abrir uma
sessão no contexto do cliente de testes e modificá-la. Ao final da transação,
a sessão é armazenada. Isto funciona independente do backend de sessão que
estiver sendo usado::
with app.test_client() as c:
with c.session_transaction() as sessao:
sessao['uma_chave'] = 'um valor'
# ao chegar aqui a sessão estará armazenada
Note que neste caso você deve usar o objeto ``sessao`` em vez do proxy
:data:`flask.session`. Entretanto o este objeto implementa a mesma interface.
.. rubric:: Notas da tradução
.. [#] Os métodos de teste podem começar com a palavra `teste` também. O
importante é que as primeiras letras sejam ``test``, pois este é o prefixo
default definido em ``unittest.TestLoader.testMethodPrefix``.
.. [#] Este teste revela uma armadilha sobre a representação das respostas
HTTP no Flask: ao incluir textos acentuados na resposta, como fazemos em
caso de erro na função ``flaskr.login``, precisamos passar este textos
como strings Unicode (instâncias de ``unicode``, denotadas pelo prefixo
``u`` nas mensagens como ``u'Senha inválida'``). Se isso não for feito lá,
encontramos uma exceção porque o Flask assume que as strings de bytes
``str`` passadas como parâmetro para o template são strings ASCII. No entanto, ao testar a resposta em ``teste_login_logout` somos obrigados a
usar strings de bytes ``str``, porque se usarmos ``unicode`` o Python
assume que a resposta em ``rv.data`` é uma string ASCII e ao tentar
converter para Unicode para poder comparar, uma exceção
``UnicodeDecodeError`` é gerada. Por isso temos strings de bytes ``str``,
e neste caso funciona porque o nosso código-fonte está em UTF-8 (veja
o comentário na linha 1), e a resposta produzida pelo Flask utiliza este
mesmo encoding. Uma alternativa que também funciona é escrever os testes
assim::
assert u'Usuário inválido' in rv.data.decode('utf-8')
Aqui estamos explicitamente convertendo os dados da resposta de UTF-8
para Unicode, e assim podemos testar com segurança contra nossa string
``unicode``. O problema que descrevemos aqui não acontece no tutorial
original do Flask, pois ele foi escrito em inglês, e lá todas as strings
são ASCII puro.