6.2 Iterando para um Novo Design da Aplicação

O processo para evoluir uma aplicação é trabalhoso e deve ser feito de forma incremental. Às vezes dá aquele desejo de sairmos modificando tudo quanto antes, mas, nessas horas temos que nos conter e planejar as ações a serem tomadas. Não há necessidade de alterarmos todo o projeto e arquitetura da aplicação de uma única vez.

Considerando nossa lista de tarefas apresentada no início do capítulo, o Teste Funcional, devido à falha apresentada, sugere que devemos iniciar pelo segundo item, ou seja, oferecer URLs únicos para cada lista.

Para dar início a mudança, precisamos alterar nosso teste unitário para refletir o que desejamos. O teste alterado fica conforme abaixo:

Alteremos o método test_redirects_after_POST para que, o redirecionamento seja para outro URL, diferente de '/'. No caso, usamos a URL /lists/the-only-list-in-the-world/ (linhas 26 a 30).

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

	def test_only_saves_items_when_necessary(self):
		self.client.get('/')
		self.assertEquals(Item.objects.count(), 0)

	def test_can_save_a_POST_request(self):
		self.client.post('/', data={'item_text': 'A new list item'})

		self.assertEquals(Item.objects.count(), 1)
		new_item = Item.objects.first()
		self.assertEquals(new_item.text, 'A new list item')

	def test_redirects_after_POST(self):
		response = self.client.post('/', data={'item_text': 'A new list item'})

		self.assertEquals(response.status_code, 302)
		self.assertEquals(response['location'], '/lists/the-only-list-in-the-world/')

	def test_displays_all_list_itens(self):
		Item.objects.create(text='itemey 1')
		Item.objects.create(text='itemey 2')

		response = self.client.get('/')

		self.assertIn('itemey 1', response.content.decode())
		self.assertIn('itemey 2', response.content.decode())


from lists.models import Item

class ItemModelTest(TestCase):

	def test_saving_and_retriving_items(self):
		first_item = Item()
		first_item.text = 'The first (ever) list item'
		first_item.save()

		second_item = Item()
		second_item.text = 'Item the second'
		second_item.save()

		saved_items = Item.objects.all()
		self.assertEquals(saved_items.count(),2)

		first_saved_item = saved_items[0]
		second_saved_item = saved_items[1]

		self.assertEquals(first_saved_item.text, 'The first (ever) list item')
		self.assertEquals(second_saved_item.text, 'Item the second')

Ao executar esse os testes unitários acima temos a seguinte saída:

