3.6 Evoluindo o Teste Funcional

Na seção anterior finalizamos completando toda uma parte da história descrita no teste funcional. É hora de melhorá-lo pois a execução dos testes parou exatamente na mensagem que havíamos deixado nos testes nos lembrando de que o teste ainda não está completo.

Entretanto, antes de complementar o teste funcional, Percival (2017) nos apresenta diversos questionamentos que devem estar surgindo sobre a viabilidade do TDD. Algumas das questões que ele argumenta que as pessoas iniciando com TDD fazem são: "Será mesmo que isso vale a pena? Todos esses testes não são excessivos? Será que não estão duplicados? Esses testes não são muito triviais? Isso é mesmo utilizado na prática?". Ele comenta que ele mesmo se fez essas perguntas e que é preciso dedicação e persistência para compreender e colher os benefícios.

"O TDD é uma disciplina, e isso significa que não é algo que surge naturalmente; como muitas das compensações não são imediatas, mas só aparecem no longo prazo, você precisará se esforçar para segui-lo no momento." (Percival, 2017)

Então vamos em frente e vamos nos esforçar para dar nosso melhor nessa tarefa de usar testes no desenvolvimento de um produto de software.

O próximo passo então é estendermos o nosso teste funcional para avançar na história nele descrita. O trecho de código a seguir ilustra como complementamos algumas frases da história, além do ponto que continha o comando self.fail('Finish the test!') (linha 22 do arquivo functional_tests.py). Vide resultado da execução no final da Seção 3.5.1. A seguir, o comando que estava na linha 22 foi movido para a linha 60, e novos comandos foram inseridos para atender a outras funcionalidades demandadas pelo usuário.

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By

import time
import unittest

class NewVisitorTest(unittest.TestCase):

	def setUp(self):
		self.browser = webdriver.Firefox()

	def tearDown(self):
		self.browser.quit()

	def test_can_start_a_list_and_retrieve_it_later(self):
		# Edith ouviu falar de uma nova aplicação online interessante
		# para lista de tarefas. Ela decide verificar a homepage

		self.browser.get("http://localhost:8000")

		# Ela percebe que o título da página e o cabeçalho mencionam
		# listas de tarefas (to-do)

		self.assertIn('To-Do', self.browser.title)
		#header_text = self.browser.find_element_by_tag_name('h1').text
		header_text = self.browser.find_element(By.TAG_NAME, 'h1').text
		self.assertIn('To-Do', header_text)
		
		# Ela é convidada a inserir um item de tarefa imediatamente

		
		inputbox = self.browser.find_element(By.ID, 'id_new_item')
		self.assertEqual(
			inputbox.get_attribute('placeholder'),
			'Enter a to-do item'
		)

		# Ela digita "Buy peacock feathers" (Comprar penas de pavão)
		# em uma nova caixa de texto (o hobby de Edith é fazer iscas
		# para pesca com fly)

		inputbox.send_keys('Buy peacock feathers')


		# Quando ela tecla enter, a página é atualizada, e agora
		# a página lista "1 - Buy peacock feathers" como um item em 
		# uma lista de tarefas

		inputbox.send_keys(Keys.ENTER)
		time.sleep(1)

		table = self.browser.find_element(By.ID,'id_list_table')
		rows = table.find_elements(By.TAG_NAME, 'tr')
		self.assertTrue(
			any(row.text == '1: Buy peacock feathers' for row in rows)
		)

		# Ainda continua havendo uma caixa de texto convidando-a a 
		# acrescentar outro item. Ela insere "Use peacock feathers 
		# make a fly" (Usar penas de pavão para fazer um fly - 
		# Edith é bem metódica)

		self.fail('Finish the test!')

		# A página é atualizada novamente e agora mostra os dois
		# itens em sua lista

		# Edith se pergunta se o site lembrará de sua lista. Então
		# ela nota que o site gerou um URL único para ela -- há um 
		# pequeno texto explicativo para isso.

		# Ela acessa essa URL -- sua lista de tarefas continua lá.

		# Satisfeita, ela volta a dormir

if __name__ == '__main__':
	unittest.main()

