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.

(superlists) tdd@mlp:~/superlists/superlists$ python functional_tests.py 
E
======================================================================
ERROR: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/functional_tests.py", line 27, in test_can_start_a_list_and_retrieve_it_later
    header_text = self.browser.find_element(By.TAG_NAME, 'h1').text
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/selenium/webdriver/remote/webdriver.py", line 748, in find_element
    return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/selenium/webdriver/remote/webdriver.py", line 354, in execute
    self.error_handler.check_response(response)
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/selenium/webdriver/remote/errorhandler.py", line 229, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: h1; For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception
Stacktrace:
RemoteError@chrome://remote/content/shared/RemoteError.sys.mjs:8:8
WebDriverError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:193:5
NoSuchElementError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:511:5
dom.find/</<@chrome://remote/content/shared/DOM.sys.mjs:136:16


----------------------------------------------------------------------
Ran 1 test in 3.870s

FAILED (errors=1)

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:

(superlists) tdd@mlp:~/superlists/superlists$ git diff
(superlists) tdd@mlp:~/superlists/superlists$ git commit -am "Functional test now checks we can input a to-do item"
(superlists) tdd@mlp:~/superlists/superlists$ git push

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.

(superlists) tdd@mlp:~/superlists/superlists$ mkdir lists/templates
(superlists) tdd@mlp:~/superlists/superlists$ subl lists/templates/home.html
(superlists) tdd@mlp:~/superlists/superlists$ cat lists/templates/home.html 
<html>
	<title>To-Do lists</title>
</html>

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.

from django.shortcuts import render

# Create your views here.
def home_page(request):
	return render(request, 'home.html')

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:

(superlists) tdd@mlp:~/superlists/superlists$ python functional_tests.py 
F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/functional_tests.py", line 25, in test_can_start_a_list_and_retrieve_it_later
    self.assertIn('To-Do', self.browser.title)
AssertionError: 'To-Do' not found in 'TemplateDoesNotExist at /'

----------------------------------------------------------------------
Ran 1 test in 4.532s

FAILED (failures=1)

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.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'lists'
]

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'.