(superlists) tdd@mlp:~/superlists/superlists$ python manage.py test lists
Found 7 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....F..
======================================================================
FAIL: test_redirects_after_POST (lists.tests.HomePageTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/lists/tests.py", line 30, in test_redirects_after_POST
    self.assertEquals(response['location'], '/lists/the-only-list-in-the-world/')
AssertionError: '/' != '/lists/the-only-list-in-the-world/'
- /
+ /lists/the-only-list-in-the-world/


----------------------------------------------------------------------
Ran 7 tests in 0.033s

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

Para resolver o primeiro problema detectado (AssertionError: '/' != '/lists/the-only-list-in-the-world/'- linha 12) podemos alterar nossa função home_page no arquivo lists/views.py, conforme abaixo:

Para corrigir o problema precisamos editar nossa função de view conforme abaixo (linha 8):

from django.shortcuts import redirect, render
from lists.models import Item

# Create your views here.
def home_page(request):
	if request.method == 'POST':
		Item.objects.create(text=request.POST['item_text'])
		return redirect('/lists/the-only-list-in-the-world/')

	items = Item.objects.all()
	return render(request, 'home.html', {'items': items})

Fica claro que essa alteração fará o teste unitário passar, mas outros testes falharem, como os testes funcionais:

(superlists) tdd@mlp:~/superlists/superlists$ python manage.py test functional_tests
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
EE
======================================================================
ERROR: test_can_start_a_list_for_one_user (functional_tests.tests.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/functional_tests/tests.py", line 66, in test_can_start_a_list_for_one_user
    self.wait_for_row_in_list_table('1: Buy peacock feathers')
  File "/home/tdd/superlists/superlists/functional_tests/tests.py", line 29, in wait_for_row_in_list_table
    raise e
  File "/home/tdd/superlists/superlists/functional_tests/tests.py", line 23, in wait_for_row_in_list_table
    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


======================================================================
ERROR: test_multiple_users_can_start_lists_at_different_urls (functional_tests.tests.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/functional_tests/tests.py", line 96, in test_multiple_users_can_start_lists_at_different_urls
    self.wait_for_row_in_list_table('1: Buy peacock feathers')
  File "/home/tdd/superlists/superlists/functional_tests/tests.py", line 29, in wait_for_row_in_list_table
    raise e
  File "/home/tdd/superlists/superlists/functional_tests/tests.py", line 23, in wait_for_row_in_list_table
    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 2 tests in 29.210s

FAILED (errors=2)
Destroying test database for alias 'default'...

Isso que provocamos é chamado de regressão, ou seja, não apenas o teste inicial esta falhando, mas outros passaram a falhar.

Para continuar, inicialmente vamos escrever um novo teste unitário para avaliar o retorno nessa nova URL. O arquivo lists/tests.py passará a ter o seguinte conteúdo. Observa-se que uma nova classe de teste foi adicionada no arquivo (ListViewTest) com um método de teste denominado test_display_all_items (linhas 43 a 51).

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

	def test_only_saves_items_when_necessary(self):
		self.client.get('/')
		self.assertEquals(Item.objects.count(), 0)

	def test_can_save_a_POST_request(self):
		self.client.post('/', data={'item_text': 'A new list item'})

		self.assertEquals(Item.objects.count(), 1)
		new_item = Item.objects.first()
		self.assertEquals(new_item.text, 'A new list item')

	def test_redirects_after_POST(self):
		response = self.client.post('/', data={'item_text': 'A new list item'})

		self.assertEquals(response.status_code, 302)
		self.assertEquals(response['location'], '/lists/the-only-list-in-the-world/')

	def test_displays_all_list_itens(self):
		Item.objects.create(text='itemey 1')
		Item.objects.create(text='itemey 2')

		response = self.client.get('/')

		self.assertIn('itemey 1', response.content.decode())
		self.assertIn('itemey 2', response.content.decode())



class ListViewTest(TestCase):
	def test_displays_all_list_itens(self):
		Item.objects.create(text='itemey 1')
		Item.objects.create(text='itemey 2')

		response = self.client.get('/lists/the-only-list-in-the-world/')

		self.assertContains(response, 'itemey 1')
		self.assertContains(response, 'itemey 2')



from lists.models import Item

class ItemModelTest(TestCase):

	def test_saving_and_retriving_items(self):
		first_item = Item()
		first_item.text = 'The first (ever) list item'
		first_item.save()

		second_item = Item()
		second_item.text = 'Item the second'
		second_item.save()

		saved_items = Item.objects.all()
		self.assertEquals(saved_items.count(),2)

		first_saved_item = saved_items[0]
		second_saved_item = saved_items[1]

		self.assertEquals(first_saved_item.text, 'The first (ever) list item')
		self.assertEquals(second_saved_item.text, 'Item the second')

Com a execução desses testes, obtemos um erro 404, pois a URL ainda não existe.

(superlists) tdd@mlp:~/superlists/superlists$ python manage.py test lists
Found 8 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......F
======================================================================
FAIL: test_displays_all_list_itens (lists.tests.ListViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/lists/tests.py", line 50, in test_displays_all_list_itens
    self.assertContains(response, 'itemey 1')
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/test/testcases.py", line 524, in assertContains
    text_repr, real_count, msg_prefix = self._assert_contains(
  File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/test/testcases.py", line 487, in _assert_contains
    self.assertEqual(
AssertionError: 404 != 200 : Couldn't retrieve content: Response code was 404 (expected 200)

----------------------------------------------------------------------
Ran 8 tests in 0.060s

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

A correção do erro 404 passa pela alteração no nosso arquivo superlists/urls.py, fazendo com que o Django considere essa nova URL não prevista inicialmente, conforme abaixo:

from django.urls import path
from lists import views

urlpatterns = [
    path('', views.home_page, name='home'),
    path('lists/the-only-list-in-the-world/', views.view_list, name='view_list')
]

Ao reexecutar os testes unitários novamente, obtemos um novo erro, cuja correção implica em implementarmos a função view_lists para tratar a requisição nesse URL.

(superlists) tdd@mlp:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
Destroying test database for alias 'default'...
Traceback (most recent call last):
  File "manage.py", line 22, in <module>
    main()
  File "manage.py", line 18, in main
    execute_from_command_line(sys.argv)
...
  File "/home/mlptdd/superlists/superlists/superlists/urls.py", line 22, in <module>
    path('lists/the-only-list-in-the-world/', views.view_list, name='view_list')
AttributeError: module 'lists.views' has no attribute 'view_list'

A alteração no arquivo lists/views.py conforme abaixo, resolve o problema acima parcialmente.

from django.shortcuts import redirect, render
from lists.models import Item

# Create your views here.
def home_page(request):
	if request.method == 'POST':
		Item.objects.create(text=request.POST['item_text'])
		return redirect('/lists/the-only-list-in-the-world/')

	items = Item.objects.all()
	return render(request, 'home.html', {'items': items})

def view_list(request):
	pass

Reexecutando os testes unitários, obtemos a seguinte saída:

(superlists) tdd@mlp:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......E
======================================================================
ERROR: test_displays_all_list_itens (lists.tests.ListViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mlptdd/superlists/superlists/lists/tests.py", line 48, in test_displays_all_list_itens
...
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/core/handlers/base.py", line 309, in check_response
    raise ValueError(
ValueError: The view lists.views.view_list didn't return an HttpResponse object. 
It returned None instead.

----------------------------------------------------------------------
Ran 8 tests in 0.029s

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

Como era de se esperar, os testes mostram que a função recém-implementada não retorna um objeto do tipo HttpResponse e o teste falha. Para corrigi-la, vamos implementar a função view_list conforme abaixo:

from django.shortcuts import redirect, render
from lists.models import Item

# Create your views here.
def home_page(request):
	if request.method == 'POST':
		Item.objects.create(text=request.POST['item_text'])
		return redirect('/lists/the-only-list-in-the-world/')

	items = Item.objects.all()
	return render(request, 'home.html', {'items': items})

def view_list(request):
	items = Item.objects.all()
	return render(request, 'home.html', {'items': items})

Feita a correção, os testes unitários executam com sucesso.

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

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

Mas ao executar os testes funcionais, apesar de avançarem, continuam falhando e ainda não chegamos ao estado funcional que tínhamos antes de iniciar o capítulo.

(superlists) tdd@mlp:~/superlists/superlists$ python manage.py test functional_tests
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
FE
======================================================================
ERROR: test_multiple_users_can_start_lists_at_different_urls (functional_tests.tests.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/functional_tests/tests.py", line 112, in test_multiple_users_can_start_lists_at_different_urls
    page_text = self.browser.find_elements(By.TAG_NAME, 'body').text
AttributeError: 'list' object has no attribute 'text'

======================================================================
FAIL: test_can_start_a_list_for_one_user (functional_tests.tests.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tdd/superlists/superlists/functional_tests/tests.py", line 80, in test_can_start_a_list_for_one_user
    self.wait_for_row_in_list_table('2: Use peacock feathers to make a fly')
  File "/home/tdd/superlists/superlists/functional_tests/tests.py", line 29, in wait_for_row_in_list_table
    raise e
  File "/home/tdd/superlists/superlists/functional_tests/tests.py", line 25, in wait_for_row_in_list_table
    self.assertIn(row_text, [row.text for row in rows])
AssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Buy peacock feathers']

----------------------------------------------------------------------
Ran 2 tests in 24.628s

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

Como enfatiza Percival (2017), as mensagens acima não são assim tão esclarecedoras e, nessa horas, temos que assumir o papel do depurador e tentar descobrir o motivo das falhas. Ele argumenta que, sabemos que a página inicial está funcionando, pois nossos testes funcionais passaram da linha 65, portanto, temos certeza que um item foi adicionado a nossa lista.

Os testes unitários estão todos passando e, desse modo, sabemos que as URLs e views estão funcionando como deveriam, os templates corretos estão sendo renderizados e é capaz de tratar requisições POST. A view que criamos para only-list-in-the-world sabe como exibir seus itens, mas não sabe como tratar requisições POST e isso nos dá uma dica de onde o problema pode estar.

"...quanto todos os testes de unidade estiverem passando, mas os testes funcionais não, geralmente eles estão apontado para um problema que não foi coberto pelos testes unitários e, ... com frequência é um problema de template." (Percival, 2017)

Quando observamos nosso template, vimos que nosso form está assim: <form method="POST">. Com essa configuração, por padrão, o navegador envia os dados de volta para o mesmo URL que está no momento. Desse modo, os testes funcionaram enquanto estávamos usando sempre o mesmo URL, como mudamos a mesma para a página da only-list-in-the-world, isso deixa de funcionar. Para adotarmos a solução mais simples, vamos apenas incluir um atributo action e redirecionar para a nossa view existente que já funcionava para as requisições POST. Para isso, o código completo do template fica conforme abaixo:

<html>
	<head>
		<title>To-Do lists</title>
	</head>
	<body>
		<h1>Your To-Do list</h1>
		<form method="POST" action="/">
			<input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />
			{% csrf_token %}
		</form>
		<table id="id_list_table">
			{% for item in items %}
			<tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>
			{% endfor %}
		</table>
	</body>
</html>

Com a inclusão do action="/" na linha 7, os testes funcionais iniciais voltam a funcionar e atingimos um estado consistente com o que tínhamos antes. Vejamos abaixo a saída dos testes funcionais:

(superlists) auri@av:~/superlists/superlists$ python manage.py test functional_tests
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.F
======================================================================
FAIL: test_multiple_users_can_start_lists_at_different_urls (tests.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mlptdd/superlists/superlists/functional_tests/tests.py", line 110, in test_multiple_users_can_start_lists_at_different_urls
    self.assertNotIn('Buy peacock feathers', page_text)
AssertionError: 'Buy peacock feathers' unexpectedly found in 'Your To-Do list\n1: Buy peacock feathers'

----------------------------------------------------------------------
Ran 2 tests in 9.941s

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

Desse modo, é um ótimo momento para colocarmos o código sob controle de versão para darmos prosseguimento.

(superlists) auri@av:~/superlists/superlists$ git commit -am "Modifications to make system stable to deal with multiple URLs"
[master 8e35b4a] Modifications to make system stable to deal with multiple URLs
 4 files changed, 135 insertions(+), 1 deletion(-)
 create mode 100644 functional_tests/tests.py
(superlists) auri@av:~/superlists/superlists$ git push
Username for 'https://github.com': aurimrv
Password for 'https://aurimrv@github.com': 
Enumerating objects: 21, done.
Counting objects: 100% (21/21), done.
Delta compression using up to 12 threads
Compressing objects: 100% (10/10), done.
Writing objects: 100% (12/12), 1.05 KiB | 357.00 KiB/s, done.
Total 12 (delta 8), reused 0 (delta 0)
remote: Resolving deltas: 100% (8/8), completed with 7 local objects.
To https://github.com/aurimrv/superlists.git
   01f87a6..8e35b4a  master -> master

Last updated