Continuando nossa implementação, observamos que, para que os itens da lista fiquem disponível e possam ser consultados a posteriori faz-se necessário que os mesmos sejam armazenados em um meio de armazenamento persistente.
No caso do Django podemos contar com a disponibilidade de um mecanismo denominado Object-Relational Mapper (ORM) ou Mapeador Objeto-Relacional, que transforma objetos de nossa aplicação em tabelas de um banco de dados relacional.
Como destaca Percival (2017), nada melhor do que escrever um teste de unidade para conhecer melhor o mecanismo de ORM e como ele pode ser utilizado na melhoria de nossa aplicação. Desse modo, podemos criar uma nova classe de teste dentro de nosso arquivo lists/tests.py. O código da mesma segue abaixo. Observe que omitimos o restante do código que estava no arquivo tests.py para melhor visualização.
# Inserir abaixo do código já exitentefrom 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')
Aparentemente, o armazenamento de itens utilizando o ORM não é algo complicado. Os testes indicam que devemos ter um objeto do tipo Item, definirmos os valores de seus atributos (no caso esperamos ter um atributo text) e, em seguida, invocar o método save() nesse objeto. A mágica ocorre nesse momento em que o método save() é invocado e o objeto é então armazenado no banco de dados interno do Django.
Posteriormente, optamos pelo uso do método Item.objects.all() que recupera a lista de objetos já armazenada e fazemos os testes para termos certeza de que o que foi recuperado é o que havia sido salvo anteriormente.
Ao executar os testes unitários da forma como estão obtemos a saída abaixo como era de se esperar, ou seja, o teste está acusando que não foi capaz de encontrar a classe Item (linha 16).
Para iniciarmos as pequenas alterações com a intenção de fazer o teste acima avançar, precisamos editar o arquivo lists/models.py e criar dentro dele a classe Item conforme abaixo:
from django.db import models# Create your models here.classItem(object):pass
Com essa alteração o resultado da execução dos testes acusa o próximo erro, indicando que nosso objeto não contém um método save() (linha 12).
A solução para isso é transformar nosso objeto Item em um verdadeiro Model do Django, herdando da classe models.Model.
from django.db import models# Create your models here.classItem(models.Model):pass
Com isso, o resultado dos nossos testes avançam e passam a emitir a próxima mensagem de erro dada abaixo (linha 14):
(superlists) tdd@mlp:~/superlists/superlists$pythonmanage.pytestFound4test(s).Creatingtestdatabaseforalias'default'...Systemcheckidentifiednoissues (0 silenced)....E======================================================================ERROR:test_saving_and_retriving_items (lists.tests.ItemModelTest)----------------------------------------------------------------------Traceback (most recentcalllast): File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/backends/utils.py", line 105, in _execute
returnself.cursor.execute(sql,params) File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/backends/sqlite3/base.py", line 329, in execute
returnsuper().execute(query,params)sqlite3.OperationalError:nosuchtable:lists_itemTheaboveexceptionwasthedirectcauseofthefollowingexception:Traceback (most recentcalllast):File"/home/tdd/superlists/superlists/lists/tests.py",line27,intest_saving_and_retriving_itemsfirst_item.save()File"/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/models/base.py",line822,insaveself.save_base( File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/models/base.py", line 909, in save_base
updated=self._save_table( File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/models/base.py", line 1071, in _save_table
results=self._do_insert( File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/models/base.py", line 1112, in _do_insert
returnmanager._insert( File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/models/manager.py", line 87, in manager_method
returngetattr(self.get_queryset(), name)(*args, **kwargs) File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/models/query.py", line 1847, in _insert
returnquery.get_compiler(using=using).execute_sql(returning_fields) File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 1823, in execute_sql
cursor.execute(sql,params) File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/backends/utils.py", line 79, in execute
returnself._execute_with_wrappers( File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/backends/utils.py", line 92, in _execute_with_wrappers
returnexecutor(sql,params,many,context) File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/backends/utils.py", line 100, in _execute
withself.db.wrap_database_errors:File"/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/utils.py",line91,in__exit__raisedj_exc_value.with_traceback(traceback) fromexc_value File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/backends/utils.py", line 105, in _execute
returnself.cursor.execute(sql,params) File "/home/tdd/.pyenv/versions/superlists/lib/python3.10/site-packages/django/db/backends/sqlite3/base.py", line 329, in execute
returnsuper().execute(query,params)django.db.utils.OperationalError:nosuchtable:lists_item----------------------------------------------------------------------Ran4testsin0.032sFAILED (errors=1)Destroyingtestdatabaseforalias'default'...
A mensagem de erro é bem longa mas o mais importante é a informação apresentada nas linhas 14 e 47, ou seja, apesar de termos utilizado o ORM para modelar como um objeto será armazenado no banco de dados relacional, para que tudo funcione é necessário utilizar um segundo sistema que irá criar esse banco de dados. Na terminologia do Django, o sistema responsável por isso se chama de migrações ou migrations.
Primeira Migração de Banco de Dados
O sistema de migrações pode se pensado como um sistema de controle de versões para banco de dados. Ocorrendo qualquer alteração nas classes que devem ter seus objetos armazenados, é necessário realizar uma nova migração.
Por hora, basta sabermos como realizar a operação de migração, e ela é feita com o comando abaixo:
Feito isso, ao tentar reexecutar os testes observamos que a execução dos mesmos vai até encontrar o atributo .text (linha 12) que ainda não está declarado em nossa classe e, portanto, não está presente nos objetos Item criados.
(superlists) tdd@mlp:~/superlists/superlists$pythonmanage.pytestFound4test(s).Creatingtestdatabaseforalias'default'...Systemcheckidentifiednoissues (0 silenced)....E======================================================================ERROR:test_saving_and_retriving_items (lists.tests.ItemModelTest)----------------------------------------------------------------------Traceback (most recentcalllast):File"/home/tdd/superlists/superlists/lists/tests.py",line39,intest_saving_and_retriving_itemsself.assertEquals(first_saved_item.text,'The first (ever) list item')AttributeError:'Item'objecthasnoattribute'text'----------------------------------------------------------------------Ran4testsin0.025sFAILED (errors=1)Destroyingtestdatabaseforalias'default'...
Para incluir esse atributo, vamos editar o arquivo lists/models.py conforme abaixo. Como pode ser observado, estamos utilizando o tipo TextField() para o atributo text. O Django oferece muitos outros tipos que podem ser encontrados na documentação oficial em https://docs.djangoproject.com/en/3.2/ref/models/fields/.
from django.db import models# Create your models here.classItem(models.Model): text = models.TextField()
Entretanto, antes de reexecutarmos os testes, após a alteração, precisamos realizar uma nova migração do banco de dados. Alterou a classe Item é necessária nova migração.
(superlists) tdd@mlp:~/superlists/superlists$pythonmanage.pymakemigrationsIt is impossible to add a non-nullable field 'text' to item without specifying a default. This is because the database needs something to populate existing rows.
Pleaseselectafix:1) Provide a one-off default now (willbesetonallexistingrowswithanullvalueforthiscolumn)2) Quit and manually define a default value in models.py.Selectanoption:2(superlists) tdd@mlp:~/superlists/superlists$
Ao tentar realizar a migração, o Django nos alerta que não podemos deixar nenhum campo com valor null. Como deixamos, ele sugere duas possíveis alternativas. Escolhemos a segunda mas, caso queiramos eliminar esse problema, basta incluirmos o valor padrão na nossa classe conforme abaixo (linha 5) e realizar uma nova migração.
from django.db import models# Create your models here.classItem(models.Model): text = models.TextField(default='')
Antes de prosseguirmos podemos colocar as alterações sob controle de versão com os comandos abaixo:
(superlists) tdd@mlp:~/superlists/superlists$gitstatusNoramomasterYourbranchisuptodatewith'origin/master'.Changesnotstagedforcommit: (utilize"git add <arquivo>..."paraatualizaroqueserásubmetido) (use"git restore <file>..."todiscardchangesinworkingdirectory)modified:lists/models.pymodified:lists/tests.pyArquivosnãomonitorados: (utilize"git add <arquivo>..."paraincluiroqueserásubmetido)lists/migrations/0001_initial.pylists/migrations/0002_item_text.pynenhumamodificaçãoadicionadaàsubmissão (utilize "git add"e/ou"git commit -a")(superlists) tdd@mlp:~/superlists/superlists$gitaddlists(superlists) tdd@mlp:~/superlists/superlists$gitcommit-am"Model for list Items and associated migrations"[master 6cbb32a] Model for list Items and associated migrations4fileschanged,64insertions(+),1deletion(-)createmode100644lists/migrations/0001_initial.pycreatemode100644lists/migrations/0002_item_text.py(superlists) tdd@mlp:~/superlists/superlists$gitpushUsernamefor'https://github.com':aurimrvPasswordfor'https://aurimrv@github.com':Enumeratingobjects:13,done.Countingobjects:100% (13/13), done.Deltacompressionusingupto12threadsCompressingobjects:100% (8/8), done.Writingobjects:100% (8/8), 1.53 KiB |1.53MiB/s,done.Total8 (delta 2), reused 0 (delta0)remote:Resolvingdeltas:100% (2/2), completed with 2 local objects.Tohttps://github.com/aurimrv/superlists.giteef7e77..6cbb32amaster ->master
Salvando o POST no Banco de Dados
Com todos os testes unitários passando, está na hora de melhorarmos os testes unitários para garantir que um item submetido via POST seja salvo no banco de dados e, posteriormente, recuperado.
Para fazer isso, podemos modificar ligeiramente o teste test_can_save_a_POST_request, já presente no nosso conjunto de teste em lists/tests.py, conforme abaixo. Inserimos nele as linhas de 18 a 21. O que essas linhas fazem é 1) verificar se um novo item foi salvo no banco de dados; 2) recuperar o primeiro elemento salvo; e 3) verificar se o texto nesse elemento corresponde ao item submetido via POST.
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_can_save_a_POST_request(self): response = 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') self.assertIn('A new list item', response.content.decode()) self.assertTemplateUsed(response, 'home.html')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 os testes unitários, o resultado agora é o exibido abaixo:
Considerando a solução que chegamos até o momento, ela apresenta ainda ao menos dois problemas: 1) estamos salvando um item vazio a cada requisição para página inicial que não seja via POST; e 2) ainda não estamos armazenando no banco qualquer informação de quem a solicitou e, portanto, não conseguimos listar itens para pessoas distintas. Vamos anotar essas pendência para resolvê-las mais adiante.
Na verdade, podemos anotar em um rascunho as pendências que detectamos, tais como:
Não salvar itens em branco a cada requisição
Code smell: teste de POST é longo demais
Exibir vários itens da tabela
Aceitar mais de uma lista
" Sempre que identificamos problemas com antecedência, há a necessidade de fazer uma avaliação para saber se devemos parar o que estamos fazendo e recomeçar ou se devemos deixá-los de lado até mais tarde. Às vezes, terminar o que estamos fazendo ainda valerá mais a pena, enquanto em outras ocasiões o problema poderá ser tão significativo que justificaria parar e repensar." (Percival, 2017)
Podemos iniciar atacando o primeiro item da lista acima. Inicialmente, para garantir que nossa solução irá funcionar, vamos escrever um novo teste unitário para assegurar que uma chamada normal a nossa página não deveria gerar um Item salvo em banco de dados. Criamos então o teste test_only_saves_items_when_necessary conforme abaixo (linhas 15 a 17).
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): response = 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') self.assertIn('A new list item', response.content.decode()) self.assertTemplateUsed(response, 'home.html')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 teste teremos uma falha esperada pois Item.objects.count() está retornando 1 e deveria ser 0.
A solução acima trata com sucesso o nosso problema de salvar itens apenas quando a página aceitar requisições via POST. A variável new_item_text guarda o texto submetido via POST ou um string vazio em outras ocasiões. Além disso, observa-se que quando for uma requisição POST utilizamos em seguida o método Item.objects.create() que também cria um item e o armazena no banco sem a necessidade do save(). Com isso, conseguimos executar os testes com sucesso.
Vamos aproveitar o momento e colocar nosso código sob controle de versão antes de continuarmos.
superlists) tdd@mlp:~/superlists/superlists$ git statusNoramomasterYourbranchisuptodatewith'origin/master'.Changesnotstagedforcommit: (utilize"git add <arquivo>..."paraatualizaroqueserásubmetido) (use"git restore <file>..."todiscardchangesinworkingdirectory)modified:lists/tests.pymodified:lists/views.pynenhumamodificaçãoadicionadaàsubmissão (utilize "git add"e/ou"git commit -a")superlists) tdd@mlp:~/superlists/superlists$ git commit -am "Correcting view to deal with normal request and request via POST"
[master 4a93b0f] Correcting view to deal with normal request and request via POST2fileschanged,17insertions(+),1deletion(-)superlists) tdd@mlp:~/superlists/superlists$ git pushUsernamefor'https://github.com':aurimrvPasswordfor'https://aurimrv@github.com':Enumeratingobjects:9,done.Countingobjects:100% (9/9), done.Deltacompressionusingupto12threadsCompressingobjects:100% (5/5), done.Writingobjects:100% (5/5), 767 bytes |767.00KiB/s,done.Total5 (delta 3), reused 0 (delta0)remote:Resolvingdeltas:100% (3/3), completed with 3 local objects.Tohttps://github.com/aurimrv/superlists.git6cbb32a..4a93b0fmaster ->master
Redirecionando Após o POST
Um dos princípios de programação Web é a recomendação de que sempre seja feito um redirecionamento após um POST. A alteração do código de nossa aplicação para atender essa demanda trará outros benefícios, tais como a eliminação da necessidade de atribuir um string vazio para o new_item_text, melhorando o código nesse aspecto também.
Como sempre, antes de iniciarmos as alterações no código vamos redigir um novo teste unitário, ou aprimorar um existente, que verifique se estamos redirecionando após um POST e retornando à página principal. Segue o código completo do arquivo lists/tests.py já com a alteração nas linhas 26 e 27.
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): response = 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') self.assertEquals(response.status_code, 302) self.assertEquals(response['location'], '/')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')
Nessas linhas estavam os asserts abaixo:
self.assertIn('A new list item', response.content.decode()) self.assertTemplateUsed(response, 'home.html')
Fizemos essa substituição pois não mais esperamos mais de resposta um .content renderizado contra um template, mas sim que tenha ocorrido um redirecionamento. Ao executar os testes unitários modificados temos o seguinte resultado:
Uma boa prática em relação a casos de teste é que deveríamos tentar fazer com que cada teste se concentre em apenas um aspecto do sistema. Infelizmente, da forma como está, um único teste está com mais responsabilidades do que deveria e merece ser refatorado. O teste problemático é o test_can_save_a_POST_request que não apenas verifica se um POST está permitindo salvar o dado submetido, mas também está verificando se o redirecionamento esta ocorrendo adequadamente. O teste alterado abaixo separa essas duas atribuições em testes distintos, teste test_can_save_a_POST_request, implementado nas linhas de 19 a 24, e test_redirects_after_POST, implementado nas linhas de 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'], '/')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')
Para ter certeza que a refatoração do teste unitário foi bem sucedida, segue o resultado da execução dos testes:
Com essa alteração, já cumprimos os dois primeiros itens da nossa lista de tarefas:
Não salvar itens em branco a cada requisição
Code smell: teste de POST é longo demais
Exibir vários itens da tabela
Aceitar mais de uma lista
Renderizando Itens no Template
O terceiro item da lista iremos cumprir nesta seção. Para isso, inicialmente vamos escrever um novo teste unitário para verificar se nosso template permite a exibição de mais de um item na lista. O novo teste (test_displays_all_list_itens) está entre as linhas 32 e 39 baixo.
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'], '/')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 os testes unitários temos o erro AssertionError: 'itemey 1' not found in..., como exibido abaixo (linha 12).
(superlists) tdd@mlp:~/superlists/superlists$pythonmanage.pytestFound7test(s).Creatingtestdatabaseforalias'default'...Systemcheckidentifiednoissues (0 silenced)..F.....======================================================================FAIL:test_displays_all_list_itens (lists.tests.HomePageTest)----------------------------------------------------------------------Traceback (most recentcalllast):File"/home/tdd/superlists/superlists/lists/tests.py",line38,intest_displays_all_list_itensself.assertIn('itemey 1',response.content.decode())AssertionError: 'itemey 1' not found in '<html>\n\t<head>\n\t\t<title>To-Do lists</title>\n\t</head>\n\t<body>\n\t\t<h1>Your To-Do list</h1>\n\t\t<form method="POST">\n\t\t\t<input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />\n\t\t\t<input type="hidden" name="csrfmiddlewaretoken" value="6gCQJC9drgRZbw8SAVyQP5sXzOUR3AhG89LeCANeCICllniIO5F5sSbVzOS4nlD6">\n\t\t</form>\n\t\t<table id="id_list_table">\n\t\t\t<tr><td>1: </td></tr>\n\t\t</table>\n\t</body>\n</html>'
----------------------------------------------------------------------Ran7testsin0.029sFAILED (failures=1)Destroyingtestdatabaseforalias'default'...
Para oferecer uma solução para o template permitir tal exibição faremos uso da tag
{% for .. in .. %} (linha 12) que permite iterar sobre listas dentro do templatehome.html e o código do mesmo fica como abaixo:
Entretanto, ao executar os testes funcionais, esses ainda apresentam falha, conforme abaixo:
(superlists) tdd@mlp:~/superlists/superlists$pythonfunctional_tests.pyF======================================================================FAIL:test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)----------------------------------------------------------------------Traceback (most recentcalllast):File"/home/tdd/superlists/superlists/functional_tests.py",line31,intest_can_start_a_list_and_retrieve_it_laterself.assertIn('To-Do',self.browser.title)AssertionError:'To-Do'notfoundin'OperationalError at /'----------------------------------------------------------------------Ran1testin4.306sFAILED (failures=1)
Como podemos observar, pela descrição apresentado acima, está bem complicado entender o que deve ser feito para resolver o problema. Algumas vezes procurar a raiz de uma falha não é uma atividade trivial. No caso acima, a solução é tentar carregar nosso site diretamente no navagador para tentar entender o que está ocorrendo.
Como podemos observar na imagem abaixo, ao carregar o site http://localhost:8000, aparece a tela com uma mensagem semelhante a essa:
Observa-se que a exceção ocorreu pois o Django não é capaz de encontrar uma tabela, denominada lists_items no banco de dados de produção. Com isso podemos concluir que nos testes unitários, a classe django.test.TestCase do Django oferece um mecanismo para o gerenciamento de um banco de dados interno facilitando nosso vida. Entretanto, os testes funcionais, estendem a classe unittest.TestCase e, desse modo, não possuem essa funcionalidade embutida neles.
A solução nesse caso é criar essa tabela em um banco de dados que possa ser utilizada em produção. O Django oferece um banco de dados em memória (SQLite) que pode ser utilizado. A configuração do mesmo está disponível no arquivo superlists/settings.py. Os dados armazenados são salvos em um arquivo chamado de db.sqlite3, localizado no diretório base do projeto, ou seja, no mesmo diretório do arquivo manage.py.
Configurado o banco de dados que será utilizado, basta usarmos um outro aplicativo do Django, denominado migrate para gerar as tabelas exigidas por nossa aplicação.
Executado o migrate, a página web acessada diretamente não exibe mais as mensagens de depuração e a execução dos testes funcionais avançam.
(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 70, in test_can_start_a_list_and_retrieve_it_later
self.check_for_row_in_list_table('2: Use peacock feathers to make a fly')
File "/home/tdd/superlists/superlists/functional_tests.py", line 20, in check_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', '1: Use peacock feathers to make a fly']
----------------------------------------------------------------------
Ran 1 test in 6.016s
FAILED (failures=1)
Aparentemente, pela mensagem de erro, o que está faltando é apenas corrigirmos a numeração dos itens de nossa lista. Podemos fazer isso alterando o nosso template conforme abaixo, utilizando a tag {{ forloop.counter }}:
Finalmente, ao executar os testes funcionais novamente paramos na instrução self.fail('Finish the test!'), encerrando assim mais uma etapa da implementação da história de usuário até esse ponto.
(superlists) tdd@mlp:~/superlists/superlists$pythonfunctional_tests.pyF======================================================================FAIL:test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)----------------------------------------------------------------------Traceback (most recentcalllast):File"/home/tdd/superlists/superlists/functional_tests.py",line76,intest_can_start_a_list_and_retrieve_it_laterself.fail('Finish the test!')AssertionError:Finishthetest!----------------------------------------------------------------------Ran1testin5.943sFAILED (failures=1)
Entretanto, ao observar a página de nossa lista de itens, há um único inconveniente: a cada execução novos itens são duplicados e aparecem na tela da nossa aplicação.
1: Buy peacock featers
2: Use peacock feathers to make a fly
3: Buy peacock featers
4: Use peacock feathers to make a fly
5: Buy peacock featers
6: Use peacock feathers to make a fly
7: Buy peacock featers
8: Use peacock feathers to make a fly
Não é a solução ideal mas, no momento, podemos apenas remover o arquivo dq.sqlite3 e recriá-lo com os comandos abaixo:
Para encerrar essa parte, vamos colocar todo o código sob controle de versão:
(superlists) tdd@mlp:~/superlists/superlists$ git status
(superlists) tdd@mlp:~/superlists/superlists$ git commit -am "Redirect after POST, and show all items in template"
(superlists) tdd@mlp:~/superlists/superlists$ git push