TDD com Python e Flask
Post originalmente publicado no Python Club.
Baseado na palestra que ofereci no encontro do Grupy-SP, em 12 de março de 2016. O código dessa atividade está disponível no meu GitHub.
A ideia desse exercício é introduzir a ideia de test driven development (TDD) usando Python e Flask — digo isso pois a aplicação final desse “tutorial” não é nada avançada, tampouco funcional. E isso se explica por dois motivos: primeiro, o foco é sentir o que é o driven do TDD, ou seja, como uma estrutura de tests first (sempre começar escrevendo os testes, e não a aplicação) pode guiar o processo de desenvolvimento; e, segundo, ser uma atividade rápida, de mais ou menos 1h.
Em outras palavras, não espere aprender muito de Python ou Flask. Aqui se concentre em sentir a diferença de utilizar um método de programar. Todo o resto é secundário.
- Preparando o ambiente
Requisitos
Para esse exercício usaremos o Python versão 3.5.1 com o framework Flask versão 0.10.1. É recomendado, mas não necessário, usar um virtualenv.
Como o código é bem simples, não acho que você vá ter muitos problemas se utilizar uma versão mais antiga do Python (ou mesmo do Flask). Em todo caso, em um detalhe ou outro você pode se deparar com mensagens distintas se utilizar o Python 2.
Você pode verificar a versão do seu Python com esse comando:
Dependendo da sua instalação, pode ser que você tenha que usar python3 ao invés de python — ou seja, o comando todo deve ser python3 --version. O resultado deve ser esse:
E instalar o Flask assim:
O pip é um gerenciador de pacotes do Python. Ele vem instalado por padrão nas versões mais novas do Python. Dependendo da sua instalação, pode ser que você tenha que usar pip3 ao invés de pip — ou seja, o comando todo deve ser pip3 install Flask. Com esse comando ele vai instalar o Flask e qualquer dependência que o Flask tenha:
Arquivos
Vamos usar, nesse exercício, basicamente 2 arquivos:
app.py: onde criamos nossa aplicação web; tests.py: onde escrevemos os testes que guiarão o desenvolvimento da aplicação, e que, também, garantirão que ela funcione.
- Criando a base dos testes
No arquivo tests.py vamos usar o módulo unittest, que já vem instalado por padrão no Python.
Criaremos uma estrutura básica para que, toda vez que esse arquivo seja executado, o unittest se encarregue de encontrar todos os nossos testes e rodá-los.
Vamos começar escrevendo com um exemplo fictício: testes para um método que ainda não criamos, um método que calcule números fatoriais. A ideia é só entender como escreveremos testes em um arquivo (tests.py) para testar o que escreveremos no outro arquivo (app.py).
A estrutura básica a seguir cria um caso de teste da unittest e, quando executada, teste nosso método fatorial(numero) para todos os números de 0 até 6:
Se você conhece um pouco de inglês, pode ler o código em voz alta, ele é quase auto explicativo: importamos o módulo unittest (linha 1), criamos um objeto que é um caso de teste do método fatorial (linha 4), escrevemos um método de teste (linha 6) e esse método se assegura de que o retorno de fatorial(numero) é o resultado que esperamos (linhas 5 a 11).
Agora podemos rodar os testes assim:
Veremos uma mensagem de erro, NameError, pois não definimos nossa função fatorial(numero):
Tudo bem, a ideia não é brincar com matemática agora. Mas vamos criar essa função lá no app.py só para ver como a gente pode “integrar” esses dois arquivos — ou seja, fazer o tests.py testar o que está em app.py.
Vamos adicionar essas linhas ao app.py:
E adicionar essa linha no topo do tests.py:
Agora, rodando os testes vemos que a integração entre app.py e tests.py está funcionando:
Ótimo. Chega de matemática, vamos ao TDD com Flask, um caso muito mais tangível do que encontramos no nosso dia-a-dia.
- Primeiros passos para a aplicação web
Criando um servidor web
Como nosso foco é começar uma aplicação web, podemos descartar os testes e o método fatorial que criamos no passo anterior. Ao invés disso, vamos escrever um teste simples, para ver se conseguimos fazer o Flask criar um servidor web.
Descarte tudo do tests.py substituindo o conteúdo do arquivo por essas linhas:
Esse arquivo agora faz quatro coisas referentes a nossa aplicação web:
- Importa o objeto meu_web_app (que ainda não criamos) do nosso arquivo app.py;
- Cria uma instância da nossa aplicação web específica para nossos testes (é o método meu_web_app.test_client(), cujo retorno batizamos de app);
- Tenta acessar a “raíz” da nossa aplicação — ou seja, se essa aplicação web estivesse no servidor pythonclub.com.br estaríamos acessando http://pythonclub.com.br/.
- Verifica se, ao acessar esse endereço, ou seja, se ao fazer a requisição HTTP para essa URL, temos como resposta o código 200, que representa sucesso.
Os códigos de status de requisição HTTP mais comuns são o 200 (sucesso), 404 (página não encontrada) e 302 (redirecionamento) — mas a lista completa é muito maior que isso.
De qualquer forma não conseguiremos rodar esses testes. O interpretador do Python vai nos retornar um erro:
Então vamos criar o objeto meu_web_app lá no app.py. Descartamos tudo que tínhamos lá substituindo o contéudo do arquivo por essas linhas:
Apenas estamos importando a classe principal do Flask, e criando uma instância dela. Em outras palavras, estamos começando a utilizar o framework.
E agora o erro muda:
Importamos nosso meu_web_app, mas quando instanciamos o Flask temos um problema. Qual problema? O erro nos diz: quando tentamos chamar Flask() na linha 3 do app.py está faltando um argumento posicional obrigatório (missing 1 required positional argument). Estamos chamando Flask() sem nenhum argumento. O erro ainda nos diz que o que falta é um nome (import_name). Vamos batizar nossa instância com um nome:
E agora temos uma nova mensagem de erro, ou seja, progresso!
Eu amo testes que falham! A melhor coisa é uma notificação em vermelho me dizendo que os testes estão falhando. Isso significa que eu tenho testes e que eles estão funcionando!
— Bruno Rocha
Temos uma aplicação web rodando, mas quando tentamos acessar a raíz dela, ela nos diz que a página não está definida, não foi encontrada (é o que nos diz o código 404).
Criando nossa primeira página
O Flask facilita muito a criação de aplicações web. De forma simplificada a qualquer método Python pode ser atribuída uma URL. Isso é feito com um decorador:
Adicionando essas linhas no app.py, os testes passam:
Se a curiosidade for grande, esse artigo (em inglês) explica direitinho como o Flask.route(rule, *options) funciona: Things which aren't magic - Flask and @app.route.
Para garantir que tudo está certinho mesmo, podemos adicionar mais um teste. Queremos que a resposta do servidor seja um HTML:
Rodando os testes, veremos que agora temos dois testes. E ambos passam!
Eliminando repetições
Repararam que duas linhas se repetem nos métodos test_get() e test_content_type()?
Podemos usar um método especial da classe unittest.TestCase para reaproveitar essas linhas. O método TestCase.setUp() é executado ao iniciar cada teste, e através do self podemos acessar objetos de um método a partir de outro método:
Não vamos precisar nesse exemplo, mas o método TestCase.tearDown() é executado ao fim de cada teste (e não no início, como a setUp()). Ou seja, se precisar repetir algum comando sempre após cada teste, a unittest também faz isso para você.
- Preenchendo a página
Conteúdo como resposta
Temos um servidor web funcionando, mas não vemos nada na nossa aplicação web. Podemos verificar isso em três passos rápidos:
Primeiro adicionamos essas linhas ao app.py para que, quando executarmos o app.py (mas não quando ele for importado no tests.py), a aplicação web seja iniciada:
Depois executamos o arquivo:
Assim vemos no terminal essa mensagem:
Se acessarmos essa URL no nosso navegador, podemos ver a aplicação rodando: http://127.0.0.1:5000/.
E veremos que realmente não há nada, é uma página em branco.
Vamos mudar isso! Vamos construir o que seria uma página individual, mostrando quem a gente é. Na minha vou querer que esteja escrito (ao menos), meu nome. Então vamos escrever um teste para isso:
Feito isso, teremos uma nova mensagem de erro nos testes:
Essa mensagem nos diz que estamos comparando uma string com um objeto que é de outro tipo, que é representado por bytes. Não é isso que queremos. Como explicitamente passamos para o teste uma string com nosso nome, podemos assumir que é o self.response.data que vem codificado em bytes. Vamos decodificá-lo para string.
Bytes precisam ser decodificados para string (método decode). Strings precisam ser codificados para bytes para então mandarmos o conteúdo para o disco, para a rede (método encode).
— Henrique Bastos
Assim temos uma nova mensagem de erro:
Nossa página está vazia, logo o teste não consegue encontrar meu nome na página. Vamos resolver isso lá no app.py:
Agora temos os testes passando, e podemos verificar isso vendo que temos o nome na tela do navegador.
Apresentando o conteúdo com HTML
O Python e o Flask cuidam principalmente do back-end da apliacação web — o que ocorre “por trás dos panos” no lado do servidor.
Mas temos também o front-end, que é o que o usuário vê, a interface com a qual o usuário interage. Normalmente o front-end é papel de outras linguagens, como o HTML, o CSS e o JavaScript.
Vamos começar com um HTML básico, criando a pasta templates e dentro dela o arquivo home.html:
Se a gente abrir essa página no navegador já podemos ver que ela é um pouco menos do que o que a gente tinha antes. Então vamos alterar nosso test_content() para garantir que ao invés de termos somente a string com nosso nome na aplicação, tempos esse template renderizado:
Assim vemos nossos testes falharem:
Criamos um HTML, mas ainda não estamos pedindo para o Flask utilizá-lo. Temos nossa home.html dentro da pasta templates pois é justamente lá que o Flask vai buscar templ
Discussion in the ATmosphere