Como pode ser observado acima, incluímos vários comandos novos no código de teste funcional. São, em sua maioria, métodos do Selenium, como o método find_element, responsável por localizar elementos na página web conforme parâmetro foenecido (By.TAG_NANE ou By.ID), ou send_keys, responsável por enviar dados para campos de formulário em páginas web.

A execução do teste acima apresenta, conforme esperado, uma falha, com a mensagem na linha 15 abaixo, ou seja, a execução do teste não foi capaz de localizar um componente h1 na página.

Antes de continuar podemos colocar o código de teste modificado sob controle de versão com os comandos que usamos periodicamente e repetidos abaixo:

Iniciando o Terceiro Passo do Ciclo do TDD (Refatorar)

O processo de refatoração envolve melhorar o código atual sem incluir novas funcionalidades. A adição de funcionalidade durante o processo de refatoração fatalmente levará a problemas indesejados. Além disso, jamais refatore o código sem que você tenha testes que permitam assegurar que possíveis alterações indesejadas sejam barradas.

No nosso exemplo, a refatoração será o uso de templates do Django para exibir o código HTML ao invés de embutir esse HTML diretamente no código da aplicação. Esse isolamento favorece a manutenção futura de nosso produto.

No Django, o recurso utilizado para isso são os templates. Para utilizá-lo, basta criar uma subpasta templates, abaixo de nosso diretório lists (lists/templates). O Django sai varrendo automaticamente essas pastas na busca por aquivos .html que referenciamos no nosso código.

No exemplo acima, criamos o arquivo lists/templates/home.html com o conteúdo que já estava embutido em nosso código views.py e refatoramos views.py conforme abaixo.

A função render acima, aceita um objeto do tipo requisição como primeiro parâmetro e o segundo é um template que será renderizado para exibição. Conforme comentado, o Django varre o diretório templates na busca pelo arquivo home.html indicado no segundo parâmetro.

Para saber se nossa refatoração funcionou basta reexecutar os testes:

Ops, parece que algo deu errado. Será que nossa refatoração quebrou o nosso código? Na verdade não. O que ocorreu com o erro acima é que ainda não registramos nossa aplicação no Django. Quando rodamos o comando pra criar nossa aplicação (python manage.py startapp lists - Seção 3.5). Após criar uma aplicação no Django, ela precisa ser registrada. Isso é feito incluíndo seu nome no arquivo settings.py, localizado no diretório superlists, um nível abaixo do arquivo manage.py.

Dentro do arquivo settings.py, procure pela seção INSTALLED_APPS e faça a inclusão de nossa aplicação lists ao final da lista, abaixo das aplicações já registradas por padrão pelo Django.

Feita a alteração podemos reexecutar os testes. Observe que ele ainda acusa um erro, mas isso é apenas porque em nosso template teclamos um ENTER após o fechamento da tag '</html>'. Desse modo, nosso assertTrue esta falhando pois a linha não termina apenas com '</html>' mas sim com algo como '</html>\n'.

Para evitar que isso ocorra, alteramos o nosso arquivo tests.py e usamos a função strip() da classe String do Python que elimina os caracteres em branco antes e após a String. O código dos testes ficaram conforme abaixo:

Com essa alteração (linhas 17 e 19 acima) nossos testes passam com sucesso, indicando que nossa refatoração foi bem sucedida.

Django Test Client

Existem diferentes formas de se testar se o Django está renderizando a página correta fazendo uso de nossos templates. Uma delas é fazer a renderização manual, comparando o que a view retorna. Para isso, o Django oferece uma função que auxilia neste processo: render_to_string. Veja a seguir, como fica o nosso caso de teste unitário fazendo uso dessa função:

Observe que as mudanças em relação ao teste anterior é o import na linha 4 e as linhas 18 e 19 que substituíram as antigas linhas de 17 a 19 (asserts). Com a chamada da função render_to_string, o Django nos devolve todo o conteúdo renderizado e armazena na variável expected_html (linha 18) que é comparada, na linha 19, com a resposta decodificada obtida da requisição (linha 17).

Entretanto, quando usamos templates, o Django oferece uma ferramenta ainda mais simples de verificarmos se nossa aplicação respondeu corretamente. Ela se chama Django Test Client. Para mais informações sobre essa ferramenta, o leitor interessado pode consultar a documentação oficial do Django em https://docs.djangoproject.com/en/3.2/topics/testing/tools/#the-test-client. Nosso caso de teste reescrito para fazer uso do Django Test Client é apresentado abaixo:

