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 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 unittest que vem pré-instalado com o Python.

A aplicação

Primeiro, precisamos de uma aplicação para testar; usaremos a aplicação do Tutorial. Se você ainda não tem essa aplicação, pegue o código fonte do exemplo traduzido.

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 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 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 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 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 [1]; isso permite que o 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 response_class. Então podemos usar o atributo 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 [2]:

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='<Olá>',
        texto='<strong>HTML</strong> é permitido aqui'
    ), follow_redirects=True)
    assert rv.status_code == 200
    assert 'nenhuma entrada' not in rv.data
    assert '&lt;Olá&gt;' in rv.data
    assert '<strong>HTML</strong> é 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.

Outros truques para testes

Além de usar o cliente de testes apresentado acima, há também o método 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 request, g e 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 Application Factories).

Note no entanto que se você está usando um contexto de requisição de teste, as funções before_request() e after_request() não são automaticamente invocadas. No entanto, as funções teardown_request() são invocadas quando o bloco with é encerrado. Se você precisa que as funções funções before_request() sejam invocadas, precisa chamar 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 after_request() terá que invocar 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

New in version 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 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 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

New in version 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 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 flask.session. Entretanto o este objeto implementa a mesma interface.

Notas da tradução

[1]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.
[2]

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.