(superlists) tdd@mlp:~/superlists/superlists$ python manage.py test
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F.
======================================================================
FAIL: test_home_page_returns_correct_html (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/lists/tests.py", line 19, in test_home_page_returns_correct_html
    self.assertTrue(html.endswith('</html>'))
AssertionError: False is not true

----------------------------------------------------------------------
Ran 2 tests in 0.006s

FAILED (failures=1)
Destroying test database for alias 'default'...

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:

from django.urls import resolve
from django.test import TestCase
from django.http import HttpRequest

from lists.views import home_page

class HomePageTest(TestCase):

	def test_root_url_resolves_to_home_page_view(self):
		found = resolve('/')
		self.assertEquals(found.func, home_page)

	def test_home_page_returns_correct_html(self):
		request = HttpRequest()
		response = home_page(request)
		html = response.content.decode('utf-8')
		self.assertTrue(html.strip().startswith('<html>'))
		self.assertIn('<title>To-Do lists</title>', html)
		self.assertTrue(html.strip().endswith('</html>'))

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

(superlists) tdd@mlp:~/superlists/superlists$ python manage.py test
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.006s

OK
Destroying test database for alias 'default'...

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:

from django.urls import resolve
from django.test import TestCase
from django.http import HttpRequest
from django.template.loader import render_to_string

from lists.views import home_page

class HomePageTest(TestCase):

	def test_root_url_resolves_to_home_page_view(self):
		found = resolve('/')
		self.assertEquals(found.func, home_page)

	def test_home_page_returns_correct_html(self):
		request = HttpRequest()
		response = home_page(request)
		html = response.content.decode('utf-8')
		expected_html = render_to_string('home.html')
		self.assertEquals(html, expected_html)

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:

from django.urls import resolve
from django.test import TestCase
from lists.views import home_page

class HomePageTest(TestCase):

	def test_root_url_resolves_to_home_page_view(self):
		found = resolve('/')
		self.assertEquals(found.func, home_page)

	def test_home_page_returns_correct_html(self):
		response = self.client.get('/')
		self.assertTemplateUsed(response, 'home.html')

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:

(superlists) tdd@mlp:~/superlists/superlists$ python manage.py test
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.010s

OK
Destroying test database for alias 'default'...

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.

from django.urls import resolve
from django.test import TestCase
from lists.views import home_page

class HomePageTest(TestCase):

	def test_root_url_resolves_to_home_page_view(self):
		found = resolve('/')
		self.assertEquals(found.func, home_page)

	def test_home_page_returns_correct_html(self):
		response = self.client.get('/')
		self.assertTemplateUsed(response, 'wrong.html')

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

(superlists) tdd@mlp:~/superlists/superlists$ python manage.py test
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F.
======================================================================
FAIL: test_home_page_returns_correct_html (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/lists/tests.py", line 13, in test_home_page_returns_correct_html
    self.assertTemplateUsed(response, 'wrong.html')
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/test/testcases.py", line 712, in assertTemplateUsed
    self._assert_template_used(template_name, template_names, msg_prefix, count)
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/test/testcases.py", line 677, in _assert_template_used
    self.assertTrue(
AssertionError: False is not true : Template 'wrong.html' was not a template used to render the response. Actual template(s) used: home.html

----------------------------------------------------------------------
Ran 2 tests in 0.010s

FAILED (failures=1)
Destroying test database for alias 'default'...

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:

(superlists) tdd@mlp:~/superlists/superlists$ git status
No ramo main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (utilize "git add <arquivo>..." para atualizar o que será submetido)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   lists/tests.py
	modified:   lists/views.py
	modified:   superlists/settings.py

Arquivos não monitorados:
  (utilize "git add <arquivo>..." para incluir o que será submetido)
	lists/templates/

nenhuma modificação adicionada à submissão (utilize "git add" e/ou "git commit -a")
(superlists) tdd@mlp:~/superlists/superlists$ git add .
(superlists) tdd@mlp:~/superlists/superlists$ git commit -am "Refactor home page view to use template"
[main d9a2bb2] Refactor home page view to use template
 4 files changed, 7 insertions(+), 10 deletions(-)
 create mode 100644 lists/templates/home.html
(superlists) tdd@mlp:~/superlists/superlists$ git push
Username for 'https://github.com': aurimrv
Password for 'https://aurimrv@github.com': 
Enumerating objects: 15, done.
Counting objects: 100% (15/15), done.
Delta compression using up to 3 threads
Compressing objects: 100% (7/7), done.
Writing objects: 100% (9/9), 817 bytes | 817.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 6 local objects.
To https://github.com/aurimrv/superlists.git
   9168eb7..d9a2bb2  main -> main

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:

(superlists) tdd@mlp:~/superlists/superlists$ python functional_tests.py 
E
======================================================================
ERROR: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/functional_tests.py", line 27, in test_can_start_a_list_and_retrieve_it_later
    header_text = self.browser.find_element(By.TAG_NAME, 'h1').text
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/selenium/webdriver/remote/webdriver.py", line 748, in find_element
    return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/selenium/webdriver/remote/webdriver.py", line 354, in execute
    self.error_handler.check_response(response)
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/selenium/webdriver/remote/errorhandler.py", line 229, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: h1; For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception
Stacktrace:
RemoteError@chrome://remote/content/shared/RemoteError.sys.mjs:8:8
WebDriverError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:193:5
NoSuchElementError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:511:5
dom.find/</<@chrome://remote/content/shared/DOM.sys.mjs:136:16


----------------------------------------------------------------------
Ran 1 test in 3.884s

FAILED (errors=1)

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:

<html>
	<head>
		<title>To-Do lists</title>
	</head>
	<body>
		<h1>Your To-Do list</h1>
	</body>
</html>

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).

(superlists) tdd@mlp:~/superlists/superlists$ python functional_tests.py 
E
======================================================================
ERROR: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/functional_tests.py", line 33, in test_can_start_a_list_and_retrieve_it_later
    inputbox = self.browser.find_element(By.ID, 'id_new_item')
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/selenium/webdriver/remote/webdriver.py", line 748, in find_element
    return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/selenium/webdriver/remote/webdriver.py", line 354, in execute
    self.error_handler.check_response(response)
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/selenium/webdriver/remote/errorhandler.py", line 229, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [id="id_new_item"]; For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception
Stacktrace:
RemoteError@chrome://remote/content/shared/RemoteError.sys.mjs:8:8
WebDriverError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:193:5
NoSuchElementError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:511:5
dom.find/</<@chrome://remote/content/shared/DOM.sys.mjs:136:16


----------------------------------------------------------------------
Ran 1 test in 3.652s

FAILED (errors=1)

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

<html>
	<head>
		<title>To-Do lists</title>
	</head>
	<body>
		<h1>Your To-Do list</h1>
		<input id="id_new_item" />
	</body>
</html>

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

(superlists) tdd@mlp:~/superlists/superlists$ python functional_tests.py 
F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/functional_tests.py", line 34, in test_can_start_a_list_and_retrieve_it_later
    self.assertEqual(
AssertionError: '' != 'Enter a to-do item'
+ Enter a to-do item

----------------------------------------------------------------------
Ran 1 test in 4.099s

FAILED (failures=1)

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.

<html>
	<head>
		<title>To-Do lists</title>
	</head>
	<body>
		<h1>Your To-Do list</h1>
		<input id="id_new_item" placeholder="Enter a to-do item" />
	</body>
</html>

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).

(superlists) tdd@mlp:~/superlists/superlists$ python functional_tests.py 
E
======================================================================
ERROR: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/functional_tests.py", line 53, in test_can_start_a_list_and_retrieve_it_later
    table = self.browser.find_element(By.ID,'id_list_table')
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/selenium/webdriver/remote/webdriver.py", line 748, in find_element
    return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/selenium/webdriver/remote/webdriver.py", line 354, in execute
    self.error_handler.check_response(response)
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/selenium/webdriver/remote/errorhandler.py", line 229, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: [id="id_list_table"]; For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#no-such-element-exception
Stacktrace:
RemoteError@chrome://remote/content/shared/RemoteError.sys.mjs:8:8
WebDriverError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:193:5
NoSuchElementError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:511:5
dom.find/</<@chrome://remote/content/shared/DOM.sys.mjs:136:16


----------------------------------------------------------------------
Ran 1 test in 4.847s

FAILED (errors=1)

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

<html>
	<head>
		<title>To-Do lists</title>
	</head>
	<body>
		<h1>Your To-Do list</h1>
		<input id="id_new_item" placeholder="Enter a to-do item" />
		<table id="id_list_table">
		</table>
	</body>
</html>

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.

(superlists) tdd@mlp:~/superlists/superlists$ python functional_tests.py 
F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/functional_tests.py", line 55, in test_can_start_a_list_and_retrieve_it_later
    self.assertTrue(
AssertionError: False is not true

----------------------------------------------------------------------
Ran 1 test in 5.013s

FAILED (failures=1)

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:

		self.assertTrue(
			any(row.text == '1: Buy peacock feathers' for row in rows),
			"New to-do item not appear in table"
		)

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.

(superlists) tdd@mlp:~/superlists/superlists$ python functional_tests.py 
F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/functional_tests.py", line 55, in test_can_start_a_list_and_retrieve_it_later
    self.assertTrue(
AssertionError: False is not true : New to-do item not appear in table

----------------------------------------------------------------------
Ran 1 test in 5.498s

FAILED (failures=1)

Last updated