Observe que agora o nosso teste test_home_page_returns_correct_html ficou muito mais simples. Ao invés de chamarmos manualmente o objeto HttpRequest e de chamar a função de view, basta fazermos uma chamada a self.client.get, passando a URL desejada.

Finalmente, para fazermos o teste se o retorno foi bem sucedido, utilizamos o método assertTemplateUsed da classe TestCase do Django. Ele nos permite confrontar a resposta do cliente com o conteúdo do template de forma mais simples.

Conforme destaca Percival (2017), o ponto principal ao usarmos o Django Test Client é que, "ao invés de testarmos constantes, estamos testando nossa implementação", ou seja, eliminamos constantes do nosso código de teste e o deixamo menos sujeito a manutenções em função das alterações no código da aplicação. Isso é muito importante para minimizarmos os custos de automatização dos testes.

O resultado da execução do teste utilizando o Django Test Client é dado abaixo:

Se ainda não tivermos confiantes de que esse novo modo de testar o retorno da página está funcionando, podemos fazer uma alteração proposital no teste para verificar se o teste ira falhar (linha 13 - template trocado para 'wrong.html'). No exemplo abaixo, ao executar o teste, o resultado indica uma falha pois o template não foi encontrado.

Com a alteração, o resultado da execução do teste falha conforme abaixo:

Refatorado nosso caso de teste unitário, é hora de colocarmos as modificações sob controle de versão. Utilizamos, em sequência, os comandos: git status, git add ., git commit e git push. O resultado é mostrado abaixo:

Finalmente, para encerrar esta seção, terminamos com a dica de Percival (2017) sobre refatoração e TDD.

"Ao refatorar, trabalhe no código ou nos testes, mas não em ambos ao mesmo tempo." (Percival, 2017)

Corrigindo a Aplicação para Evoluir no Atendimento do Teste Funcional

Agora que refatoramos nosso teste unitário, podemos iniciar a correção da nossa aplicação para evoluir no atendimento do teste funcional. Para termos uma ideia, ao executar os testes funcionais, obteremos o seguinte resultado:

O erro reportado na linha 15 é de que não foi possível encontrar o elemento h1. Realmente, não usamos h1 em nosso template. Assim sendo, podemos melhorar nossa aplicação editando o arquivo lists/templates/home.html conforme abaixo:

Com a alteração, o resultado dos testes muda. Agora o Selenium não está encontrando o elemento com id="id_new_item" (linha 15 da mensagem abaixo).

Vamos resolver isso alterando novamente nosso template lists/templates/home.html. Podemos incluir o código HTML conforme abaixo:

Evoluímos, agora nossos testes param na mensagem abaixo:

Vamos melhorar nosso template para atender a essa demanda dos testes funcionais. Para isso, o código do nosso template lists/templates/home.htmlficará conforme abaixo. Utilizamos o atributo placeholder do HTML para dica para um item ser fornecido.

Avançamos mais um pouco e o resultado dos nossos testes agora indicam um erro por não encontrar o elemento com id="id_list_table" (linha 15 da mensagem abaixo).

Vamos a ele então. A correção do nosso template para atender a essa demanda do teste funcional pode ser conforme abaixo:

Com a inclusão da tag table, a execução dos testes para em um novo erro relacionado com a linha 55 do nosso arquivo functional_tests.py. Parece um erro meio obscuro e está relacionado com aquele assertTrue contendo o comando any dentro dele.

Da forma como o assertTrue está a mensagem de erro gerada não é muito significativa. A maioria dos métodos assert do unittest aceitam um último parâmetro que é uma string exibida quando o assert falha. Podemos utilizar esse recurso para oferecer uma mensagem de erro mais significativa nesse caso. O comando assertTrue poderia ser reescrito para:

Com isso, a mensagem exibida ficou mais clara, correto?! Entretanto, fazer esse teste passar irá demandar um esforço maior pois precisaremos processar os dados enviados no formulário. Faremos isso no próximo capítulo pois precisaremos, além de processar, armazenar os dados em um banco de dados para posteriormente recuperarmos os itens da lista.

Last updated

Was this helpful?