layout | title | date | categories |
---|---|---|---|
post |
Sobre Testes, TDD, pytest, web e Tornado Web! |
2018-04-03 18:52:40 -0300 |
test tdd pytest tornado python |
Olá!
- Firefox instalado e atualizado
- python
- pip
- Google Chrome (opcional)
sudo pip install pyenv
- Quem desenvolve em python?
- Quem desenvolve com testes?
- Quem conhece testes?
- Quem trabalha com web?
- Quem conhece Selenium?
- Quem conhece Tornado?
- Quem trabalha com Tornado?
- Apresentar cenário caótico de desenvolvimento de uma app web simples
- Em seguida sugere uma mudança no cenário
Vamos construir um exemplo de teste simples sobre uma página web:
# src/test_functional.py
from selenium import webdriver
browser = webdriver.Firefox()
response = browser.get("http://localhost:8000")
assert "dimdim converter" in browser.title
Neste código, temos três fases:
- setup: abrimos um navegador web
- execução: visitamos uma página
- verificação: esperamos uma página contendo 'dimdim converter'
Vamos executar nosso script de testes:
python test_functional.py
Um erro ocorre, pois as condições esperadas não estão satisfeitas.
A mensagem de erro impressa no console informa que o endereço acessado não pode ser alcançado.
Então, vamos construir uma solução para eliminarmos esta mensagem e chegarmos a um programa funcionando.
Em python, possuimos um servidor web pronto para ser utilizado, através do módulo 'http.server'
Para iniciar nosso servidor web, execute o seguinte comando em um terminal auxiliar:
python -m http.server 8000
Agora, reexecute o teste:
python test_functional.py
Evolução! O erro anterior não ocorre mais
Neste ponto, montaremos uma página simples para atender a esta requisição.
Crie um arquivo chamado src/index.html com o seguinte conteúdo:
<!-- src/index.html -->
<html>
<head>
<title>dimdim converter</title>
</head>
</html>
Executando o teste novamente:
python test_functional.py
Sucesso!!
Nosso teste funcionou, nenhum erro foi reportado ou outra mensagem foi impressa.
Percebe-se que sempre uma nova instância do navegador é aberta a cada chamada e este mantêm-se aberto.
Para fechá-lo sempre que o teste funcionar com sucesso, podemos chamar o método browser.quit() do Selenium.
# test_functional.py
from selenium import webdriver
# setup: inicializações
browser = webdriver.Firefox()
# execução: exercitando o código
response = browser.get("http://localhost:8000")
# verificações de expectativas
assert "dimdim converter" in browser.title
# encerramento: liberação de recursos (cleanup)
browser.quit()
Mas, se ocorrer algum erro, ele ficará aberto. Podemos colocar nossa assertion dentro de um bloco try...finally, garantindo que sempre fechará.
# test_functional.py
from selenium import webdriver
# setup: inicializações
browser = webdriver.Firefox()
# execução: exercitando o código
response = browser.get("http://localhost:8000")
try:
# verificações de expectativas
assert "dimdim converter" in browser.title
finally:
# encerramento: liberação de recursos (cleanup)
browser.quit()
try:
assert "abracadabra" not in browser.title
finally:
browser.quit()
Adotada esta estratégia, percebe-se que esta manutenção de setup e finalização podem ser redundantes, não-escalável ou irregular.
Podemos reorganizar um pouco nosso teste, utilizando o módulo unittest, nativa do python.
Neste caso, a estrutura básica de teste é Orientada a Objetos. Portanto, devemos declarar uma Classe de Teste, herdando unittest.TestCase, além de sobrescrever alguns métodos auxiliares.
# pytest_functional.py
import unittest
from selenium import webdriver
class ConverterPageTestCase(unittest.TestCase):
def setUp(self):
self.browser = webdriver.Firefox()
def tearDown(self):
self.browser.quit()
def test_title_should_be_for_converter(self):
self.browser.get("http://localhost:8000")
self.assertIn("dimdim converter", self.browser.title)
def test_content_should_not_be_empty(self):
response = self.browser.get("http://localhost:8000")
self.assertGreater(len(self.browser.find_element_by_tag_name("body").text), 0)
Para executar o código acima, digite em seu console:
python -m unittest test_functional.py
Note que algumas mensagens serão apresentadas na tela, como o total de testes executados, sucessos e falhas.
Um error ocorrerá no teste test_content_should_not_be_empty
, onde é verificado se há algo dentro da tag . Para este teste funcionar, devemos modificar nosso arquivo HTML original.
<html>
<head>
<title>dimdim converter</title>
</head>
<body>abc</body>
</html>
Reexecutando nosso script:
python -m unittest test_functional.py
Sucesso novamente!
Nesta versão, organizamos nosso código utilizando unittest, separamos a fase de inicialização, a fase de limpeza de recursos e nossos testes.
Sobre testes:
Perceba a evolução em nosso código seguindo um ciclo regular:
https://twitter.com/bitfield/status/980843303246602240
- Write just enough of a failing test to specify the feature / reproduce the bug ("Red")
- Write just enough production code to make the test pass ("Green")
- Tidy and reorganize code and tests ("Refactor")
- Stop
Porém, adicionamos bastante código ao redor. Isto pode ser chamado de 'boillerplate code'. Algo muito declarativo, além de algumas aberturas para dependências de recursos não tão comuns a todos os testes.
Como alternativa, introduzimos o uso da biblioteca 'py.test', atualmente chamada simplesmente: 'pytest'. Muito sobre a biblioteca pode ser encontrado na página oficial do projeto, https://pytest.org.
Vamos reescrever os testes anteriores seguindo práticas adotadas pela biblioteca pytest
from selenium import webdriver
def test_title_should_be_for_converter():
browser = webdriver.Firefox()
browser.get("http://localhost:8000")
assert "dimdim converter" in browser.title
browser.quit()
def test_content_should_not_be_empty():
browser = webdriver.Firefox()
browser.get("http://localhost:8000")
assert len(browser.find_element_by_tag_name("body").text) > 0
browser.quit()
Para executar este código, será necessário instalar a biblioteca pytest. Para isso, digite em seu terminal:
pip install pytest
Em seguida, execute o comando:
pytest
Note a diferença entre os resultados do unittest e a execução do pytest.
Nesta nova versão do código, temos alguns pontos a notar:
Desta forma, o código está mais simples, sem dependência do unittest. Alguns detalhes podemos identificar:
- Pro: Retiramos import unittest
- Pro: Retiramos os métodos setUp e tearDown gerais da classe;
- Con: Introduzimos a inicialização e fechamento do navegador dentro dos testes. Será que podemos fazer melhor? Sim, normalmente sim.
- Con: Se ocorrer um erro, navegador permanecerá aberto
Para contornar o problema do navegador aberto, mesmo durante após erro, podemos contornar ingenuamente da seguinte maneira:
from selenium import webdriver
def test_title_should_be_for_converter():
browser = webdriver.Firefox()
try:
browser.get("http://localhost:8000")
assert "dimdim converter" in browser.title
finally:
browser.quit()
def test_content_should_not_be_empty():
browser = webdriver.Firefox()
try:
browser.get("http://localhost:8000")
assert len(browser.find_element_by_tag_name("body").text) > 0
finally:
browser.quit()
Retornamos ao boilerplate code inicial, com redundância de código e controles irregulares de recursos.
Para contornar estes pontos Cons, podemos adotar uma nova técnica de injeção de dependência, denominada "fixture" no contexto do pytest. Pela definição da documentação oficial, o conjunto de fixtures é um modo a prover uma base fixa onde os testes podem ser executados com confiança e repetidamente. Algumas características destas fixtures são:
. Nomes explícitos, disponíveis facilmente aos testes . Modularidade: São funções simples, que podem ser construídas utilizando outras fixtures ou recursos . Escalabilidade
No código anterior, identificamos o objeto 'browser' como um recurso comum e repetido a ambos os testes, além de precisarmos fechá-lo sempre, independente do resultado do teste.
Este será o primeiro recurso extraído a uma nova fixture.
import pytest
from selenium import webdriver
@pytest.fixture
def browser():
browser = webdriver.Firefox()
yield browser # atenção no uso do yield
browser.quit()
def test_title_should_be_for_converter(browser):
browser.get("http://localhost:8000")
assert "dimdim converter" in browser.title
def test_content_should_not_be_empty(browser):
browser.get("http://localhost:8000")
assert len(browser.find_element_by_tag_name("body").text) > 0
Para cada teste utilizando o navegador, uma nova janela é aberta no ambiente do usuário, a navegação é simulada e o navegador é encerrado.
Podemos evitar esta abertura executando o navegador em background, sem interface, utilizando o modo "headless". Isto simplifica o fluxo de execução.
Fixtures podem ser compostas por outras fixtures. Por exemplo, se quisermos executar os testes em outro navegador, como o Chrome, por exemplo. Podemos adicionar outras fixtures para cada navegador.
A seguir, um exemplo completo com fixtures compostas:
import pytest
from selenium import webdriver
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from selenium.webdriver.chrome.options import Options as ChromeOptions
@pytest.fixture
def browser_firefox():
options = FirefoxOptions()
options.set_headless(headless=True)
return webdriver.Firefox(firefox_options=options)
@pytest.fixture
def browser_chrome():
options = ChromeOptions()
options.set_headless(headless=True)
return webdriver.Chrome(chrome_options=options)
@pytest.fixture
def browser(browser_firefox):
b = browser_firefox
yield b
b.quit()
def test_title_should_be_for_converter(browser):
browser.get("http://localhost:8000")
assert "dimdim converter" in browser.title
def test_content_should_not_be_empty(browser):
browser.get("http://localhost:8000")
assert len(browser.find_element_by_tag_name("body").text) > 0
Então, vamos evoluir nosso conversor com algumas novas features.
Nossa aplicação deve atender ao usuário, são:
. Na primeira visita do usuário, a tela estará com a moeda de origem Dolar (USD) selecionada, com o valor 1 e moeda destino Real (BRL); . O usuario pode escolher entre Real e Dolar; . O usuário escolhe uma moeda de origem, inserir o valor e uma moeda de destino. Clica em enviar e uma nova página será desenhada com o valor do campo preenchido e um resultado;
Abaixo, iniciamos a estender o código com estas novas funcionalidades.
import pytest
from selenium import webdriver
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from selenium.webdriver.chrome.options import Options as ChromeOptions
@pytest.fixture
def browser_firefox():
options = FirefoxOptions()
options.set_headless(headless=True)
return webdriver.Firefox(firefox_options=options)
@pytest.fixture
def browser_chrome():
options = ChromeOptions()
options.set_headless(headless=True)
return webdriver.Chrome(chrome_options=options)
@pytest.fixture
def browser(browser_firefox):
b = browser_firefox
yield b
b.quit()
def test_title_should_be_for_converter(browser):
browser.get("http://localhost:8000")
assert "dimdim converter" in browser.title
def test_content_should_not_be_empty(browser):
browser.get("http://localhost:8000")
assert len(browser.find_element_by_tag_name("body").text) > 0
def test_initial_fields_setup_should_be_one_usd_to_brl(browser):
browser.get("http://localhost:8000")
assert "USD" == browser.find_element_by_css_selector("select.from_currency").text
assert "BRL" == browser.find_element_by_css_selector("select.to_currency").text
assert "1" == browser.find_element_by_css_selector("input").text
Perceba que este código começa a ficar mais complexo, com elementos não inerentes ao teste funcional, mas como suporte. A biblioteca pytest utiliza um arquivo chamado conftest.py que pode ser colocar na raíz de cada módulo. Ao iniciar a coleta de testes em um projeto, pasta, módulo, ele carrega o conteúdo deste arquivo e o disponibiliza na sessão dos testes.
Em nosso caso, podemos mover as fixtures relacionadas ao browser para este arquivo. Teremos um novo arquivo:
# src/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from selenium.webdriver.chrome.options import Options as ChromeOptions
@pytest.fixture
def browser_firefox():
options = FirefoxOptions()
options.set_headless(headless=True)
return webdriver.Firefox(firefox_options=options)
@pytest.fixture
def browser_chrome():
options = ChromeOptions()
options.set_headless(headless=True)
return webdriver.Chrome(chrome_options=options)
@pytest.fixture
def browser(browser_firefox):
b = browser_firefox
yield b
b.quit()
# src/test_functional.py
def test_title_should_be_for_converter(browser):
browser.get("http://localhost:8000")
assert "dimdim converter" in browser.title
def test_content_should_not_be_empty(browser):
browser.get("http://localhost:8000")
assert len(browser.find_element_by_tag_name("body").text) > 0
def test_initial_fields_setup_should_be_one_usd_to_brl(browser):
browser.get("http://localhost:8000")
assert "USD" == browser.find_element_by_css_selector("select.from_currency").text
assert "BRL" == browser.find_element_by_css_selector("select.to_currency").text
assert "1" == browser.find_element_by_css_selector("input").text
O arquivo resultante test_functional.py está simplificado, sem dependências ou complexidade não inerente aos testes funcionais. O novo arquivo conftest.py
torna disponíveis recursos que outros módulos podem utilizar.