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 resolvefrom django.test import TestCasefrom lists.views import home_pageclassHomePageTest(TestCase):deftest_root_url_resolves_to_home_page_view(self): found =resolve('/') self.assertEquals(found.func, home_page)deftest_home_page_returns_correct_html(self): response = self.client.get('/') self.assertTemplateUsed(response, 'home.html')deftest_only_saves_items_when_necessary(self): self.client.get('/') self.assertEquals(Item.objects.count(), 0)deftest_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')deftest_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/')deftest_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 ItemclassItemModelTest(TestCase):deftest_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:
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):
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$pythonmanage.pytestfunctional_testsFound2test(s).Creatingtestdatabaseforalias'default'...Systemcheckidentifiednoissues (0 silenced).EE======================================================================ERROR:test_can_start_a_list_for_one_user (functional_tests.tests.NewVisitorTest)----------------------------------------------------------------------Traceback (most recentcalllast):File"/home/tdd/superlists/superlists/functional_tests/tests.py",line66,intest_can_start_a_list_for_one_userself.wait_for_row_in_list_table('1: Buy peacock feathers')File"/home/tdd/superlists/superlists/functional_tests/tests.py",line29,inwait_for_row_in_list_tableraiseeFile"/home/tdd/superlists/superlists/functional_tests/tests.py",line23,inwait_for_row_in_list_tabletable=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
returnself.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
raiseexception_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:8WebDriverError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:193:5NoSuchElementError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:511:5dom.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 recentcalllast): 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",line29,inwait_for_row_in_list_tableraiseeFile"/home/tdd/superlists/superlists/functional_tests/tests.py",line23,inwait_for_row_in_list_tabletable=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
returnself.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
raiseexception_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:8WebDriverError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:193:5NoSuchElementError@chrome://remote/content/shared/webdriver/Errors.sys.mjs:511:5dom.find/</<@chrome://remote/content/shared/DOM.sys.mjs:136:16----------------------------------------------------------------------Ran2testsin29.210sFAILED (errors=2)Destroyingtestdatabaseforalias'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 resolvefrom django.test import TestCasefrom lists.views import home_pageclassHomePageTest(TestCase):deftest_root_url_resolves_to_home_page_view(self): found =resolve('/') self.assertEquals(found.func, home_page)deftest_home_page_returns_correct_html(self): response = self.client.get('/') self.assertTemplateUsed(response, 'home.html')deftest_only_saves_items_when_necessary(self): self.client.get('/') self.assertEquals(Item.objects.count(), 0)deftest_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')deftest_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/')deftest_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())classListViewTest(TestCase):deftest_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 ItemclassItemModelTest(TestCase):deftest_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$pythonmanage.pytestlistsFound8test(s).Creatingtestdatabaseforalias'default'...Systemcheckidentifiednoissues (0 silenced)........F======================================================================FAIL:test_displays_all_list_itens (lists.tests.ListViewTest)----------------------------------------------------------------------Traceback (most recentcalllast):File"/home/tdd/superlists/superlists/lists/tests.py",line50,intest_displays_all_list_itensself.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.060sFAILED (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:
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.
Reexecutando os testes unitários, obtemos a seguinte saída:
(superlists) tdd@mlp:~/superlists/superlists$pythonmanage.pytestlistsCreatingtestdatabaseforalias'default'...Systemcheckidentifiednoissues (0 silenced)........E======================================================================ERROR:test_displays_all_list_itens (lists.tests.ListViewTest)----------------------------------------------------------------------Traceback (most recentcalllast):File"/home/mlptdd/superlists/superlists/lists/tests.py",line48,intest_displays_all_list_itens...File"/home/mlptdd/superlists/lib/python3.8/site-packages/django/core/handlers/base.py",line309,incheck_responseraiseValueError(ValueError:Theviewlists.views.view_listdidn't return an HttpResponse object. It returned None instead.----------------------------------------------------------------------Ran 8 tests in 0.029sFAILED (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:
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$pythonmanage.pytestfunctional_testsFound2test(s).Creatingtestdatabaseforalias'default'...Systemcheckidentifiednoissues (0 silenced).FE======================================================================ERROR:test_multiple_users_can_start_lists_at_different_urls (functional_tests.tests.NewVisitorTest)----------------------------------------------------------------------Traceback (most recentcalllast): 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').textAttributeError:'list'objecthasnoattribute'text'======================================================================FAIL:test_can_start_a_list_for_one_user (functional_tests.tests.NewVisitorTest)----------------------------------------------------------------------Traceback (most recentcalllast):File"/home/tdd/superlists/superlists/functional_tests/tests.py",line80,intest_can_start_a_list_for_one_userself.wait_for_row_in_list_table('2: Use peacock feathers to make a fly')File"/home/tdd/superlists/superlists/functional_tests/tests.py",line29,inwait_for_row_in_list_tableraiseeFile"/home/tdd/superlists/superlists/functional_tests/tests.py",line25,inwait_for_row_in_list_tableself.assertIn(row_text, [row.text forrowinrows])AssertionError:'2: Use peacock feathers to make a fly'notfoundin ['1: Buy peacock feathers']----------------------------------------------------------------------Ran2testsin24.628sFAILED (failures=1, errors=1)Destroyingtestdatabaseforalias'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:
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:
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 URLs4fileschanged,135insertions(+),1deletion(-)createmode100644functional_tests/tests.py(superlists) auri@av:~/superlists/superlists$gitpushUsernamefor'https://github.com':aurimrvPasswordfor'https://aurimrv@github.com':Enumeratingobjects:21,done.Countingobjects:100% (21/21), done.Deltacompressionusingupto12threadsCompressingobjects:100% (10/10), done.Writingobjects:100% (12/12), 1.05 KiB |357.00KiB/s,done.Total12 (delta 8), reused 0 (delta0)remote:Resolvingdeltas:100% (8/8), completed with 7 local objects.Tohttps://github.com/aurimrv/superlists.git01f87a6..8e35b4amaster ->master